1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.User do
12 alias Pleroma.Activity
13 alias Pleroma.Notification
15 alias Pleroma.Registration
19 alias Pleroma.Web.ActivityPub.ActivityPub
20 alias Pleroma.Web.ActivityPub.Utils
21 alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
22 alias Pleroma.Web.OAuth
23 alias Pleroma.Web.OStatus
24 alias Pleroma.Web.RelMe
25 alias Pleroma.Web.Websub
29 @type t :: %__MODULE__{}
31 @primary_key {:id, Pleroma.FlakeId, autogenerate: true}
33 # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
34 @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])?)*$/
36 @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
37 @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
41 field(:email, :string)
43 field(:nickname, :string)
44 field(:password_hash, :string)
45 field(:password, :string, virtual: true)
46 field(:password_confirmation, :string, virtual: true)
47 field(:following, {:array, :string}, default: [])
48 field(:ap_id, :string)
50 field(:local, :boolean, default: true)
51 field(:follower_address, :string)
52 field(:search_rank, :float, virtual: true)
53 field(:search_type, :integer, virtual: true)
54 field(:tags, {:array, :string}, default: [])
55 field(:last_refreshed_at, :naive_datetime_usec)
56 field(:last_digest_emailed_at, :naive_datetime)
57 has_many(:notifications, Notification)
58 has_many(:registrations, Registration)
59 embeds_one(:info, Pleroma.User.Info)
64 def auth_active?(%User{info: %User.Info{confirmation_pending: true}}),
65 do: !Pleroma.Config.get([:instance, :account_activation_required])
67 def auth_active?(%User{}), do: true
69 def visible_for?(user, for_user \\ nil)
71 def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
73 def visible_for?(%User{} = user, for_user) do
74 auth_active?(user) || superuser?(for_user)
77 def visible_for?(_, _), do: false
79 def superuser?(%User{local: true, info: %User.Info{is_admin: true}}), do: true
80 def superuser?(%User{local: true, info: %User.Info{is_moderator: true}}), do: true
81 def superuser?(_), do: false
83 def avatar_url(user, options \\ []) do
85 %{"url" => [%{"href" => href} | _]} -> href
86 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
90 def banner_url(user, options \\ []) do
91 case user.info.banner do
92 %{"url" => [%{"href" => href} | _]} -> href
93 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
97 def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url
98 def profile_url(%User{ap_id: ap_id}), do: ap_id
99 def profile_url(_), do: nil
101 def ap_id(%User{nickname: nickname}) do
102 "#{Web.base_url()}/users/#{nickname}"
105 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
106 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
108 def user_info(%User{} = user) do
109 oneself = if user.local, do: 1, else: 0
112 following_count: length(user.following) - oneself,
113 note_count: user.info.note_count,
114 follower_count: user.info.follower_count,
115 locked: user.info.locked,
116 confirmation_pending: user.info.confirmation_pending,
117 default_scope: user.info.default_scope
121 def remote_user_creation(params) do
124 |> Map.put(:info, params[:info] || %{})
126 info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])
130 |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
131 |> validate_required([:name, :ap_id])
132 |> unique_constraint(:nickname)
133 |> validate_format(:nickname, @email_regex)
134 |> validate_length(:bio, max: 5000)
135 |> validate_length(:name, max: 100)
136 |> put_change(:local, false)
137 |> put_embed(:info, info_cng)
140 case info_cng.changes[:source_data] do
141 %{"followers" => followers} ->
143 |> put_change(:follower_address, followers)
146 followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
149 |> put_change(:follower_address, followers)
156 def update_changeset(struct, params \\ %{}) do
158 |> cast(params, [:bio, :name, :avatar])
159 |> unique_constraint(:nickname)
160 |> validate_format(:nickname, local_nickname_regex())
161 |> validate_length(:bio, max: 5000)
162 |> validate_length(:name, min: 1, max: 100)
165 def upgrade_changeset(struct, params \\ %{}) do
168 |> Map.put(:last_refreshed_at, NaiveDateTime.utc_now())
172 |> User.Info.user_upgrade(params[:info])
175 |> cast(params, [:bio, :name, :follower_address, :avatar, :last_refreshed_at])
176 |> unique_constraint(:nickname)
177 |> validate_format(:nickname, local_nickname_regex())
178 |> validate_length(:bio, max: 5000)
179 |> validate_length(:name, max: 100)
180 |> put_embed(:info, info_cng)
183 def password_update_changeset(struct, params) do
186 |> cast(params, [:password, :password_confirmation])
187 |> validate_required([:password, :password_confirmation])
188 |> validate_confirmation(:password)
190 OAuth.Token.delete_user_tokens(struct)
191 OAuth.Authorization.delete_user_authorizations(struct)
193 if changeset.valid? do
194 hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
197 |> put_change(:password_hash, hashed)
203 def reset_password(user, data) do
204 update_and_set_cache(password_update_changeset(user, data))
207 def register_changeset(struct, params \\ %{}, opts \\ []) do
208 confirmation_status =
209 if opts[:confirmed] || !Pleroma.Config.get([:instance, :account_activation_required]) do
215 info_change = User.Info.confirmation_changeset(%User.Info{}, confirmation_status)
219 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
220 |> validate_required([:name, :nickname, :password, :password_confirmation])
221 |> validate_confirmation(:password)
222 |> unique_constraint(:email)
223 |> unique_constraint(:nickname)
224 |> validate_exclusion(:nickname, Pleroma.Config.get([Pleroma.User, :restricted_nicknames]))
225 |> validate_format(:nickname, local_nickname_regex())
226 |> validate_format(:email, @email_regex)
227 |> validate_length(:bio, max: 1000)
228 |> validate_length(:name, min: 1, max: 100)
229 |> put_change(:info, info_change)
232 if opts[:external] do
235 validate_required(changeset, [:email])
238 if changeset.valid? do
239 hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
240 ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
241 followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
244 |> put_change(:password_hash, hashed)
245 |> put_change(:ap_id, ap_id)
246 |> unique_constraint(:ap_id)
247 |> put_change(:following, [followers])
248 |> put_change(:follower_address, followers)
254 defp autofollow_users(user) do
255 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
258 User.Query.build(%{nickname: candidates, local: true})
261 follow_all(user, autofollowed_users)
264 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
265 def register(%Ecto.Changeset{} = changeset) do
266 with {:ok, user} <- Repo.insert(changeset),
267 {:ok, user} <- autofollow_users(user),
268 {:ok, user} <- set_cache(user),
269 {:ok, _} <- Pleroma.User.WelcomeMessage.post_welcome_message_to_user(user),
270 {:ok, _} <- try_send_confirmation_email(user) do
275 def try_send_confirmation_email(%User{} = user) do
276 if user.info.confirmation_pending &&
277 Pleroma.Config.get([:instance, :account_activation_required]) do
279 |> Pleroma.Emails.UserEmail.account_confirmation_email()
280 |> Pleroma.Emails.Mailer.deliver_async()
288 def needs_update?(%User{local: true}), do: false
290 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
292 def needs_update?(%User{local: false} = user) do
293 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
296 def needs_update?(_), do: true
298 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
302 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
303 follow(follower, followed)
306 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
307 if not User.ap_enabled?(followed) do
308 follow(follower, followed)
314 def maybe_follow(%User{} = follower, %User{info: _info} = followed) do
315 if not following?(follower, followed) do
316 follow(follower, followed)
322 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
323 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
324 def follow_all(follower, followeds) do
327 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
328 |> Enum.map(fn %{follower_address: fa} -> fa end)
332 where: u.id == ^follower.id,
337 "array(select distinct unnest (array_cat(?, ?)))",
346 {1, [follower]} = Repo.update_all(q, [])
348 Enum.each(followeds, fn followed ->
349 update_follower_count(followed)
355 def follow(%User{} = follower, %User{info: info} = followed) do
356 user_config = Application.get_env(:pleroma, :user)
357 deny_follow_blocked = Keyword.get(user_config, :deny_follow_blocked)
359 ap_followers = followed.follower_address
362 following?(follower, followed) or info.deactivated ->
363 {:error, "Could not follow user: #{followed.nickname} is already on your list."}
365 deny_follow_blocked and blocks?(followed, follower) ->
366 {:error, "Could not follow user: #{followed.nickname} blocked you."}
369 if !followed.local && follower.local && !ap_enabled?(followed) do
370 Websub.subscribe(follower, followed)
375 where: u.id == ^follower.id,
376 update: [push: [following: ^ap_followers]],
380 {1, [follower]} = Repo.update_all(q, [])
382 {:ok, _} = update_follower_count(followed)
388 def unfollow(%User{} = follower, %User{} = followed) do
389 ap_followers = followed.follower_address
391 if following?(follower, followed) and follower.ap_id != followed.ap_id do
394 where: u.id == ^follower.id,
395 update: [pull: [following: ^ap_followers]],
399 {1, [follower]} = Repo.update_all(q, [])
401 {:ok, followed} = update_follower_count(followed)
405 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
407 {:error, "Not subscribed!"}
411 @spec following?(User.t(), User.t()) :: boolean
412 def following?(%User{} = follower, %User{} = followed) do
413 Enum.member?(follower.following, followed.follower_address)
416 def follow_import(%User{} = follower, followed_identifiers)
417 when is_list(followed_identifiers) do
419 followed_identifiers,
420 fn followed_identifier ->
421 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
422 {:ok, follower} <- maybe_direct_follow(follower, followed),
423 {:ok, _} <- ActivityPub.follow(follower, followed) do
427 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
434 def locked?(%User{} = user) do
435 user.info.locked || false
439 Repo.get_by(User, id: id)
442 def get_by_ap_id(ap_id) do
443 Repo.get_by(User, ap_id: ap_id)
446 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
447 # of the ap_id and the domain and tries to get that user
448 def get_by_guessed_nickname(ap_id) do
449 domain = URI.parse(ap_id).host
450 name = List.last(String.split(ap_id, "/"))
451 nickname = "#{name}@#{domain}"
453 get_cached_by_nickname(nickname)
456 def set_cache({:ok, user}), do: set_cache(user)
457 def set_cache({:error, err}), do: {:error, err}
459 def set_cache(%User{} = user) do
460 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
461 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
462 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
466 def update_and_set_cache(changeset) do
467 with {:ok, user} <- Repo.update(changeset) do
474 def invalidate_cache(user) do
475 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
476 Cachex.del(:user_cache, "nickname:#{user.nickname}")
477 Cachex.del(:user_cache, "user_info:#{user.id}")
480 def get_cached_by_ap_id(ap_id) do
481 key = "ap_id:#{ap_id}"
482 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
485 def get_cached_by_id(id) do
489 Cachex.fetch!(:user_cache, key, fn _ ->
493 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
494 {:commit, user.ap_id}
500 get_cached_by_ap_id(ap_id)
503 def get_cached_by_nickname(nickname) do
504 key = "nickname:#{nickname}"
506 Cachex.fetch!(:user_cache, key, fn ->
507 user_result = get_or_fetch_by_nickname(nickname)
510 {:ok, user} -> {:commit, user}
511 {:error, _error} -> {:ignore, nil}
516 def get_cached_by_nickname_or_id(nickname_or_id) do
517 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
520 def get_by_nickname(nickname) do
521 Repo.get_by(User, nickname: nickname) ||
522 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
523 Repo.get_by(User, nickname: local_nickname(nickname))
527 def get_by_email(email), do: Repo.get_by(User, email: email)
529 def get_by_nickname_or_email(nickname_or_email) do
530 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
533 def get_cached_user_info(user) do
534 key = "user_info:#{user.id}"
535 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
538 def fetch_by_nickname(nickname) do
539 ap_try = ActivityPub.make_user_from_nickname(nickname)
542 {:ok, user} -> {:ok, user}
543 _ -> OStatus.make_user(nickname)
547 def get_or_fetch_by_nickname(nickname) do
548 with %User{} = user <- get_by_nickname(nickname) do
552 with [_nick, _domain] <- String.split(nickname, "@"),
553 {:ok, user} <- fetch_by_nickname(nickname) do
554 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
556 {:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])
561 _e -> {:error, "not found " <> nickname}
566 @doc "Fetch some posts when the user has just been federated with"
567 def fetch_initial_posts(user) do
568 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
571 # Insert all the posts in reverse order, so they're in the right order on the timeline
572 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
573 &Pleroma.Web.Federator.incoming_ap_doc/1
577 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
578 def get_followers_query(%User{} = user, nil) do
579 User.Query.build(%{followers: user})
582 def get_followers_query(user, page) do
583 from(u in get_followers_query(user, nil))
584 |> User.Query.paginate(page, 20)
587 @spec get_followers_query(User.t()) :: Ecto.Query.t()
588 def get_followers_query(user), do: get_followers_query(user, nil)
590 def get_followers(user, page \\ nil) do
591 q = get_followers_query(user, page)
596 def get_followers_ids(user, page \\ nil) do
597 q = get_followers_query(user, page)
599 Repo.all(from(u in q, select: u.id))
602 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
603 def get_friends_query(%User{} = user, nil) do
604 User.Query.build(%{friends: user})
607 def get_friends_query(user, page) do
608 from(u in get_friends_query(user, nil))
609 |> User.Query.paginate(page, 20)
612 @spec get_friends_query(User.t()) :: Ecto.Query.t()
613 def get_friends_query(user), do: get_friends_query(user, nil)
615 def get_friends(user, page \\ nil) do
616 q = get_friends_query(user, page)
621 def get_friends_ids(user, page \\ nil) do
622 q = get_friends_query(user, page)
624 Repo.all(from(u in q, select: u.id))
627 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
628 def get_follow_requests(%User{} = user) do
630 Activity.follow_requests_for_actor(user)
631 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
632 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
633 |> group_by([a, u], u.id)
640 def increase_note_count(%User{} = user) do
642 |> where(id: ^user.id)
647 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
654 |> Repo.update_all([])
656 {1, [user]} -> set_cache(user)
661 def decrease_note_count(%User{} = user) do
663 |> where(id: ^user.id)
668 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
675 |> Repo.update_all([])
677 {1, [user]} -> set_cache(user)
682 def update_note_count(%User{} = user) do
686 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
690 note_count = Repo.one(note_count_query)
692 info_cng = User.Info.set_note_count(user.info, note_count)
696 |> put_embed(:info, info_cng)
698 update_and_set_cache(cng)
701 def update_follower_count(%User{} = user) do
702 follower_count_query =
703 User.Query.build(%{followers: user}) |> select([u], %{count: count(u.id)})
706 |> where(id: ^user.id)
707 |> join(:inner, [u], s in subquery(follower_count_query))
712 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
719 |> Repo.update_all([])
721 {1, [user]} -> set_cache(user)
726 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
727 def get_users_from_set(ap_ids, local_only \\ true) do
728 criteria = %{ap_id: ap_ids}
729 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
731 User.Query.build(criteria)
735 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
736 def get_recipients_from_activity(%Activity{recipients: to}) do
737 User.Query.build(%{recipients_from_activity: to, local: true})
741 def search(query, resolve \\ false, for_user \\ nil) do
742 # Strip the beginning @ off if there is a query
743 query = String.trim_leading(query, "@")
745 if resolve, do: get_or_fetch(query)
748 Repo.transaction(fn ->
749 Ecto.Adapters.SQL.query(Repo, "select set_limit(0.25)", [])
750 Repo.all(search_query(query, for_user))
756 def search_query(query, for_user) do
757 fts_subquery = fts_search_subquery(query)
758 trigram_subquery = trigram_search_subquery(query)
759 union_query = from(s in trigram_subquery, union_all: ^fts_subquery)
760 distinct_query = from(s in subquery(union_query), order_by: s.search_type, distinct: s.id)
762 from(s in subquery(boost_search_rank_query(distinct_query, for_user)),
763 order_by: [desc: s.search_rank],
768 defp boost_search_rank_query(query, nil), do: query
770 defp boost_search_rank_query(query, for_user) do
771 friends_ids = get_friends_ids(for_user)
772 followers_ids = get_followers_ids(for_user)
774 from(u in subquery(query),
779 CASE WHEN (?) THEN (?) * 1.3
780 WHEN (?) THEN (?) * 1.2
781 WHEN (?) THEN (?) * 1.1
784 u.id in ^friends_ids and u.id in ^followers_ids,
786 u.id in ^friends_ids,
788 u.id in ^followers_ids,
796 defp fts_search_subquery(term, query \\ User) do
799 |> String.replace(~r/\W+/, " ")
802 |> Enum.map(&(&1 <> ":*"))
813 setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
814 setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B'),
815 to_tsquery('simple', ?),
827 (setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
828 setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B')) @@ to_tsquery('simple', ?)
837 defp trigram_search_subquery(term) do
841 # ^1 gives 'Postgrex expected a binary, got 1' for some weird reason
842 search_type: fragment("?", 1),
845 "similarity(?, trim(? || ' ' || coalesce(?, '')))",
851 where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term)
855 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
858 fn blocked_identifier ->
859 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
860 {:ok, blocker} <- block(blocker, blocked),
861 {:ok, _} <- ActivityPub.block(blocker, blocked) do
865 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
872 def mute(muter, %User{ap_id: ap_id}) do
875 |> User.Info.add_to_mutes(ap_id)
879 |> put_embed(:info, info_cng)
881 update_and_set_cache(cng)
884 def unmute(muter, %{ap_id: ap_id}) do
887 |> User.Info.remove_from_mutes(ap_id)
891 |> put_embed(:info, info_cng)
893 update_and_set_cache(cng)
896 def subscribe(subscriber, %{ap_id: ap_id}) do
897 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
899 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
900 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
903 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
907 |> User.Info.add_to_subscribers(subscriber.ap_id)
910 |> put_embed(:info, info_cng)
911 |> update_and_set_cache()
916 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
917 with %User{} = user <- get_cached_by_ap_id(ap_id) do
920 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
923 |> put_embed(:info, info_cng)
924 |> update_and_set_cache()
928 def block(blocker, %User{ap_id: ap_id} = blocked) do
929 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
931 if following?(blocker, blocked) do
932 {:ok, blocker, _} = unfollow(blocker, blocked)
939 if subscribed_to?(blocked, blocker) do
940 {:ok, blocker} = unsubscribe(blocked, blocker)
946 if following?(blocked, blocker) do
947 unfollow(blocked, blocker)
950 {:ok, blocker} = update_follower_count(blocker)
954 |> User.Info.add_to_block(ap_id)
958 |> put_embed(:info, info_cng)
960 update_and_set_cache(cng)
963 # helper to handle the block given only an actor's AP id
964 def block(blocker, %{ap_id: ap_id}) do
965 block(blocker, get_cached_by_ap_id(ap_id))
968 def unblock(blocker, %{ap_id: ap_id}) do
971 |> User.Info.remove_from_block(ap_id)
975 |> put_embed(:info, info_cng)
977 update_and_set_cache(cng)
980 def mutes?(nil, _), do: false
981 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
983 def blocks?(user, %{ap_id: ap_id}) do
984 blocks = user.info.blocks
985 domain_blocks = user.info.domain_blocks
986 %{host: host} = URI.parse(ap_id)
988 Enum.member?(blocks, ap_id) ||
989 Enum.any?(domain_blocks, fn domain ->
994 def subscribed_to?(user, %{ap_id: ap_id}) do
995 with %User{} = target <- get_cached_by_ap_id(ap_id) do
996 Enum.member?(target.info.subscribers, user.ap_id)
1000 @spec muted_users(User.t()) :: [User.t()]
1001 def muted_users(user) do
1002 User.Query.build(%{ap_id: user.info.mutes})
1006 @spec blocked_users(User.t()) :: [User.t()]
1007 def blocked_users(user) do
1008 User.Query.build(%{ap_id: user.info.blocks})
1012 @spec subscribers(User.t()) :: [User.t()]
1013 def subscribers(user) do
1014 User.Query.build(%{ap_id: user.info.subscribers})
1018 def block_domain(user, domain) do
1021 |> User.Info.add_to_domain_block(domain)
1025 |> put_embed(:info, info_cng)
1027 update_and_set_cache(cng)
1030 def unblock_domain(user, domain) do
1033 |> User.Info.remove_from_domain_block(domain)
1037 |> put_embed(:info, info_cng)
1039 update_and_set_cache(cng)
1042 def deactivate(%User{} = user, status \\ true) do
1043 info_cng = User.Info.set_activation_status(user.info, status)
1047 |> put_embed(:info, info_cng)
1049 update_and_set_cache(cng)
1052 def update_notification_settings(%User{} = user, settings \\ %{}) do
1053 info_changeset = User.Info.update_notification_settings(user.info, settings)
1056 |> put_embed(:info, info_changeset)
1057 |> update_and_set_cache()
1060 @spec delete(User.t()) :: :ok
1061 def delete(%User{} = user),
1062 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:delete, user])
1064 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1065 def perform(:delete, %User{} = user) do
1066 {:ok, user} = User.deactivate(user)
1068 # Remove all relationships
1069 {:ok, followers} = User.get_followers(user)
1071 Enum.each(followers, fn follower -> User.unfollow(follower, user) end)
1073 {:ok, friends} = User.get_friends(user)
1075 Enum.each(friends, fn followed -> User.unfollow(user, followed) end)
1077 delete_user_activities(user)
1080 def delete_user_activities(%User{ap_id: ap_id} = user) do
1083 |> Activity.query_by_actor()
1084 |> Activity.with_preloaded_object()
1087 Repo.transaction(fn -> Enum.each(stream, &delete_activity(&1)) end, timeout: :infinity)
1092 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1093 Object.normalize(activity) |> ActivityPub.delete()
1096 defp delete_activity(_activity), do: "Doing nothing"
1098 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1099 Pleroma.HTML.Scrubber.TwitterText
1102 @default_scrubbers Pleroma.Config.get([:markup, :scrub_policy])
1104 def html_filter_policy(_), do: @default_scrubbers
1106 def fetch_by_ap_id(ap_id) do
1107 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1114 case OStatus.make_user(ap_id) do
1115 {:ok, user} -> {:ok, user}
1116 _ -> {:error, "Could not fetch by AP id"}
1121 def get_or_fetch_by_ap_id(ap_id) do
1122 user = get_cached_by_ap_id(ap_id)
1124 if !is_nil(user) and !User.needs_update?(user) do
1127 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1128 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1130 resp = fetch_by_ap_id(ap_id)
1132 if should_fetch_initial do
1133 with {:ok, %User{} = user} = resp do
1134 {:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])
1142 def get_or_create_instance_user do
1143 relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"
1145 if user = get_cached_by_ap_id(relay_uri) do
1149 %User{info: %User.Info{}}
1150 |> cast(%{}, [:ap_id, :nickname, :local])
1151 |> put_change(:ap_id, relay_uri)
1152 |> put_change(:nickname, nil)
1153 |> put_change(:local, true)
1154 |> put_change(:follower_address, relay_uri <> "/followers")
1156 {:ok, user} = Repo.insert(changes)
1162 def public_key_from_info(%{
1163 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1167 |> :public_key.pem_decode()
1169 |> :public_key.pem_entry_decode()
1175 def public_key_from_info(%{magic_key: magic_key}) do
1176 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1179 def get_public_key_for_ap_id(ap_id) do
1180 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1181 {:ok, public_key} <- public_key_from_info(user.info) do
1188 defp blank?(""), do: nil
1189 defp blank?(n), do: n
1191 def insert_or_update_user(data) do
1193 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1194 |> remote_user_creation()
1195 |> Repo.insert(on_conflict: :replace_all, conflict_target: :nickname)
1199 def ap_enabled?(%User{local: true}), do: true
1200 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1201 def ap_enabled?(_), do: false
1203 @doc "Gets or fetch a user by uri or nickname."
1204 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1205 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1206 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1208 # wait a period of time and return newest version of the User structs
1209 # this is because we have synchronous follow APIs and need to simulate them
1210 # with an async handshake
1211 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1212 with %User{} = a <- User.get_cached_by_id(a.id),
1213 %User{} = b <- User.get_cached_by_id(b.id) do
1221 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1222 with :ok <- :timer.sleep(timeout),
1223 %User{} = a <- User.get_cached_by_id(a.id),
1224 %User{} = b <- User.get_cached_by_id(b.id) do
1232 def parse_bio(bio) when is_binary(bio) and bio != "" do
1234 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1238 def parse_bio(_), do: ""
1240 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1241 # TODO: get profile URLs other than user.ap_id
1242 profile_urls = [user.ap_id]
1245 |> CommonUtils.format_input("text/plain",
1246 mentions_format: :full,
1247 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1252 def parse_bio(_, _), do: ""
1254 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1255 Repo.transaction(fn ->
1256 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1260 def tag(nickname, tags) when is_binary(nickname),
1261 do: tag(get_by_nickname(nickname), tags)
1263 def tag(%User{} = user, tags),
1264 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1266 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1267 Repo.transaction(fn ->
1268 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1272 def untag(nickname, tags) when is_binary(nickname),
1273 do: untag(get_by_nickname(nickname), tags)
1275 def untag(%User{} = user, tags),
1276 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1278 defp update_tags(%User{} = user, new_tags) do
1279 {:ok, updated_user} =
1281 |> change(%{tags: new_tags})
1282 |> update_and_set_cache()
1287 defp normalize_tags(tags) do
1290 |> Enum.map(&String.downcase(&1))
1293 defp local_nickname_regex do
1294 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1295 @extended_local_nickname_regex
1297 @strict_local_nickname_regex
1301 def local_nickname(nickname_or_mention) do
1304 |> String.split("@")
1308 def full_nickname(nickname_or_mention),
1309 do: String.trim_leading(nickname_or_mention, "@")
1311 def error_user(ap_id) do
1316 nickname: "erroruser@example.com",
1317 inserted_at: NaiveDateTime.utc_now()
1321 @spec all_superusers() :: [User.t()]
1322 def all_superusers do
1323 User.Query.build(%{super_users: true, local: true})
1327 def showing_reblogs?(%User{} = user, %User{} = target) do
1328 target.ap_id not in user.info.muted_reblogs
1332 The function returns a query to get users with no activity for given interval of days.
1333 Inactive users are those who didn't read any notification, or had any activity where
1334 the user is the activity's actor, during `inactivity_threshold` days.
1335 Deactivated users will not appear in this list.
1339 iex> Pleroma.User.list_inactive_users()
1342 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1343 def list_inactive_users_query(inactivity_threshold \\ 7) do
1344 negative_inactivity_threshold = -inactivity_threshold
1345 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1346 # Subqueries are not supported in `where` clauses, join gets too complicated.
1347 has_read_notifications =
1348 from(n in Pleroma.Notification,
1349 where: n.seen == true,
1351 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1354 |> Pleroma.Repo.all()
1356 from(u in Pleroma.User,
1357 left_join: a in Pleroma.Activity,
1358 on: u.ap_id == a.actor,
1359 where: not is_nil(u.nickname),
1360 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1361 where: u.id not in ^has_read_notifications,
1364 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1365 is_nil(max(a.inserted_at))
1370 Enable or disable email notifications for user
1374 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1375 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1377 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1378 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1380 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1381 {:ok, t()} | {:error, Ecto.Changeset.t()}
1382 def switch_email_notifications(user, type, status) do
1383 info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})
1386 |> put_embed(:info, info)
1387 |> update_and_set_cache()
1391 Set `last_digest_emailed_at` value for the user to current time
1393 @spec touch_last_digest_emailed_at(t()) :: t()
1394 def touch_last_digest_emailed_at(user) do
1395 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1397 {:ok, updated_user} =
1399 |> change(%{last_digest_emailed_at: now})
1400 |> update_and_set_cache()