defmodule Pleroma.User do
use Ecto.Schema
- import Ecto.{Changeset, Query}
- alias Pleroma.{Repo, User, Object, Web, Activity, Notification}
+ import Ecto.Changeset
+ import Ecto.Query
+
+ alias Pleroma.Repo
+ alias Pleroma.User
+ alias Pleroma.Object
+ alias Pleroma.Web
+ alias Pleroma.Activity
+ alias Pleroma.Notification
alias Comeonin.Pbkdf2
alias Pleroma.Formatter
alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
- alias Pleroma.Web.{OStatus, Websub, OAuth}
- alias Pleroma.Web.ActivityPub.{Utils, ActivityPub}
+ alias Pleroma.Web.OStatus
+ alias Pleroma.Web.Websub
+ alias Pleroma.Web.OAuth
+ alias Pleroma.Web.ActivityPub.Utils
+ alias Pleroma.Web.ActivityPub.ActivityPub
require Logger
"#{ap_id(user)}/followers"
end
- def follow_changeset(struct, params \\ %{}) do
- struct
- |> cast(params, [:following])
- |> validate_required([:following])
- end
-
def user_info(%User{} = user) do
oneself = if user.local, do: 1, else: 0
changeset
|> put_change(:password_hash, hashed)
|> put_change(:ap_id, ap_id)
+ |> unique_constraint(:ap_id)
|> put_change(:following, [followers])
|> put_change(:follower_address, followers)
else
@doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
def register(%Ecto.Changeset{} = changeset) do
with {:ok, user} <- Repo.insert(changeset),
- {:ok, _} <- try_send_confirmation_email(user),
- {:ok, user} <- autofollow_users(user) do
+ {:ok, user} <- autofollow_users(user),
+ {:ok, _} <- Pleroma.User.WelcomeMessage.post_welcome_message_to_user(user),
+ {:ok, _} <- try_send_confirmation_email(user) do
{:ok, user}
end
end
Pleroma.Config.get([:instance, :account_activation_required]) do
user
|> Pleroma.UserEmail.account_confirmation_email()
- |> Pleroma.Mailer.deliver()
+ |> Pleroma.Mailer.deliver_async()
else
{:ok, :noop}
end
end
end
- @doc "A mass follow for local users. Ignores blocks and has no side effects"
+ @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
@spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
def follow_all(follower, followeds) do
- followed_addresses = Enum.map(followeds, fn %{follower_address: fa} -> fa end)
+ followed_addresses =
+ followeds
+ |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
+ |> Enum.map(fn %{follower_address: fa} -> fa end)
q =
from(u in User,
where: u.id == ^follower.id,
- update: [set: [following: fragment("array_cat(?, ?)", u.following, ^followed_addresses)]]
+ update: [
+ set: [
+ following:
+ fragment(
+ "array(select distinct unnest (array_cat(?, ?)))",
+ u.following,
+ ^followed_addresses
+ )
+ ]
+ ]
)
{1, [follower]} = Repo.update_all(q, [], returning: true)
)
end
+ def update_follow_request_count(%User{} = user) do
+ subquery =
+ user
+ |> User.get_follow_requests_query()
+ |> select([a], %{count: count(a.id)})
+
+ User
+ |> where(id: ^user.id)
+ |> join(:inner, [u], s in subquery(subquery))
+ |> update([u, s],
+ set: [
+ info:
+ fragment(
+ "jsonb_set(?, '{follow_request_count}', ?::varchar::jsonb, true)",
+ u.info,
+ s.count
+ )
+ ]
+ )
+ |> Repo.update_all([], returning: true)
+ |> case do
+ {1, [user]} -> {:ok, user}
+ _ -> {:error, user}
+ end
+ end
+
def get_follow_requests(%User{} = user) do
q = get_follow_requests_query(user)
reqs = Repo.all(q)
Repo.all(query)
end
- def search(query, resolve \\ false, for_user \\ nil) do
+ def search(term, options \\ %{}) do
# Strip the beginning @ off if there is a query
- query = String.trim_leading(query, "@")
+ term = String.trim_leading(term, "@")
+ query = options[:query] || User
- if resolve, do: User.get_or_fetch_by_nickname(query)
+ if options[:resolve], do: get_or_fetch(term)
- fts_results = do_search(fts_search_subquery(query), for_user)
+ fts_results =
+ do_search(fts_search_subquery(term, query), options[:for_user], limit: options[:limit])
{:ok, trigram_results} =
Repo.transaction(fn ->
Ecto.Adapters.SQL.query(Repo, "select set_limit(0.25)", [])
- do_search(trigram_search_subquery(query), for_user)
+
+ do_search(trigram_search_subquery(term, query), options[:for_user], limit: options[:limit])
end)
Enum.uniq_by(fts_results ++ trigram_results, & &1.id)
end
- defp do_search(subquery, for_user, options \\ []) do
+ def all(page, page_size) do
+ from(
+ u in User,
+ limit: ^page_size,
+ offset: ^((page - 1) * page_size),
+ order_by: u.id
+ )
+ |> Repo.all()
+ end
+
+ def count_all_except_one(user) do
+ query =
+ from(
+ u in User,
+ where: u.id != ^user.id
+ )
+
+ Repo.aggregate(query, :count, :id)
+ end
+
+ defp do_search(subquery, for_user, options) do
q =
from(
s in subquery(subquery),
boost_search_results(results, for_user)
end
- defp fts_search_subquery(query) do
+ defp fts_search_subquery(term, query) do
processed_query =
- query
+ term
|> String.replace(~r/\W+/, " ")
|> String.trim()
|> String.split()
|> Enum.join(" | ")
from(
- u in User,
+ u in query,
select_merge: %{
search_rank:
fragment(
)
end
- defp trigram_search_subquery(query) do
+ defp trigram_search_subquery(term, query) do
from(
- u in User,
+ u in query,
select_merge: %{
search_rank:
fragment(
"similarity(?, trim(? || ' ' || coalesce(?, '')))",
- ^query,
+ ^term,
u.nickname,
u.name
)
},
- where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^query)
+ where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term)
)
end
)
end
+ def mute(muter, %User{ap_id: ap_id}) do
+ info_cng =
+ muter.info
+ |> User.Info.add_to_mutes(ap_id)
+
+ cng =
+ change(muter)
+ |> put_embed(:info, info_cng)
+
+ update_and_set_cache(cng)
+ end
+
+ def unmute(muter, %{ap_id: ap_id}) do
+ info_cng =
+ muter.info
+ |> User.Info.remove_from_mutes(ap_id)
+
+ cng =
+ change(muter)
+ |> put_embed(:info, info_cng)
+
+ update_and_set_cache(cng)
+ end
+
def block(blocker, %User{ap_id: ap_id} = blocked) do
# sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
blocker =
update_and_set_cache(cng)
end
+ def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
+
def blocks?(user, %{ap_id: ap_id}) do
blocks = user.info.blocks
domain_blocks = user.info.domain_blocks
end)
end
+ def muted_users(user),
+ do: Repo.all(from(u in User, where: u.ap_id in ^user.info.mutes))
+
def blocked_users(user),
do: Repo.all(from(u in User, where: u.ap_id in ^user.info.blocks))
update_and_set_cache(cng)
end
+ def maybe_local_user_query(local) do
+ if local, do: local_user_query(), else: User
+ end
+
def local_user_query do
from(
u in User,
def parse_bio(bio, _user) when bio == "", do: bio
def parse_bio(bio, user) do
- mentions = Formatter.parse_mentions(bio)
- tags = Formatter.parse_tags(bio)
-
emoji =
(user.info.source_data["tag"] || [])
|> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
end)
bio
- |> CommonUtils.format_input(mentions, tags, "text/plain", user_links: [format: :full])
+ |> CommonUtils.format_input("text/plain", mentions_format: :full)
+ |> elem(0)
|> Formatter.emojify(emoji)
end
{:ok, updated_user} =
user
|> change(%{tags: new_tags})
- |> Repo.update()
+ |> update_and_set_cache()
updated_user
end
inserted_at: NaiveDateTime.utc_now()
}
end
+
+ def all_superusers do
+ from(
+ u in User,
+ where: u.local == true,
+ where: fragment("?->'is_admin' @> 'true' OR ?->'is_moderator' @> 'true'", u.info, u.info)
+ )
+ |> Repo.all()
+ end
end