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
field(:follower_address, :string)
field(:search_rank, :float, virtual: true)
field(:tags, {:array, :string}, default: [])
+ field(:bookmarks, {:array, :string}, default: [])
field(:last_refreshed_at, :naive_datetime)
has_many(:notifications, Notification)
embeds_one(:info, Pleroma.User.Info)
"#{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
- following =
- (follower.following ++ Enum.map(followeds, fn %{follower_address: fa} -> fa end))
- |> Enum.uniq()
+ 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(select distinct unnest (array_cat(?, ?)))",
+ u.following,
+ ^followed_addresses
+ )
+ ]
+ ]
+ )
- {:ok, follower} =
- follower
- |> follow_changeset(%{following: following})
- |> update_and_set_cache
+ {1, [follower]} = Repo.update_all(q, [], returning: true)
Enum.each(followeds, fn followed ->
update_follower_count(followed)
end)
- {:ok, follower}
+ set_cache(follower)
end
def follow(%User{} = follower, %User{info: info} = followed) do
Websub.subscribe(follower, followed)
end
- following =
- [ap_followers | follower.following]
- |> Enum.uniq()
+ q =
+ from(u in User,
+ where: u.id == ^follower.id,
+ update: [push: [following: ^ap_followers]]
+ )
- follower =
- follower
- |> follow_changeset(%{following: following})
- |> update_and_set_cache
+ {1, [follower]} = Repo.update_all(q, [], returning: true)
{:ok, _} = update_follower_count(followed)
- follower
+ set_cache(follower)
end
end
ap_followers = followed.follower_address
if following?(follower, followed) and follower.ap_id != followed.ap_id do
- following =
- follower.following
- |> List.delete(ap_followers)
+ q =
+ from(u in User,
+ where: u.id == ^follower.id,
+ update: [pull: [following: ^ap_followers]]
+ )
- {:ok, follower} =
- follower
- |> follow_changeset(%{following: following})
- |> update_and_set_cache
+ {1, [follower]} = Repo.update_all(q, [], returning: true)
{:ok, followed} = update_follower_count(followed)
+ set_cache(follower)
+
{:ok, follower, Utils.fetch_latest_follow(follower, followed)}
else
{:error, "Not subscribed!"}
get_by_nickname(nickname)
end
+ def set_cache(user) do
+ Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
+ Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
+ Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
+ {:ok, user}
+ end
+
def update_and_set_cache(changeset) do
with {:ok, user} <- Repo.update(changeset) do
- Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
- Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
- Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
- {:ok, user}
+ set_cache(user)
else
e -> e
end
),
where:
fragment(
- "? @> ?",
+ "coalesce((?)->'object'->>'id', (?)->>'object') = ?",
+ a.data,
a.data,
- ^%{"object" => user.ap_id}
+ ^user.ap_id
)
)
end
- def get_follow_requests(%User{} = user) do
- q = get_follow_requests_query(user)
- reqs = Repo.all(q)
+ 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
users =
- Enum.map(reqs, fn req -> req.actor end)
- |> Enum.uniq()
- |> Enum.map(fn ap_id -> get_by_ap_id(ap_id) end)
- |> Enum.filter(fn u -> !is_nil(u) end)
- |> Enum.filter(fn u -> !following?(u, user) end)
+ user
+ |> User.get_follow_requests_query()
+ |> join(:inner, [a], u in User, a.actor == u.ap_id)
+ |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
+ |> group_by([a, u], u.id)
+ |> select([a, u], u)
+ |> Repo.all()
{:ok, users}
end
# Strip the beginning @ off if there is a query
query = String.trim_leading(query, "@")
- if resolve, do: User.get_or_fetch_by_nickname(query)
+ if resolve, do: get_or_fetch(query)
fts_results = do_search(fts_search_subquery(query), for_user)
Enum.uniq_by(fts_results ++ trigram_results, & &1.id)
end
+ def all_except_one(user) do
+ query = from(u in User, where: u.id != ^user.id)
+
+ Repo.all(query)
+ end
+
defp do_search(subquery, for_user, options \\ []) do
q =
from(
)
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?(nil, _), do: false
+ 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))
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
+ def bookmark(%User{} = user, status_id) do
+ bookmarks = Enum.uniq(user.bookmarks ++ [status_id])
+ update_bookmarks(user, bookmarks)
+ end
+
+ def unbookmark(%User{} = user, status_id) do
+ bookmarks = Enum.uniq(user.bookmarks -- [status_id])
+ update_bookmarks(user, bookmarks)
+ end
+
+ def update_bookmarks(%User{} = user, bookmarks) do
+ user
+ |> change(%{bookmarks: bookmarks})
+ |> update_and_set_cache
+ end
+
defp normalize_tags(tags) do
[tags]
|> List.flatten()
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