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
@type t :: %__MODULE__{}
+ @primary_key {:id, Pleroma.FlakeId, autogenerate: true}
+
@email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
@strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
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)
- {:ok, follower} =
- follower
- |> follow_changeset(%{following: following})
- |> update_and_set_cache
+ q =
+ from(u in User,
+ where: u.id == ^follower.id,
+ update: [
+ set: [
+ following:
+ fragment(
+ "array(select distinct unnest (array_cat(?, ?)))",
+ u.following,
+ ^followed_addresses
+ )
+ ]
+ ]
+ )
+
+ {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!"}
user.info.locked || false
end
+ def get_by_id(id) do
+ Repo.get_by(User, id: id)
+ end
+
def get_by_ap_id(ap_id) do
Repo.get_by(User, ap_id: ap_id)
end
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
Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
end
+ def get_cached_by_id(id) do
+ key = "id:#{id}"
+
+ ap_id =
+ Cachex.fetch!(:user_cache, key, fn _ ->
+ user = get_by_id(id)
+
+ if user do
+ Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
+ {:commit, user.ap_id}
+ else
+ {:ignore, ""}
+ end
+ end)
+
+ get_cached_by_ap_id(ap_id)
+ end
+
def get_cached_by_nickname(nickname) do
key = "nickname:#{nickname}"
Cachex.fetch!(:user_cache, key, fn _ -> get_or_fetch_by_nickname(nickname) end)
end
+ def get_cached_by_nickname_or_id(nickname_or_id) do
+ get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
+ end
+
def get_by_nickname(nickname) do
Repo.get_by(User, nickname: nickname) ||
if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
)
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
+ 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