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 def get_all_by_ids(ids) do
503 from(u in __MODULE__, where: u.id in ^ids)
507 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
508 # of the ap_id and the domain and tries to get that user
509 def get_by_guessed_nickname(ap_id) do
510 domain = URI.parse(ap_id).host
511 name = List.last(String.split(ap_id, "/"))
512 nickname = "#{name}@#{domain}"
514 get_cached_by_nickname(nickname)
517 def set_cache({:ok, user}), do: set_cache(user)
518 def set_cache({:error, err}), do: {:error, err}
520 def set_cache(%User{} = user) do
521 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
522 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
523 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
527 def update_and_set_cache(changeset) do
528 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
535 def invalidate_cache(user) do
536 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
537 Cachex.del(:user_cache, "nickname:#{user.nickname}")
538 Cachex.del(:user_cache, "user_info:#{user.id}")
541 def get_cached_by_ap_id(ap_id) do
542 key = "ap_id:#{ap_id}"
543 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
546 def get_cached_by_id(id) do
550 Cachex.fetch!(:user_cache, key, fn _ ->
554 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
555 {:commit, user.ap_id}
561 get_cached_by_ap_id(ap_id)
564 def get_cached_by_nickname(nickname) do
565 key = "nickname:#{nickname}"
567 Cachex.fetch!(:user_cache, key, fn ->
568 user_result = get_or_fetch_by_nickname(nickname)
571 {:ok, user} -> {:commit, user}
572 {:error, _error} -> {:ignore, nil}
577 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
578 restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
581 is_integer(nickname_or_id) or Pleroma.FlakeId.is_flake_id?(nickname_or_id) ->
582 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
584 restrict_to_local == false ->
585 get_cached_by_nickname(nickname_or_id)
587 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
588 get_cached_by_nickname(nickname_or_id)
595 def get_by_nickname(nickname) do
596 Repo.get_by(User, nickname: nickname) ||
597 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
598 Repo.get_by(User, nickname: local_nickname(nickname))
602 def get_by_email(email), do: Repo.get_by(User, email: email)
604 def get_by_nickname_or_email(nickname_or_email) do
605 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
608 def get_cached_user_info(user) do
609 key = "user_info:#{user.id}"
610 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
613 def fetch_by_nickname(nickname) do
614 ap_try = ActivityPub.make_user_from_nickname(nickname)
617 {:ok, user} -> {:ok, user}
618 _ -> OStatus.make_user(nickname)
622 def get_or_fetch_by_nickname(nickname) do
623 with %User{} = user <- get_by_nickname(nickname) do
627 with [_nick, _domain] <- String.split(nickname, "@"),
628 {:ok, user} <- fetch_by_nickname(nickname) do
629 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
630 fetch_initial_posts(user)
635 _e -> {:error, "not found " <> nickname}
640 @doc "Fetch some posts when the user has just been federated with"
641 def fetch_initial_posts(user),
642 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:fetch_initial_posts, user])
644 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
645 def get_followers_query(%User{} = user, nil) do
646 User.Query.build(%{followers: user, deactivated: false})
649 def get_followers_query(user, page) do
650 from(u in get_followers_query(user, nil))
651 |> User.Query.paginate(page, 20)
654 @spec get_followers_query(User.t()) :: Ecto.Query.t()
655 def get_followers_query(user), do: get_followers_query(user, nil)
657 @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
658 def get_followers(user, page \\ nil) do
659 q = get_followers_query(user, page)
664 @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
665 def get_external_followers(user, page \\ nil) do
668 |> get_followers_query(page)
669 |> User.Query.build(%{external: true})
674 def get_followers_ids(user, page \\ nil) do
675 q = get_followers_query(user, page)
677 Repo.all(from(u in q, select: u.id))
680 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
681 def get_friends_query(%User{} = user, nil) do
682 User.Query.build(%{friends: user, deactivated: false})
685 def get_friends_query(user, page) do
686 from(u in get_friends_query(user, nil))
687 |> User.Query.paginate(page, 20)
690 @spec get_friends_query(User.t()) :: Ecto.Query.t()
691 def get_friends_query(user), do: get_friends_query(user, nil)
693 def get_friends(user, page \\ nil) do
694 q = get_friends_query(user, page)
699 def get_friends_ids(user, page \\ nil) do
700 q = get_friends_query(user, page)
702 Repo.all(from(u in q, select: u.id))
705 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
706 def get_follow_requests(%User{} = user) do
708 Activity.follow_requests_for_actor(user)
709 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
710 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
711 |> group_by([a, u], u.id)
718 def increase_note_count(%User{} = user) do
720 |> where(id: ^user.id)
725 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
732 |> Repo.update_all([])
734 {1, [user]} -> set_cache(user)
739 def decrease_note_count(%User{} = user) do
741 |> where(id: ^user.id)
746 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
753 |> Repo.update_all([])
755 {1, [user]} -> set_cache(user)
760 def update_note_count(%User{} = user) do
764 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
768 note_count = Repo.one(note_count_query)
770 info_cng = User.Info.set_note_count(user.info, note_count)
774 |> put_embed(:info, info_cng)
775 |> update_and_set_cache()
778 def update_mascot(user, url) do
780 User.Info.mascot_update(
787 |> put_embed(:info, info_changeset)
788 |> update_and_set_cache()
791 @spec maybe_fetch_follow_information(User.t()) :: User.t()
792 def maybe_fetch_follow_information(user) do
793 with {:ok, user} <- fetch_follow_information(user) do
797 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
803 def fetch_follow_information(user) do
804 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
805 info_cng = User.Info.follow_information_update(user.info, info)
810 |> put_embed(:info, info_cng)
812 update_and_set_cache(changeset)
819 def update_follower_count(%User{} = user) do
820 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
821 follower_count_query =
822 User.Query.build(%{followers: user, deactivated: false})
823 |> select([u], %{count: count(u.id)})
826 |> where(id: ^user.id)
827 |> join(:inner, [u], s in subquery(follower_count_query))
832 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
839 |> Repo.update_all([])
841 {1, [user]} -> set_cache(user)
845 {:ok, maybe_fetch_follow_information(user)}
849 @spec maybe_update_following_count(User.t()) :: User.t()
850 def maybe_update_following_count(%User{local: false} = user) do
851 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
852 maybe_fetch_follow_information(user)
858 def maybe_update_following_count(user), do: user
860 def remove_duplicated_following(%User{following: following} = user) do
861 uniq_following = Enum.uniq(following)
863 if length(following) == length(uniq_following) do
867 |> update_changeset(%{following: uniq_following})
868 |> update_and_set_cache()
872 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
873 def get_users_from_set(ap_ids, local_only \\ true) do
874 criteria = %{ap_id: ap_ids, deactivated: false}
875 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
877 User.Query.build(criteria)
881 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
882 def get_recipients_from_activity(%Activity{recipients: to}) do
883 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
887 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
888 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
892 User.Info.add_to_mutes(info, ap_id)
893 |> User.Info.add_to_muted_notifications(info, ap_id, notifications?)
897 |> put_embed(:info, info_cng)
899 update_and_set_cache(cng)
902 def unmute(muter, %{ap_id: ap_id}) do
906 User.Info.remove_from_mutes(info, ap_id)
907 |> User.Info.remove_from_muted_notifications(info, ap_id)
911 |> put_embed(:info, info_cng)
913 update_and_set_cache(cng)
916 def subscribe(subscriber, %{ap_id: ap_id}) do
917 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
919 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
920 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
923 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
927 |> User.Info.add_to_subscribers(subscriber.ap_id)
930 |> put_embed(:info, info_cng)
931 |> update_and_set_cache()
936 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
937 with %User{} = user <- get_cached_by_ap_id(ap_id) do
938 info_cng = User.Info.remove_from_subscribers(user.info, unsubscriber.ap_id)
941 |> put_embed(:info, info_cng)
942 |> update_and_set_cache()
946 def block(blocker, %User{ap_id: ap_id} = blocked) do
947 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
949 if following?(blocker, blocked) do
950 {:ok, blocker, _} = unfollow(blocker, blocked)
956 # clear any requested follows as well
958 case CommonAPI.reject_follow_request(blocked, blocker) do
959 {:ok, %User{} = updated_blocked} -> updated_blocked
964 if subscribed_to?(blocked, blocker) do
965 {:ok, blocker} = unsubscribe(blocked, blocker)
971 if following?(blocked, blocker) do
972 unfollow(blocked, blocker)
975 {:ok, blocker} = update_follower_count(blocker)
979 |> User.Info.add_to_block(ap_id)
983 |> put_embed(:info, info_cng)
985 update_and_set_cache(cng)
988 # helper to handle the block given only an actor's AP id
989 def block(blocker, %{ap_id: ap_id}) do
990 block(blocker, get_cached_by_ap_id(ap_id))
993 def unblock(blocker, %{ap_id: ap_id}) do
996 |> User.Info.remove_from_block(ap_id)
1000 |> put_embed(:info, info_cng)
1002 update_and_set_cache(cng)
1005 def mutes?(nil, _), do: false
1006 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
1008 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
1009 def muted_notifications?(nil, _), do: false
1011 def muted_notifications?(user, %{ap_id: ap_id}),
1012 do: Enum.member?(user.info.muted_notifications, ap_id)
1014 def blocks?(%User{} = user, %User{} = target) do
1015 blocks_ap_id?(user, target) || blocks_domain?(user, target)
1018 def blocks?(nil, _), do: false
1020 def blocks_ap_id?(%User{} = user, %User{} = target) do
1021 Enum.member?(user.info.blocks, target.ap_id)
1024 def blocks_ap_id?(_, _), do: false
1026 def blocks_domain?(%User{} = user, %User{} = target) do
1027 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
1028 %{host: host} = URI.parse(target.ap_id)
1029 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1032 def blocks_domain?(_, _), do: false
1034 def subscribed_to?(user, %{ap_id: ap_id}) do
1035 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1036 Enum.member?(target.info.subscribers, user.ap_id)
1040 @spec muted_users(User.t()) :: [User.t()]
1041 def muted_users(user) do
1042 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
1046 @spec blocked_users(User.t()) :: [User.t()]
1047 def blocked_users(user) do
1048 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
1052 @spec subscribers(User.t()) :: [User.t()]
1053 def subscribers(user) do
1054 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
1058 def block_domain(user, domain) do
1061 |> User.Info.add_to_domain_block(domain)
1065 |> put_embed(:info, info_cng)
1067 update_and_set_cache(cng)
1070 def unblock_domain(user, domain) do
1073 |> User.Info.remove_from_domain_block(domain)
1077 |> put_embed(:info, info_cng)
1079 update_and_set_cache(cng)
1082 def deactivate_async(user, status \\ true) do
1083 PleromaJobQueue.enqueue(:background, __MODULE__, [:deactivate_async, user, status])
1086 def deactivate(%User{} = user, status \\ true) do
1087 info_cng = User.Info.set_activation_status(user.info, status)
1089 with {:ok, friends} <- User.get_friends(user),
1090 {:ok, followers} <- User.get_followers(user),
1094 |> put_embed(:info, info_cng)
1095 |> update_and_set_cache() do
1096 Enum.each(followers, &invalidate_cache(&1))
1097 Enum.each(friends, &update_follower_count(&1))
1103 def update_notification_settings(%User{} = user, settings \\ %{}) do
1104 info_changeset = User.Info.update_notification_settings(user.info, settings)
1107 |> put_embed(:info, info_changeset)
1108 |> update_and_set_cache()
1111 @spec delete(User.t()) :: :ok
1112 def delete(%User{} = user),
1113 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:delete, user])
1115 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1116 def perform(:delete, %User{} = user) do
1117 {:ok, _user} = ActivityPub.delete(user)
1119 # Remove all relationships
1120 {:ok, followers} = User.get_followers(user)
1122 Enum.each(followers, fn follower ->
1123 ActivityPub.unfollow(follower, user)
1124 User.unfollow(follower, user)
1127 {:ok, friends} = User.get_friends(user)
1129 Enum.each(friends, fn followed ->
1130 ActivityPub.unfollow(user, followed)
1131 User.unfollow(user, followed)
1134 delete_user_activities(user)
1135 invalidate_cache(user)
1139 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1140 def perform(:fetch_initial_posts, %User{} = user) do
1141 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1144 # Insert all the posts in reverse order, so they're in the right order on the timeline
1145 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
1146 &Pleroma.Web.Federator.incoming_ap_doc/1
1152 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1154 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1155 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1156 when is_list(blocked_identifiers) do
1158 blocked_identifiers,
1159 fn blocked_identifier ->
1160 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1161 {:ok, blocker} <- block(blocker, blocked),
1162 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1166 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1173 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1174 def perform(:follow_import, %User{} = follower, followed_identifiers)
1175 when is_list(followed_identifiers) do
1177 followed_identifiers,
1178 fn followed_identifier ->
1179 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1180 {:ok, follower} <- maybe_direct_follow(follower, followed),
1181 {:ok, _} <- ActivityPub.follow(follower, followed) do
1185 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1192 @spec external_users_query() :: Ecto.Query.t()
1193 def external_users_query do
1201 @spec external_users(keyword()) :: [User.t()]
1202 def external_users(opts \\ []) do
1204 external_users_query()
1205 |> select([u], struct(u, [:id, :ap_id, :info]))
1209 do: where(query, [u], u.id > ^opts[:max_id]),
1214 do: limit(query, ^opts[:limit]),
1220 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers),
1222 PleromaJobQueue.enqueue(:background, __MODULE__, [
1228 def follow_import(%User{} = follower, followed_identifiers) when is_list(followed_identifiers),
1230 PleromaJobQueue.enqueue(:background, __MODULE__, [
1233 followed_identifiers
1236 def delete_user_activities(%User{ap_id: ap_id} = user) do
1238 |> Activity.query_by_actor()
1239 |> RepoStreamer.chunk_stream(50)
1240 |> Stream.each(fn activities ->
1241 Enum.each(activities, &delete_activity(&1))
1248 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1250 |> Object.normalize()
1251 |> ActivityPub.delete()
1254 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1255 user = get_cached_by_ap_id(activity.actor)
1256 object = Object.normalize(activity)
1258 ActivityPub.unlike(user, object)
1261 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1262 user = get_cached_by_ap_id(activity.actor)
1263 object = Object.normalize(activity)
1265 ActivityPub.unannounce(user, object)
1268 defp delete_activity(_activity), do: "Doing nothing"
1270 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1271 Pleroma.HTML.Scrubber.TwitterText
1274 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1276 def fetch_by_ap_id(ap_id) do
1277 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1284 case OStatus.make_user(ap_id) do
1285 {:ok, user} -> {:ok, user}
1286 _ -> {:error, "Could not fetch by AP id"}
1291 def get_or_fetch_by_ap_id(ap_id) do
1292 user = get_cached_by_ap_id(ap_id)
1294 if !is_nil(user) and !User.needs_update?(user) do
1297 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1298 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1300 resp = fetch_by_ap_id(ap_id)
1302 if should_fetch_initial do
1303 with {:ok, %User{} = user} <- resp do
1304 fetch_initial_posts(user)
1312 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1313 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1314 if user = get_cached_by_ap_id(uri) do
1318 %User{info: %User.Info{}}
1319 |> cast(%{}, [:ap_id, :nickname, :local])
1320 |> put_change(:ap_id, uri)
1321 |> put_change(:nickname, nickname)
1322 |> put_change(:local, true)
1323 |> put_change(:follower_address, uri <> "/followers")
1325 {:ok, user} = Repo.insert(changes)
1331 def public_key_from_info(%{
1332 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1336 |> :public_key.pem_decode()
1338 |> :public_key.pem_entry_decode()
1344 def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
1345 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1348 def public_key_from_info(_), do: {:error, "not found key"}
1350 def get_public_key_for_ap_id(ap_id) do
1351 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1352 {:ok, public_key} <- public_key_from_info(user.info) do
1359 defp blank?(""), do: nil
1360 defp blank?(n), do: n
1362 def insert_or_update_user(data) do
1364 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1365 |> remote_user_creation()
1366 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1370 def ap_enabled?(%User{local: true}), do: true
1371 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1372 def ap_enabled?(_), do: false
1374 @doc "Gets or fetch a user by uri or nickname."
1375 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1376 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1377 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1379 # wait a period of time and return newest version of the User structs
1380 # this is because we have synchronous follow APIs and need to simulate them
1381 # with an async handshake
1382 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1383 with %User{} = a <- User.get_cached_by_id(a.id),
1384 %User{} = b <- User.get_cached_by_id(b.id) do
1392 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1393 with :ok <- :timer.sleep(timeout),
1394 %User{} = a <- User.get_cached_by_id(a.id),
1395 %User{} = b <- User.get_cached_by_id(b.id) do
1403 def parse_bio(bio) when is_binary(bio) and bio != "" do
1405 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1409 def parse_bio(_), do: ""
1411 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1412 # TODO: get profile URLs other than user.ap_id
1413 profile_urls = [user.ap_id]
1416 |> CommonUtils.format_input("text/plain",
1417 mentions_format: :full,
1418 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1423 def parse_bio(_, _), do: ""
1425 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1426 Repo.transaction(fn ->
1427 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1431 def tag(nickname, tags) when is_binary(nickname),
1432 do: tag(get_by_nickname(nickname), tags)
1434 def tag(%User{} = user, tags),
1435 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1437 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1438 Repo.transaction(fn ->
1439 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1443 def untag(nickname, tags) when is_binary(nickname),
1444 do: untag(get_by_nickname(nickname), tags)
1446 def untag(%User{} = user, tags),
1447 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1449 defp update_tags(%User{} = user, new_tags) do
1450 {:ok, updated_user} =
1452 |> change(%{tags: new_tags})
1453 |> update_and_set_cache()
1458 defp normalize_tags(tags) do
1461 |> Enum.map(&String.downcase(&1))
1464 defp local_nickname_regex do
1465 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1466 @extended_local_nickname_regex
1468 @strict_local_nickname_regex
1472 def local_nickname(nickname_or_mention) do
1475 |> String.split("@")
1479 def full_nickname(nickname_or_mention),
1480 do: String.trim_leading(nickname_or_mention, "@")
1482 def error_user(ap_id) do
1487 nickname: "erroruser@example.com",
1488 inserted_at: NaiveDateTime.utc_now()
1492 @spec all_superusers() :: [User.t()]
1493 def all_superusers do
1494 User.Query.build(%{super_users: true, local: true, deactivated: false})
1498 def showing_reblogs?(%User{} = user, %User{} = target) do
1499 target.ap_id not in user.info.muted_reblogs
1503 The function returns a query to get users with no activity for given interval of days.
1504 Inactive users are those who didn't read any notification, or had any activity where
1505 the user is the activity's actor, during `inactivity_threshold` days.
1506 Deactivated users will not appear in this list.
1510 iex> Pleroma.User.list_inactive_users()
1513 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1514 def list_inactive_users_query(inactivity_threshold \\ 7) do
1515 negative_inactivity_threshold = -inactivity_threshold
1516 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1517 # Subqueries are not supported in `where` clauses, join gets too complicated.
1518 has_read_notifications =
1519 from(n in Pleroma.Notification,
1520 where: n.seen == true,
1522 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1525 |> Pleroma.Repo.all()
1527 from(u in Pleroma.User,
1528 left_join: a in Pleroma.Activity,
1529 on: u.ap_id == a.actor,
1530 where: not is_nil(u.nickname),
1531 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1532 where: u.id not in ^has_read_notifications,
1535 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1536 is_nil(max(a.inserted_at))
1541 Enable or disable email notifications for user
1545 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1546 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1548 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1549 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1551 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1552 {:ok, t()} | {:error, Ecto.Changeset.t()}
1553 def switch_email_notifications(user, type, status) do
1554 info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})
1557 |> put_embed(:info, info)
1558 |> update_and_set_cache()
1562 Set `last_digest_emailed_at` value for the user to current time
1564 @spec touch_last_digest_emailed_at(t()) :: t()
1565 def touch_last_digest_emailed_at(user) do
1566 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1568 {:ok, updated_user} =
1570 |> change(%{last_digest_emailed_at: now})
1571 |> update_and_set_cache()
1576 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1577 def toggle_confirmation(%User{} = user) do
1578 need_confirmation? = !user.info.confirmation_pending
1581 User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
1585 |> put_embed(:info, info_changeset)
1586 |> update_and_set_cache()
1589 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1593 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1594 # use instance-default
1595 config = Pleroma.Config.get([:assets, :mascots])
1596 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1597 mascot = Keyword.get(config, default_mascot)
1600 "id" => "default-mascot",
1601 "url" => mascot[:url],
1602 "preview_url" => mascot[:url],
1604 "mime_type" => mascot[:mime_type]
1609 def ensure_keys_present(%User{info: info} = user) do
1613 {:ok, pem} = Keys.generate_rsa_pem()
1616 |> Ecto.Changeset.change()
1617 |> Ecto.Changeset.put_embed(:info, User.Info.set_keys(info, pem))
1618 |> update_and_set_cache()
1622 def get_ap_ids_by_nicknames(nicknames) do
1624 where: u.nickname in ^nicknames,
1630 defdelegate search(query, opts \\ []), to: User.Search
1632 defp put_password_hash(
1633 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1635 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1638 defp put_password_hash(changeset), do: changeset
1640 def is_internal_user?(%User{nickname: nil}), do: true
1641 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1642 def is_internal_user?(_), do: false