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
13 alias Pleroma.Activity
15 alias Pleroma.Notification
17 alias Pleroma.Registration
19 alias Pleroma.RepoStreamer
22 alias Pleroma.Web.ActivityPub.ActivityPub
23 alias Pleroma.Web.ActivityPub.Utils
24 alias Pleroma.Web.CommonAPI
25 alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
26 alias Pleroma.Web.OAuth
27 alias Pleroma.Web.OStatus
28 alias Pleroma.Web.RelMe
29 alias Pleroma.Web.Websub
33 @type t :: %__MODULE__{}
35 @primary_key {:id, Pleroma.FlakeId, autogenerate: true}
37 # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
38 @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])?)*$/
40 @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
41 @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
45 field(:email, :string)
47 field(:nickname, :string)
48 field(:password_hash, :string)
49 field(:password, :string, virtual: true)
50 field(:password_confirmation, :string, virtual: true)
51 field(:following, {:array, :string}, default: [])
52 field(:ap_id, :string)
54 field(:local, :boolean, default: true)
55 field(:follower_address, :string)
56 field(:following_address, :string)
57 field(:search_rank, :float, virtual: true)
58 field(:search_type, :integer, virtual: true)
59 field(:tags, {:array, :string}, default: [])
60 field(:last_refreshed_at, :naive_datetime_usec)
61 field(:last_digest_emailed_at, :naive_datetime)
62 has_many(:notifications, Notification)
63 has_many(:registrations, Registration)
64 embeds_one(:info, User.Info)
69 def auth_active?(%User{info: %User.Info{confirmation_pending: true}}),
70 do: !Pleroma.Config.get([:instance, :account_activation_required])
72 def auth_active?(%User{}), do: true
74 def visible_for?(user, for_user \\ nil)
76 def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
78 def visible_for?(%User{} = user, for_user) do
79 auth_active?(user) || superuser?(for_user)
82 def visible_for?(_, _), do: false
84 def superuser?(%User{local: true, info: %User.Info{is_admin: true}}), do: true
85 def superuser?(%User{local: true, info: %User.Info{is_moderator: true}}), do: true
86 def superuser?(_), do: false
88 def avatar_url(user, options \\ []) do
90 %{"url" => [%{"href" => href} | _]} -> href
91 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
95 def banner_url(user, options \\ []) do
96 case user.info.banner do
97 %{"url" => [%{"href" => href} | _]} -> href
98 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
102 def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url
103 def profile_url(%User{ap_id: ap_id}), do: ap_id
104 def profile_url(_), do: nil
106 def ap_id(%User{nickname: nickname}) do
107 "#{Web.base_url()}/users/#{nickname}"
110 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
111 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
113 @spec ap_following(User.t()) :: Sring.t()
114 def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
115 def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
117 def user_info(%User{} = user, args \\ %{}) do
119 if args[:following_count],
120 do: args[:following_count],
121 else: user.info.following_count || following_count(user)
124 if args[:follower_count], do: args[:follower_count], else: user.info.follower_count
127 note_count: user.info.note_count,
128 locked: user.info.locked,
129 confirmation_pending: user.info.confirmation_pending,
130 default_scope: user.info.default_scope
132 |> Map.put(:following_count, following_count)
133 |> Map.put(:follower_count, follower_count)
136 def follow_state(%User{} = user, %User{} = target) do
137 follow_activity = Utils.fetch_latest_follow(user, target)
140 do: follow_activity.data["state"],
141 # Ideally this would be nil, but then Cachex does not commit the value
145 def get_cached_follow_state(user, target) do
146 key = "follow_state:#{user.ap_id}|#{target.ap_id}"
147 Cachex.fetch!(:user_cache, key, fn _ -> {:commit, follow_state(user, target)} end)
150 def set_follow_state_cache(user_ap_id, target_ap_id, state) do
153 "follow_state:#{user_ap_id}|#{target_ap_id}",
158 def set_info_cache(user, args) do
159 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user, args))
162 @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
163 def restrict_deactivated(query) do
165 where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info)
169 def following_count(%User{following: []}), do: 0
171 def following_count(%User{} = user) do
173 |> get_friends_query()
174 |> Repo.aggregate(:count, :id)
177 def remote_user_creation(params) do
178 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
179 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
181 params = Map.put(params, :info, params[:info] || %{})
182 info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])
186 |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
187 |> validate_required([:name, :ap_id])
188 |> unique_constraint(:nickname)
189 |> validate_format(:nickname, @email_regex)
190 |> validate_length(:bio, max: bio_limit)
191 |> validate_length(:name, max: name_limit)
192 |> put_change(:local, false)
193 |> put_embed(:info, info_cng)
196 case info_cng.changes[:source_data] do
197 %{"followers" => followers, "following" => following} ->
199 |> put_change(:follower_address, followers)
200 |> put_change(:following_address, following)
203 followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
206 |> put_change(:follower_address, followers)
213 def update_changeset(struct, params \\ %{}) do
214 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
215 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
218 |> cast(params, [:bio, :name, :avatar, :following])
219 |> unique_constraint(:nickname)
220 |> validate_format(:nickname, local_nickname_regex())
221 |> validate_length(:bio, max: bio_limit)
222 |> validate_length(:name, min: 1, max: name_limit)
225 def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
226 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
227 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
229 params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
230 info_cng = User.Info.user_upgrade(struct.info, params[:info], remote?)
241 |> unique_constraint(:nickname)
242 |> validate_format(:nickname, local_nickname_regex())
243 |> validate_length(:bio, max: bio_limit)
244 |> validate_length(:name, max: name_limit)
245 |> put_embed(:info, info_cng)
248 def password_update_changeset(struct, params) do
250 |> cast(params, [:password, :password_confirmation])
251 |> validate_required([:password, :password_confirmation])
252 |> validate_confirmation(:password)
256 @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
257 def reset_password(%User{id: user_id} = user, data) do
260 |> Multi.update(:user, password_update_changeset(user, data))
261 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
262 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
264 case Repo.transaction(multi) do
265 {:ok, %{user: user} = _} -> set_cache(user)
266 {:error, _, changeset, _} -> {:error, changeset}
270 def register_changeset(struct, params \\ %{}, opts \\ []) do
271 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
272 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
275 if is_nil(opts[:need_confirmation]) do
276 Pleroma.Config.get([:instance, :account_activation_required])
278 opts[:need_confirmation]
282 User.Info.confirmation_changeset(%User.Info{}, need_confirmation: need_confirmation?)
286 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
287 |> validate_required([:name, :nickname, :password, :password_confirmation])
288 |> validate_confirmation(:password)
289 |> unique_constraint(:email)
290 |> unique_constraint(:nickname)
291 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
292 |> validate_format(:nickname, local_nickname_regex())
293 |> validate_format(:email, @email_regex)
294 |> validate_length(:bio, max: bio_limit)
295 |> validate_length(:name, min: 1, max: name_limit)
296 |> put_change(:info, info_change)
299 if opts[:external] do
302 validate_required(changeset, [:email])
305 if changeset.valid? do
306 ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
307 followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
311 |> put_change(:ap_id, ap_id)
312 |> unique_constraint(:ap_id)
313 |> put_change(:following, [followers])
314 |> put_change(:follower_address, followers)
320 defp autofollow_users(user) do
321 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
324 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
327 follow_all(user, autofollowed_users)
330 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
331 def register(%Ecto.Changeset{} = changeset) do
332 with {:ok, user} <- Repo.insert(changeset),
333 {:ok, user} <- post_register_action(user) do
338 def post_register_action(%User{} = user) do
339 with {:ok, user} <- autofollow_users(user),
340 {:ok, user} <- set_cache(user),
341 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
342 {:ok, _} <- try_send_confirmation_email(user) do
347 def try_send_confirmation_email(%User{} = user) do
348 if user.info.confirmation_pending &&
349 Pleroma.Config.get([:instance, :account_activation_required]) do
351 |> Pleroma.Emails.UserEmail.account_confirmation_email()
352 |> Pleroma.Emails.Mailer.deliver_async()
360 def needs_update?(%User{local: true}), do: false
362 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
364 def needs_update?(%User{local: false} = user) do
365 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
368 def needs_update?(_), do: true
370 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
371 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
375 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
376 follow(follower, followed)
379 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
380 if not User.ap_enabled?(followed) do
381 follow(follower, followed)
387 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
388 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
389 def follow_all(follower, followeds) do
392 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
393 |> Enum.map(fn %{follower_address: fa} -> fa end)
397 where: u.id == ^follower.id,
402 "array(select distinct unnest (array_cat(?, ?)))",
411 {1, [follower]} = Repo.update_all(q, [])
413 Enum.each(followeds, fn followed ->
414 update_follower_count(followed)
420 def follow(%User{} = follower, %User{info: info} = followed) do
421 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
422 ap_followers = followed.follower_address
426 {:error, "Could not follow user: You are deactivated."}
428 deny_follow_blocked and blocks?(followed, follower) ->
429 {:error, "Could not follow user: #{followed.nickname} blocked you."}
432 if !followed.local && follower.local && !ap_enabled?(followed) do
433 Websub.subscribe(follower, followed)
438 where: u.id == ^follower.id,
439 update: [push: [following: ^ap_followers]],
443 {1, [follower]} = Repo.update_all(q, [])
445 follower = maybe_update_following_count(follower)
447 {:ok, _} = update_follower_count(followed)
453 def unfollow(%User{} = follower, %User{} = followed) do
454 ap_followers = followed.follower_address
456 if following?(follower, followed) and follower.ap_id != followed.ap_id do
459 where: u.id == ^follower.id,
460 update: [pull: [following: ^ap_followers]],
464 {1, [follower]} = Repo.update_all(q, [])
466 follower = maybe_update_following_count(follower)
468 {:ok, followed} = update_follower_count(followed)
472 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
474 {:error, "Not subscribed!"}
478 @spec following?(User.t(), User.t()) :: boolean
479 def following?(%User{} = follower, %User{} = followed) do
480 Enum.member?(follower.following, followed.follower_address)
483 def locked?(%User{} = user) do
484 user.info.locked || false
488 Repo.get_by(User, id: id)
491 def get_by_ap_id(ap_id) do
492 Repo.get_by(User, ap_id: ap_id)
495 def get_all_by_ap_id(ap_ids) do
496 from(u in __MODULE__,
497 where: u.ap_id in ^ap_ids
502 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
503 # of the ap_id and the domain and tries to get that user
504 def get_by_guessed_nickname(ap_id) do
505 domain = URI.parse(ap_id).host
506 name = List.last(String.split(ap_id, "/"))
507 nickname = "#{name}@#{domain}"
509 get_cached_by_nickname(nickname)
512 def set_cache({:ok, user}), do: set_cache(user)
513 def set_cache({:error, err}), do: {:error, err}
515 def set_cache(%User{} = user) do
516 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
517 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
518 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
522 def update_and_set_cache(changeset) do
523 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
530 def invalidate_cache(user) do
531 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
532 Cachex.del(:user_cache, "nickname:#{user.nickname}")
533 Cachex.del(:user_cache, "user_info:#{user.id}")
536 def get_cached_by_ap_id(ap_id) do
537 key = "ap_id:#{ap_id}"
538 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
541 def get_cached_by_id(id) do
545 Cachex.fetch!(:user_cache, key, fn _ ->
549 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
550 {:commit, user.ap_id}
556 get_cached_by_ap_id(ap_id)
559 def get_cached_by_nickname(nickname) do
560 key = "nickname:#{nickname}"
562 Cachex.fetch!(:user_cache, key, fn ->
563 user_result = get_or_fetch_by_nickname(nickname)
566 {:ok, user} -> {:commit, user}
567 {:error, _error} -> {:ignore, nil}
572 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
573 restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
576 is_integer(nickname_or_id) or Pleroma.FlakeId.is_flake_id?(nickname_or_id) ->
577 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
579 restrict_to_local == false ->
580 get_cached_by_nickname(nickname_or_id)
582 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
583 get_cached_by_nickname(nickname_or_id)
590 def get_by_nickname(nickname) do
591 Repo.get_by(User, nickname: nickname) ||
592 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
593 Repo.get_by(User, nickname: local_nickname(nickname))
597 def get_by_email(email), do: Repo.get_by(User, email: email)
599 def get_by_nickname_or_email(nickname_or_email) do
600 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
603 def get_cached_user_info(user) do
604 key = "user_info:#{user.id}"
605 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
608 def fetch_by_nickname(nickname) do
609 ap_try = ActivityPub.make_user_from_nickname(nickname)
612 {:ok, user} -> {:ok, user}
613 _ -> OStatus.make_user(nickname)
617 def get_or_fetch_by_nickname(nickname) do
618 with %User{} = user <- get_by_nickname(nickname) do
622 with [_nick, _domain] <- String.split(nickname, "@"),
623 {:ok, user} <- fetch_by_nickname(nickname) do
624 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
625 fetch_initial_posts(user)
630 _e -> {:error, "not found " <> nickname}
635 @doc "Fetch some posts when the user has just been federated with"
636 def fetch_initial_posts(user),
637 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:fetch_initial_posts, user])
639 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
640 def get_followers_query(%User{} = user, nil) do
641 User.Query.build(%{followers: user, deactivated: false})
644 def get_followers_query(user, page) do
645 from(u in get_followers_query(user, nil))
646 |> User.Query.paginate(page, 20)
649 @spec get_followers_query(User.t()) :: Ecto.Query.t()
650 def get_followers_query(user), do: get_followers_query(user, nil)
652 @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
653 def get_followers(user, page \\ nil) do
654 q = get_followers_query(user, page)
659 @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
660 def get_external_followers(user, page \\ nil) do
663 |> get_followers_query(page)
664 |> User.Query.build(%{external: true})
669 def get_followers_ids(user, page \\ nil) do
670 q = get_followers_query(user, page)
672 Repo.all(from(u in q, select: u.id))
675 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
676 def get_friends_query(%User{} = user, nil) do
677 User.Query.build(%{friends: user, deactivated: false})
680 def get_friends_query(user, page) do
681 from(u in get_friends_query(user, nil))
682 |> User.Query.paginate(page, 20)
685 @spec get_friends_query(User.t()) :: Ecto.Query.t()
686 def get_friends_query(user), do: get_friends_query(user, nil)
688 def get_friends(user, page \\ nil) do
689 q = get_friends_query(user, page)
694 def get_friends_ids(user, page \\ nil) do
695 q = get_friends_query(user, page)
697 Repo.all(from(u in q, select: u.id))
700 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
701 def get_follow_requests(%User{} = user) do
703 Activity.follow_requests_for_actor(user)
704 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
705 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
706 |> group_by([a, u], u.id)
713 def increase_note_count(%User{} = user) do
715 |> where(id: ^user.id)
720 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
727 |> Repo.update_all([])
729 {1, [user]} -> set_cache(user)
734 def decrease_note_count(%User{} = user) do
736 |> where(id: ^user.id)
741 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
748 |> Repo.update_all([])
750 {1, [user]} -> set_cache(user)
755 def update_note_count(%User{} = user) do
759 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
763 note_count = Repo.one(note_count_query)
765 info_cng = User.Info.set_note_count(user.info, note_count)
769 |> put_embed(:info, info_cng)
770 |> update_and_set_cache()
773 @spec maybe_fetch_follow_information(User.t()) :: User.t()
774 def maybe_fetch_follow_information(user) do
775 with {:ok, user} <- fetch_follow_information(user) do
779 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
785 def fetch_follow_information(user) do
786 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
787 info_cng = User.Info.follow_information_update(user.info, info)
792 |> put_embed(:info, info_cng)
794 update_and_set_cache(changeset)
801 def update_follower_count(%User{} = user) do
802 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
803 follower_count_query =
804 User.Query.build(%{followers: user, deactivated: false})
805 |> select([u], %{count: count(u.id)})
808 |> where(id: ^user.id)
809 |> join(:inner, [u], s in subquery(follower_count_query))
814 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
821 |> Repo.update_all([])
823 {1, [user]} -> set_cache(user)
827 {:ok, maybe_fetch_follow_information(user)}
831 @spec maybe_update_following_count(User.t()) :: User.t()
832 def maybe_update_following_count(%User{local: false} = user) do
833 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
834 maybe_fetch_follow_information(user)
840 def maybe_update_following_count(user), do: user
842 def remove_duplicated_following(%User{following: following} = user) do
843 uniq_following = Enum.uniq(following)
845 if length(following) == length(uniq_following) do
849 |> update_changeset(%{following: uniq_following})
850 |> update_and_set_cache()
854 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
855 def get_users_from_set(ap_ids, local_only \\ true) do
856 criteria = %{ap_id: ap_ids, deactivated: false}
857 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
859 User.Query.build(criteria)
863 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
864 def get_recipients_from_activity(%Activity{recipients: to}) do
865 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
869 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
870 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
874 User.Info.add_to_mutes(info, ap_id)
875 |> User.Info.add_to_muted_notifications(info, ap_id, notifications?)
879 |> put_embed(:info, info_cng)
881 update_and_set_cache(cng)
884 def unmute(muter, %{ap_id: ap_id}) do
888 User.Info.remove_from_mutes(info, ap_id)
889 |> User.Info.remove_from_muted_notifications(info, ap_id)
893 |> put_embed(:info, info_cng)
895 update_and_set_cache(cng)
898 def subscribe(subscriber, %{ap_id: ap_id}) do
899 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
901 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
902 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
905 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
909 |> User.Info.add_to_subscribers(subscriber.ap_id)
912 |> put_embed(:info, info_cng)
913 |> update_and_set_cache()
918 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
919 with %User{} = user <- get_cached_by_ap_id(ap_id) do
922 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
925 |> put_embed(:info, info_cng)
926 |> update_and_set_cache()
930 def block(blocker, %User{ap_id: ap_id} = blocked) do
931 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
933 if following?(blocker, blocked) do
934 {:ok, blocker, _} = unfollow(blocker, blocked)
940 # clear any requested follows as well
942 case CommonAPI.reject_follow_request(blocked, blocker) do
943 {:ok, %User{} = updated_blocked} -> updated_blocked
948 if subscribed_to?(blocked, blocker) do
949 {:ok, blocker} = unsubscribe(blocked, blocker)
955 if following?(blocked, blocker) do
956 unfollow(blocked, blocker)
959 {:ok, blocker} = update_follower_count(blocker)
963 |> User.Info.add_to_block(ap_id)
967 |> put_embed(:info, info_cng)
969 update_and_set_cache(cng)
972 # helper to handle the block given only an actor's AP id
973 def block(blocker, %{ap_id: ap_id}) do
974 block(blocker, get_cached_by_ap_id(ap_id))
977 def unblock(blocker, %{ap_id: ap_id}) do
980 |> User.Info.remove_from_block(ap_id)
984 |> put_embed(:info, info_cng)
986 update_and_set_cache(cng)
989 def mutes?(nil, _), do: false
990 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
992 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
993 def muted_notifications?(nil, _), do: false
995 def muted_notifications?(user, %{ap_id: ap_id}),
996 do: Enum.member?(user.info.muted_notifications, ap_id)
998 def blocks?(%User{} = user, %User{} = target) do
999 blocks_ap_id?(user, target) || blocks_domain?(user, target)
1002 def blocks?(nil, _), do: false
1004 def blocks_ap_id?(%User{} = user, %User{} = target) do
1005 Enum.member?(user.info.blocks, target.ap_id)
1008 def blocks_ap_id?(_, _), do: false
1010 def blocks_domain?(%User{} = user, %User{} = target) do
1011 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
1012 %{host: host} = URI.parse(target.ap_id)
1013 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1016 def blocks_domain?(_, _), do: false
1018 def subscribed_to?(user, %{ap_id: ap_id}) do
1019 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1020 Enum.member?(target.info.subscribers, user.ap_id)
1024 @spec muted_users(User.t()) :: [User.t()]
1025 def muted_users(user) do
1026 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
1030 @spec blocked_users(User.t()) :: [User.t()]
1031 def blocked_users(user) do
1032 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
1036 @spec subscribers(User.t()) :: [User.t()]
1037 def subscribers(user) do
1038 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
1042 def block_domain(user, domain) do
1045 |> User.Info.add_to_domain_block(domain)
1049 |> put_embed(:info, info_cng)
1051 update_and_set_cache(cng)
1054 def unblock_domain(user, domain) do
1057 |> User.Info.remove_from_domain_block(domain)
1061 |> put_embed(:info, info_cng)
1063 update_and_set_cache(cng)
1066 def deactivate_async(user, status \\ true) do
1067 PleromaJobQueue.enqueue(:background, __MODULE__, [:deactivate_async, user, status])
1070 def deactivate(%User{} = user, status \\ true) do
1071 info_cng = User.Info.set_activation_status(user.info, status)
1073 with {:ok, friends} <- User.get_friends(user),
1074 {:ok, followers} <- User.get_followers(user),
1078 |> put_embed(:info, info_cng)
1079 |> update_and_set_cache() do
1080 Enum.each(followers, &invalidate_cache(&1))
1081 Enum.each(friends, &update_follower_count(&1))
1087 def update_notification_settings(%User{} = user, settings \\ %{}) do
1088 info_changeset = User.Info.update_notification_settings(user.info, settings)
1091 |> put_embed(:info, info_changeset)
1092 |> update_and_set_cache()
1095 @spec delete(User.t()) :: :ok
1096 def delete(%User{} = user),
1097 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:delete, user])
1099 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1100 def perform(:delete, %User{} = user) do
1101 {:ok, _user} = ActivityPub.delete(user)
1103 # Remove all relationships
1104 {:ok, followers} = User.get_followers(user)
1106 Enum.each(followers, fn follower ->
1107 ActivityPub.unfollow(follower, user)
1108 User.unfollow(follower, user)
1111 {:ok, friends} = User.get_friends(user)
1113 Enum.each(friends, fn followed ->
1114 ActivityPub.unfollow(user, followed)
1115 User.unfollow(user, followed)
1118 delete_user_activities(user)
1119 invalidate_cache(user)
1123 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1124 def perform(:fetch_initial_posts, %User{} = user) do
1125 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1128 # Insert all the posts in reverse order, so they're in the right order on the timeline
1129 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
1130 &Pleroma.Web.Federator.incoming_ap_doc/1
1136 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1138 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1139 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1140 when is_list(blocked_identifiers) do
1142 blocked_identifiers,
1143 fn blocked_identifier ->
1144 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1145 {:ok, blocker} <- block(blocker, blocked),
1146 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1150 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1157 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1158 def perform(:follow_import, %User{} = follower, followed_identifiers)
1159 when is_list(followed_identifiers) do
1161 followed_identifiers,
1162 fn followed_identifier ->
1163 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1164 {:ok, follower} <- maybe_direct_follow(follower, followed),
1165 {:ok, _} <- ActivityPub.follow(follower, followed) do
1169 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1176 @spec external_users_query() :: Ecto.Query.t()
1177 def external_users_query do
1185 @spec external_users(keyword()) :: [User.t()]
1186 def external_users(opts \\ []) do
1188 external_users_query()
1189 |> select([u], struct(u, [:id, :ap_id, :info]))
1193 do: where(query, [u], u.id > ^opts[:max_id]),
1198 do: limit(query, ^opts[:limit]),
1204 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers),
1206 PleromaJobQueue.enqueue(:background, __MODULE__, [
1212 def follow_import(%User{} = follower, followed_identifiers) when is_list(followed_identifiers),
1214 PleromaJobQueue.enqueue(:background, __MODULE__, [
1217 followed_identifiers
1220 def delete_user_activities(%User{ap_id: ap_id} = user) do
1222 |> Activity.query_by_actor()
1223 |> RepoStreamer.chunk_stream(50)
1224 |> Stream.each(fn activities ->
1225 Enum.each(activities, &delete_activity(&1))
1232 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1234 |> Object.normalize()
1235 |> ActivityPub.delete()
1238 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1239 user = get_cached_by_ap_id(activity.actor)
1240 object = Object.normalize(activity)
1242 ActivityPub.unlike(user, object)
1245 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1246 user = get_cached_by_ap_id(activity.actor)
1247 object = Object.normalize(activity)
1249 ActivityPub.unannounce(user, object)
1252 defp delete_activity(_activity), do: "Doing nothing"
1254 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1255 Pleroma.HTML.Scrubber.TwitterText
1258 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1260 def fetch_by_ap_id(ap_id) do
1261 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1268 case OStatus.make_user(ap_id) do
1269 {:ok, user} -> {:ok, user}
1270 _ -> {:error, "Could not fetch by AP id"}
1275 def get_or_fetch_by_ap_id(ap_id) do
1276 user = get_cached_by_ap_id(ap_id)
1278 if !is_nil(user) and !User.needs_update?(user) do
1281 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1282 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1284 resp = fetch_by_ap_id(ap_id)
1286 if should_fetch_initial do
1287 with {:ok, %User{} = user} <- resp do
1288 fetch_initial_posts(user)
1296 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1297 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1298 if user = get_cached_by_ap_id(uri) do
1302 %User{info: %User.Info{}}
1303 |> cast(%{}, [:ap_id, :nickname, :local])
1304 |> put_change(:ap_id, uri)
1305 |> put_change(:nickname, nickname)
1306 |> put_change(:local, true)
1307 |> put_change(:follower_address, uri <> "/followers")
1309 {:ok, user} = Repo.insert(changes)
1315 def public_key_from_info(%{
1316 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1320 |> :public_key.pem_decode()
1322 |> :public_key.pem_entry_decode()
1328 def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
1329 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1332 def public_key_from_info(_), do: {:error, "not found key"}
1334 def get_public_key_for_ap_id(ap_id) do
1335 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1336 {:ok, public_key} <- public_key_from_info(user.info) do
1343 defp blank?(""), do: nil
1344 defp blank?(n), do: n
1346 def insert_or_update_user(data) do
1348 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1349 |> remote_user_creation()
1350 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1354 def ap_enabled?(%User{local: true}), do: true
1355 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1356 def ap_enabled?(_), do: false
1358 @doc "Gets or fetch a user by uri or nickname."
1359 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1360 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1361 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1363 # wait a period of time and return newest version of the User structs
1364 # this is because we have synchronous follow APIs and need to simulate them
1365 # with an async handshake
1366 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1367 with %User{} = a <- User.get_cached_by_id(a.id),
1368 %User{} = b <- User.get_cached_by_id(b.id) do
1376 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1377 with :ok <- :timer.sleep(timeout),
1378 %User{} = a <- User.get_cached_by_id(a.id),
1379 %User{} = b <- User.get_cached_by_id(b.id) do
1387 def parse_bio(bio) when is_binary(bio) and bio != "" do
1389 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1393 def parse_bio(_), do: ""
1395 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1396 # TODO: get profile URLs other than user.ap_id
1397 profile_urls = [user.ap_id]
1400 |> CommonUtils.format_input("text/plain",
1401 mentions_format: :full,
1402 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1407 def parse_bio(_, _), do: ""
1409 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1410 Repo.transaction(fn ->
1411 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1415 def tag(nickname, tags) when is_binary(nickname),
1416 do: tag(get_by_nickname(nickname), tags)
1418 def tag(%User{} = user, tags),
1419 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1421 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1422 Repo.transaction(fn ->
1423 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1427 def untag(nickname, tags) when is_binary(nickname),
1428 do: untag(get_by_nickname(nickname), tags)
1430 def untag(%User{} = user, tags),
1431 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1433 defp update_tags(%User{} = user, new_tags) do
1434 {:ok, updated_user} =
1436 |> change(%{tags: new_tags})
1437 |> update_and_set_cache()
1442 defp normalize_tags(tags) do
1445 |> Enum.map(&String.downcase(&1))
1448 defp local_nickname_regex do
1449 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1450 @extended_local_nickname_regex
1452 @strict_local_nickname_regex
1456 def local_nickname(nickname_or_mention) do
1459 |> String.split("@")
1463 def full_nickname(nickname_or_mention),
1464 do: String.trim_leading(nickname_or_mention, "@")
1466 def error_user(ap_id) do
1471 nickname: "erroruser@example.com",
1472 inserted_at: NaiveDateTime.utc_now()
1476 @spec all_superusers() :: [User.t()]
1477 def all_superusers do
1478 User.Query.build(%{super_users: true, local: true, deactivated: false})
1482 def showing_reblogs?(%User{} = user, %User{} = target) do
1483 target.ap_id not in user.info.muted_reblogs
1487 The function returns a query to get users with no activity for given interval of days.
1488 Inactive users are those who didn't read any notification, or had any activity where
1489 the user is the activity's actor, during `inactivity_threshold` days.
1490 Deactivated users will not appear in this list.
1494 iex> Pleroma.User.list_inactive_users()
1497 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1498 def list_inactive_users_query(inactivity_threshold \\ 7) do
1499 negative_inactivity_threshold = -inactivity_threshold
1500 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1501 # Subqueries are not supported in `where` clauses, join gets too complicated.
1502 has_read_notifications =
1503 from(n in Pleroma.Notification,
1504 where: n.seen == true,
1506 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1509 |> Pleroma.Repo.all()
1511 from(u in Pleroma.User,
1512 left_join: a in Pleroma.Activity,
1513 on: u.ap_id == a.actor,
1514 where: not is_nil(u.nickname),
1515 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1516 where: u.id not in ^has_read_notifications,
1519 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1520 is_nil(max(a.inserted_at))
1525 Enable or disable email notifications for user
1529 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1530 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1532 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1533 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1535 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1536 {:ok, t()} | {:error, Ecto.Changeset.t()}
1537 def switch_email_notifications(user, type, status) do
1538 info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})
1541 |> put_embed(:info, info)
1542 |> update_and_set_cache()
1546 Set `last_digest_emailed_at` value for the user to current time
1548 @spec touch_last_digest_emailed_at(t()) :: t()
1549 def touch_last_digest_emailed_at(user) do
1550 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1552 {:ok, updated_user} =
1554 |> change(%{last_digest_emailed_at: now})
1555 |> update_and_set_cache()
1560 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1561 def toggle_confirmation(%User{} = user) do
1562 need_confirmation? = !user.info.confirmation_pending
1565 User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
1569 |> put_embed(:info, info_changeset)
1570 |> update_and_set_cache()
1573 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1577 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1578 # use instance-default
1579 config = Pleroma.Config.get([:assets, :mascots])
1580 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1581 mascot = Keyword.get(config, default_mascot)
1584 "id" => "default-mascot",
1585 "url" => mascot[:url],
1586 "preview_url" => mascot[:url],
1588 "mime_type" => mascot[:mime_type]
1593 def ensure_keys_present(%User{info: info} = user) do
1597 {:ok, pem} = Keys.generate_rsa_pem()
1600 |> Ecto.Changeset.change()
1601 |> Ecto.Changeset.put_embed(:info, User.Info.set_keys(info, pem))
1602 |> update_and_set_cache()
1606 def get_ap_ids_by_nicknames(nicknames) do
1608 where: u.nickname in ^nicknames,
1614 defdelegate search(query, opts \\ []), to: User.Search
1616 defp put_password_hash(
1617 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1619 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1622 defp put_password_hash(changeset), do: changeset
1624 def is_internal_user?(%User{nickname: nil}), do: true
1625 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1626 def is_internal_user?(_), do: false