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
14 alias Pleroma.Delivery
16 alias Pleroma.Notification
18 alias Pleroma.Registration
20 alias Pleroma.RepoStreamer
23 alias Pleroma.Web.ActivityPub.ActivityPub
24 alias Pleroma.Web.ActivityPub.Utils
25 alias Pleroma.Web.CommonAPI
26 alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
27 alias Pleroma.Web.OAuth
28 alias Pleroma.Web.OStatus
29 alias Pleroma.Web.RelMe
30 alias Pleroma.Web.Websub
31 alias Pleroma.Workers.BackgroundWorker
35 @type t :: %__MODULE__{}
37 @primary_key {:id, Pleroma.FlakeId, autogenerate: true}
39 # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
40 @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])?)*$/
42 @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
43 @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
47 field(:email, :string)
49 field(:nickname, :string)
50 field(:password_hash, :string)
51 field(:password, :string, virtual: true)
52 field(:password_confirmation, :string, virtual: true)
53 field(:following, {:array, :string}, default: [])
54 field(:ap_id, :string)
56 field(:local, :boolean, default: true)
57 field(:follower_address, :string)
58 field(:following_address, :string)
59 field(:search_rank, :float, virtual: true)
60 field(:search_type, :integer, virtual: true)
61 field(:tags, {:array, :string}, default: [])
62 field(:last_refreshed_at, :naive_datetime_usec)
63 field(:last_digest_emailed_at, :naive_datetime)
64 has_many(:notifications, Notification)
65 has_many(:registrations, Registration)
66 has_many(:deliveries, Delivery)
67 embeds_one(:info, User.Info)
72 def auth_active?(%User{info: %User.Info{confirmation_pending: true}}),
73 do: !Pleroma.Config.get([:instance, :account_activation_required])
75 def auth_active?(%User{}), do: true
77 def visible_for?(user, for_user \\ nil)
79 def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
81 def visible_for?(%User{} = user, for_user) do
82 auth_active?(user) || superuser?(for_user)
85 def visible_for?(_, _), do: false
87 def superuser?(%User{local: true, info: %User.Info{is_admin: true}}), do: true
88 def superuser?(%User{local: true, info: %User.Info{is_moderator: true}}), do: true
89 def superuser?(_), do: false
91 def avatar_url(user, options \\ []) do
93 %{"url" => [%{"href" => href} | _]} -> href
94 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
98 def banner_url(user, options \\ []) do
99 case user.info.banner do
100 %{"url" => [%{"href" => href} | _]} -> href
101 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
105 def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url
106 def profile_url(%User{ap_id: ap_id}), do: ap_id
107 def profile_url(_), do: nil
109 def ap_id(%User{nickname: nickname}) do
110 "#{Web.base_url()}/users/#{nickname}"
113 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
114 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
116 @spec ap_following(User.t()) :: Sring.t()
117 def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
118 def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
120 def user_info(%User{} = user, args \\ %{}) do
122 if args[:following_count],
123 do: args[:following_count],
124 else: user.info.following_count || following_count(user)
127 if args[:follower_count], do: args[:follower_count], else: user.info.follower_count
130 note_count: user.info.note_count,
131 locked: user.info.locked,
132 confirmation_pending: user.info.confirmation_pending,
133 default_scope: user.info.default_scope
135 |> Map.put(:following_count, following_count)
136 |> Map.put(:follower_count, follower_count)
139 def follow_state(%User{} = user, %User{} = target) do
140 follow_activity = Utils.fetch_latest_follow(user, target)
143 do: follow_activity.data["state"],
144 # Ideally this would be nil, but then Cachex does not commit the value
148 def get_cached_follow_state(user, target) do
149 key = "follow_state:#{user.ap_id}|#{target.ap_id}"
150 Cachex.fetch!(:user_cache, key, fn _ -> {:commit, follow_state(user, target)} end)
153 @spec set_follow_state_cache(String.t(), String.t(), String.t()) :: {:ok | :error, boolean()}
154 def set_follow_state_cache(user_ap_id, target_ap_id, state) do
157 "follow_state:#{user_ap_id}|#{target_ap_id}",
162 def set_info_cache(user, args) do
163 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user, args))
166 @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
167 def restrict_deactivated(query) do
169 where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info)
173 def following_count(%User{following: []}), do: 0
175 def following_count(%User{} = user) do
177 |> get_friends_query()
178 |> Repo.aggregate(:count, :id)
181 defp truncate_if_exists(params, key, max_length) do
182 if Map.has_key?(params, key) and is_binary(params[key]) do
183 {value, _chopped} = String.split_at(params[key], max_length)
184 Map.put(params, key, value)
190 def remote_user_creation(params) do
191 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
192 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
196 |> Map.put(:info, params[:info] || %{})
197 |> truncate_if_exists(:name, name_limit)
198 |> truncate_if_exists(:bio, bio_limit)
200 info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])
204 |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
205 |> validate_required([:name, :ap_id])
206 |> unique_constraint(:nickname)
207 |> validate_format(:nickname, @email_regex)
208 |> validate_length(:bio, max: bio_limit)
209 |> validate_length(:name, max: name_limit)
210 |> put_change(:local, false)
211 |> put_embed(:info, info_cng)
214 case info_cng.changes[:source_data] do
215 %{"followers" => followers, "following" => following} ->
217 |> put_change(:follower_address, followers)
218 |> put_change(:following_address, following)
221 followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
224 |> put_change(:follower_address, followers)
231 def update_changeset(struct, params \\ %{}) do
232 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
233 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
236 |> cast(params, [:bio, :name, :avatar, :following])
237 |> unique_constraint(:nickname)
238 |> validate_format(:nickname, local_nickname_regex())
239 |> validate_length(:bio, max: bio_limit)
240 |> validate_length(:name, min: 1, max: name_limit)
243 def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
244 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
245 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
247 params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
248 info_cng = User.Info.user_upgrade(struct.info, params[:info], remote?)
259 |> unique_constraint(:nickname)
260 |> validate_format(:nickname, local_nickname_regex())
261 |> validate_length(:bio, max: bio_limit)
262 |> validate_length(:name, max: name_limit)
263 |> put_embed(:info, info_cng)
266 def password_update_changeset(struct, params) do
268 |> cast(params, [:password, :password_confirmation])
269 |> validate_required([:password, :password_confirmation])
270 |> validate_confirmation(:password)
274 @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
275 def reset_password(%User{id: user_id} = user, data) do
278 |> Multi.update(:user, password_update_changeset(user, data))
279 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
280 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
282 case Repo.transaction(multi) do
283 {:ok, %{user: user} = _} -> set_cache(user)
284 {:error, _, changeset, _} -> {:error, changeset}
288 def register_changeset(struct, params \\ %{}, opts \\ []) do
289 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
290 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
293 if is_nil(opts[:need_confirmation]) do
294 Pleroma.Config.get([:instance, :account_activation_required])
296 opts[:need_confirmation]
300 User.Info.confirmation_changeset(%User.Info{}, need_confirmation: need_confirmation?)
304 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
305 |> validate_required([:name, :nickname, :password, :password_confirmation])
306 |> validate_confirmation(:password)
307 |> unique_constraint(:email)
308 |> unique_constraint(:nickname)
309 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
310 |> validate_format(:nickname, local_nickname_regex())
311 |> validate_format(:email, @email_regex)
312 |> validate_length(:bio, max: bio_limit)
313 |> validate_length(:name, min: 1, max: name_limit)
314 |> put_change(:info, info_change)
317 if opts[:external] do
320 validate_required(changeset, [:email])
323 if changeset.valid? do
324 ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
325 followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
329 |> put_change(:ap_id, ap_id)
330 |> unique_constraint(:ap_id)
331 |> put_change(:following, [followers])
332 |> put_change(:follower_address, followers)
338 defp autofollow_users(user) do
339 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
342 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
345 follow_all(user, autofollowed_users)
348 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
349 def register(%Ecto.Changeset{} = changeset) do
350 with {:ok, user} <- Repo.insert(changeset),
351 {:ok, user} <- post_register_action(user) do
356 def post_register_action(%User{} = user) do
357 with {:ok, user} <- autofollow_users(user),
358 {:ok, user} <- set_cache(user),
359 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
360 {:ok, _} <- try_send_confirmation_email(user) do
365 def try_send_confirmation_email(%User{} = user) do
366 if user.info.confirmation_pending &&
367 Pleroma.Config.get([:instance, :account_activation_required]) do
369 |> Pleroma.Emails.UserEmail.account_confirmation_email()
370 |> Pleroma.Emails.Mailer.deliver_async()
378 def needs_update?(%User{local: true}), do: false
380 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
382 def needs_update?(%User{local: false} = user) do
383 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
386 def needs_update?(_), do: true
388 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
389 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
393 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
394 follow(follower, followed)
397 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
398 if not User.ap_enabled?(followed) do
399 follow(follower, followed)
405 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
406 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
407 def follow_all(follower, followeds) do
410 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
411 |> Enum.map(fn %{follower_address: fa} -> fa end)
415 where: u.id == ^follower.id,
420 "array(select distinct unnest (array_cat(?, ?)))",
429 {1, [follower]} = Repo.update_all(q, [])
431 Enum.each(followeds, fn followed ->
432 update_follower_count(followed)
438 def follow(%User{} = follower, %User{info: info} = followed) do
439 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
440 ap_followers = followed.follower_address
444 {:error, "Could not follow user: You are deactivated."}
446 deny_follow_blocked and blocks?(followed, follower) ->
447 {:error, "Could not follow user: #{followed.nickname} blocked you."}
450 if !followed.local && follower.local && !ap_enabled?(followed) do
451 Websub.subscribe(follower, followed)
456 where: u.id == ^follower.id,
457 update: [push: [following: ^ap_followers]],
461 {1, [follower]} = Repo.update_all(q, [])
463 follower = maybe_update_following_count(follower)
465 {:ok, _} = update_follower_count(followed)
471 def unfollow(%User{} = follower, %User{} = followed) do
472 ap_followers = followed.follower_address
474 if following?(follower, followed) and follower.ap_id != followed.ap_id do
477 where: u.id == ^follower.id,
478 update: [pull: [following: ^ap_followers]],
482 {1, [follower]} = Repo.update_all(q, [])
484 follower = maybe_update_following_count(follower)
486 {:ok, followed} = update_follower_count(followed)
490 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
492 {:error, "Not subscribed!"}
496 @spec following?(User.t(), User.t()) :: boolean
497 def following?(%User{} = follower, %User{} = followed) do
498 Enum.member?(follower.following, followed.follower_address)
501 def locked?(%User{} = user) do
502 user.info.locked || false
506 Repo.get_by(User, id: id)
509 def get_by_ap_id(ap_id) do
510 Repo.get_by(User, ap_id: ap_id)
513 def get_all_by_ap_id(ap_ids) do
514 from(u in __MODULE__,
515 where: u.ap_id in ^ap_ids
520 def get_all_by_ids(ids) do
521 from(u in __MODULE__, where: u.id in ^ids)
525 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
526 # of the ap_id and the domain and tries to get that user
527 def get_by_guessed_nickname(ap_id) do
528 domain = URI.parse(ap_id).host
529 name = List.last(String.split(ap_id, "/"))
530 nickname = "#{name}@#{domain}"
532 get_cached_by_nickname(nickname)
535 def set_cache({:ok, user}), do: set_cache(user)
536 def set_cache({:error, err}), do: {:error, err}
538 def set_cache(%User{} = user) do
539 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
540 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
541 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
545 def update_and_set_cache(changeset) do
546 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
553 def invalidate_cache(user) do
554 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
555 Cachex.del(:user_cache, "nickname:#{user.nickname}")
556 Cachex.del(:user_cache, "user_info:#{user.id}")
559 def get_cached_by_ap_id(ap_id) do
560 key = "ap_id:#{ap_id}"
561 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
564 def get_cached_by_id(id) do
568 Cachex.fetch!(:user_cache, key, fn _ ->
572 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
573 {:commit, user.ap_id}
579 get_cached_by_ap_id(ap_id)
582 def get_cached_by_nickname(nickname) do
583 key = "nickname:#{nickname}"
585 Cachex.fetch!(:user_cache, key, fn ->
586 user_result = get_or_fetch_by_nickname(nickname)
589 {:ok, user} -> {:commit, user}
590 {:error, _error} -> {:ignore, nil}
595 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
596 restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
599 is_integer(nickname_or_id) or Pleroma.FlakeId.is_flake_id?(nickname_or_id) ->
600 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
602 restrict_to_local == false ->
603 get_cached_by_nickname(nickname_or_id)
605 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
606 get_cached_by_nickname(nickname_or_id)
613 def get_by_nickname(nickname) do
614 Repo.get_by(User, nickname: nickname) ||
615 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
616 Repo.get_by(User, nickname: local_nickname(nickname))
620 def get_by_email(email), do: Repo.get_by(User, email: email)
622 def get_by_nickname_or_email(nickname_or_email) do
623 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
626 def get_cached_user_info(user) do
627 key = "user_info:#{user.id}"
628 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
631 def fetch_by_nickname(nickname) do
632 ap_try = ActivityPub.make_user_from_nickname(nickname)
635 {:ok, user} -> {:ok, user}
636 _ -> OStatus.make_user(nickname)
640 def get_or_fetch_by_nickname(nickname) do
641 with %User{} = user <- get_by_nickname(nickname) do
645 with [_nick, _domain] <- String.split(nickname, "@"),
646 {:ok, user} <- fetch_by_nickname(nickname) do
647 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
648 fetch_initial_posts(user)
653 _e -> {:error, "not found " <> nickname}
658 @doc "Fetch some posts when the user has just been federated with"
659 def fetch_initial_posts(user) do
660 BackgroundWorker.enqueue("fetch_initial_posts", %{"user_id" => user.id})
663 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
664 def get_followers_query(%User{} = user, nil) do
665 User.Query.build(%{followers: user, deactivated: false})
668 def get_followers_query(user, page) do
669 from(u in get_followers_query(user, nil))
670 |> User.Query.paginate(page, 20)
673 @spec get_followers_query(User.t()) :: Ecto.Query.t()
674 def get_followers_query(user), do: get_followers_query(user, nil)
676 @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
677 def get_followers(user, page \\ nil) do
678 q = get_followers_query(user, page)
683 @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
684 def get_external_followers(user, page \\ nil) do
687 |> get_followers_query(page)
688 |> User.Query.build(%{external: true})
693 def get_followers_ids(user, page \\ nil) do
694 q = get_followers_query(user, page)
696 Repo.all(from(u in q, select: u.id))
699 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
700 def get_friends_query(%User{} = user, nil) do
701 User.Query.build(%{friends: user, deactivated: false})
704 def get_friends_query(user, page) do
705 from(u in get_friends_query(user, nil))
706 |> User.Query.paginate(page, 20)
709 @spec get_friends_query(User.t()) :: Ecto.Query.t()
710 def get_friends_query(user), do: get_friends_query(user, nil)
712 def get_friends(user, page \\ nil) do
713 q = get_friends_query(user, page)
718 def get_friends_ids(user, page \\ nil) do
719 q = get_friends_query(user, page)
721 Repo.all(from(u in q, select: u.id))
724 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
725 def get_follow_requests(%User{} = user) do
727 Activity.follow_requests_for_actor(user)
728 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
729 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
730 |> group_by([a, u], u.id)
737 def increase_note_count(%User{} = user) do
739 |> where(id: ^user.id)
744 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
751 |> Repo.update_all([])
753 {1, [user]} -> set_cache(user)
758 def decrease_note_count(%User{} = user) do
760 |> where(id: ^user.id)
765 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
772 |> Repo.update_all([])
774 {1, [user]} -> set_cache(user)
779 def update_note_count(%User{} = user) do
783 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
787 note_count = Repo.one(note_count_query)
789 info_cng = User.Info.set_note_count(user.info, note_count)
793 |> put_embed(:info, info_cng)
794 |> update_and_set_cache()
797 def update_mascot(user, url) do
799 User.Info.mascot_update(
806 |> put_embed(:info, info_changeset)
807 |> update_and_set_cache()
810 @spec maybe_fetch_follow_information(User.t()) :: User.t()
811 def maybe_fetch_follow_information(user) do
812 with {:ok, user} <- fetch_follow_information(user) do
816 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
822 def fetch_follow_information(user) do
823 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
824 info_cng = User.Info.follow_information_update(user.info, info)
829 |> put_embed(:info, info_cng)
831 update_and_set_cache(changeset)
838 def update_follower_count(%User{} = user) do
839 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
840 follower_count_query =
841 User.Query.build(%{followers: user, deactivated: false})
842 |> select([u], %{count: count(u.id)})
845 |> where(id: ^user.id)
846 |> join(:inner, [u], s in subquery(follower_count_query))
851 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
858 |> Repo.update_all([])
860 {1, [user]} -> set_cache(user)
864 {:ok, maybe_fetch_follow_information(user)}
868 @spec maybe_update_following_count(User.t()) :: User.t()
869 def maybe_update_following_count(%User{local: false} = user) do
870 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
871 maybe_fetch_follow_information(user)
877 def maybe_update_following_count(user), do: user
879 def remove_duplicated_following(%User{following: following} = user) do
880 uniq_following = Enum.uniq(following)
882 if length(following) == length(uniq_following) do
886 |> update_changeset(%{following: uniq_following})
887 |> update_and_set_cache()
891 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
892 def get_users_from_set(ap_ids, local_only \\ true) do
893 criteria = %{ap_id: ap_ids, deactivated: false}
894 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
896 User.Query.build(criteria)
900 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
901 def get_recipients_from_activity(%Activity{recipients: to}) do
902 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
906 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
907 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
911 User.Info.add_to_mutes(info, ap_id)
912 |> User.Info.add_to_muted_notifications(info, ap_id, notifications?)
916 |> put_embed(:info, info_cng)
918 update_and_set_cache(cng)
921 def unmute(muter, %{ap_id: ap_id}) do
925 User.Info.remove_from_mutes(info, ap_id)
926 |> User.Info.remove_from_muted_notifications(info, ap_id)
930 |> put_embed(:info, info_cng)
932 update_and_set_cache(cng)
935 def subscribe(subscriber, %{ap_id: ap_id}) do
936 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
938 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
939 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
942 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
946 |> User.Info.add_to_subscribers(subscriber.ap_id)
949 |> put_embed(:info, info_cng)
950 |> update_and_set_cache()
955 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
956 with %User{} = user <- get_cached_by_ap_id(ap_id) do
957 info_cng = User.Info.remove_from_subscribers(user.info, unsubscriber.ap_id)
960 |> put_embed(:info, info_cng)
961 |> update_and_set_cache()
965 def block(blocker, %User{ap_id: ap_id} = blocked) do
966 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
968 if following?(blocker, blocked) do
969 {:ok, blocker, _} = unfollow(blocker, blocked)
975 # clear any requested follows as well
977 case CommonAPI.reject_follow_request(blocked, blocker) do
978 {:ok, %User{} = updated_blocked} -> updated_blocked
983 if subscribed_to?(blocked, blocker) do
984 {:ok, blocker} = unsubscribe(blocked, blocker)
990 if following?(blocked, blocker) do
991 unfollow(blocked, blocker)
994 {:ok, blocker} = update_follower_count(blocker)
998 |> User.Info.add_to_block(ap_id)
1002 |> put_embed(:info, info_cng)
1004 update_and_set_cache(cng)
1007 # helper to handle the block given only an actor's AP id
1008 def block(blocker, %{ap_id: ap_id}) do
1009 block(blocker, get_cached_by_ap_id(ap_id))
1012 def unblock(blocker, %{ap_id: ap_id}) do
1015 |> User.Info.remove_from_block(ap_id)
1019 |> put_embed(:info, info_cng)
1021 update_and_set_cache(cng)
1024 def mutes?(nil, _), do: false
1025 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
1027 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
1028 def muted_notifications?(nil, _), do: false
1030 def muted_notifications?(user, %{ap_id: ap_id}),
1031 do: Enum.member?(user.info.muted_notifications, ap_id)
1033 def blocks?(%User{} = user, %User{} = target) do
1034 blocks_ap_id?(user, target) || blocks_domain?(user, target)
1037 def blocks?(nil, _), do: false
1039 def blocks_ap_id?(%User{} = user, %User{} = target) do
1040 Enum.member?(user.info.blocks, target.ap_id)
1043 def blocks_ap_id?(_, _), do: false
1045 def blocks_domain?(%User{} = user, %User{} = target) do
1046 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
1047 %{host: host} = URI.parse(target.ap_id)
1048 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1051 def blocks_domain?(_, _), do: false
1053 def subscribed_to?(user, %{ap_id: ap_id}) do
1054 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1055 Enum.member?(target.info.subscribers, user.ap_id)
1059 @spec muted_users(User.t()) :: [User.t()]
1060 def muted_users(user) do
1061 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
1065 @spec blocked_users(User.t()) :: [User.t()]
1066 def blocked_users(user) do
1067 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
1071 @spec subscribers(User.t()) :: [User.t()]
1072 def subscribers(user) do
1073 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
1077 def block_domain(user, domain) do
1080 |> User.Info.add_to_domain_block(domain)
1084 |> put_embed(:info, info_cng)
1086 update_and_set_cache(cng)
1089 def unblock_domain(user, domain) do
1092 |> User.Info.remove_from_domain_block(domain)
1096 |> put_embed(:info, info_cng)
1098 update_and_set_cache(cng)
1101 def deactivate_async(user, status \\ true) do
1102 BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
1105 def deactivate(%User{} = user, status \\ true) do
1106 info_cng = User.Info.set_activation_status(user.info, status)
1108 with {:ok, friends} <- User.get_friends(user),
1109 {:ok, followers} <- User.get_followers(user),
1113 |> put_embed(:info, info_cng)
1114 |> update_and_set_cache() do
1115 Enum.each(followers, &invalidate_cache(&1))
1116 Enum.each(friends, &update_follower_count(&1))
1122 def update_notification_settings(%User{} = user, settings \\ %{}) do
1123 info_changeset = User.Info.update_notification_settings(user.info, settings)
1126 |> put_embed(:info, info_changeset)
1127 |> update_and_set_cache()
1130 def delete(%User{} = user) do
1131 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1134 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1135 def perform(:delete, %User{} = user) do
1136 {:ok, _user} = ActivityPub.delete(user)
1138 # Remove all relationships
1139 {:ok, followers} = User.get_followers(user)
1141 Enum.each(followers, fn follower ->
1142 ActivityPub.unfollow(follower, user)
1143 User.unfollow(follower, user)
1146 {:ok, friends} = User.get_friends(user)
1148 Enum.each(friends, fn followed ->
1149 ActivityPub.unfollow(user, followed)
1150 User.unfollow(user, followed)
1153 delete_user_activities(user)
1154 invalidate_cache(user)
1158 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1159 def perform(:fetch_initial_posts, %User{} = user) do
1160 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1163 # Insert all the posts in reverse order, so they're in the right order on the timeline
1164 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
1165 &Pleroma.Web.Federator.incoming_ap_doc/1
1171 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1173 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1174 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1175 when is_list(blocked_identifiers) do
1177 blocked_identifiers,
1178 fn blocked_identifier ->
1179 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1180 {:ok, blocker} <- block(blocker, blocked),
1181 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1185 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1192 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1193 def perform(:follow_import, %User{} = follower, followed_identifiers)
1194 when is_list(followed_identifiers) do
1196 followed_identifiers,
1197 fn followed_identifier ->
1198 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1199 {:ok, follower} <- maybe_direct_follow(follower, followed),
1200 {:ok, _} <- ActivityPub.follow(follower, followed) do
1204 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1211 @spec external_users_query() :: Ecto.Query.t()
1212 def external_users_query do
1220 @spec external_users(keyword()) :: [User.t()]
1221 def external_users(opts \\ []) do
1223 external_users_query()
1224 |> select([u], struct(u, [:id, :ap_id, :info]))
1228 do: where(query, [u], u.id > ^opts[:max_id]),
1233 do: limit(query, ^opts[:limit]),
1239 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1240 BackgroundWorker.enqueue("blocks_import", %{
1241 "blocker_id" => blocker.id,
1242 "blocked_identifiers" => blocked_identifiers
1246 def follow_import(%User{} = follower, followed_identifiers)
1247 when is_list(followed_identifiers) do
1248 BackgroundWorker.enqueue("follow_import", %{
1249 "follower_id" => follower.id,
1250 "followed_identifiers" => followed_identifiers
1254 def delete_user_activities(%User{ap_id: ap_id} = user) do
1256 |> Activity.Queries.by_actor()
1257 |> RepoStreamer.chunk_stream(50)
1258 |> Stream.each(fn activities ->
1259 Enum.each(activities, &delete_activity(&1))
1266 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1268 |> Object.normalize()
1269 |> ActivityPub.delete()
1272 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1273 user = get_cached_by_ap_id(activity.actor)
1274 object = Object.normalize(activity)
1276 ActivityPub.unlike(user, object)
1279 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1280 user = get_cached_by_ap_id(activity.actor)
1281 object = Object.normalize(activity)
1283 ActivityPub.unannounce(user, object)
1286 defp delete_activity(_activity), do: "Doing nothing"
1288 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1289 Pleroma.HTML.Scrubber.TwitterText
1292 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1294 def fetch_by_ap_id(ap_id) do
1295 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1302 case OStatus.make_user(ap_id) do
1303 {:ok, user} -> {:ok, user}
1304 _ -> {:error, "Could not fetch by AP id"}
1309 def get_or_fetch_by_ap_id(ap_id) do
1310 user = get_cached_by_ap_id(ap_id)
1312 if !is_nil(user) and !User.needs_update?(user) do
1315 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1316 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1318 resp = fetch_by_ap_id(ap_id)
1320 if should_fetch_initial do
1321 with {:ok, %User{} = user} <- resp do
1322 fetch_initial_posts(user)
1330 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1331 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1332 if user = get_cached_by_ap_id(uri) do
1336 %User{info: %User.Info{}}
1337 |> cast(%{}, [:ap_id, :nickname, :local])
1338 |> put_change(:ap_id, uri)
1339 |> put_change(:nickname, nickname)
1340 |> put_change(:local, true)
1341 |> put_change(:follower_address, uri <> "/followers")
1343 {:ok, user} = Repo.insert(changes)
1349 def public_key_from_info(%{
1350 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1354 |> :public_key.pem_decode()
1356 |> :public_key.pem_entry_decode()
1362 def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
1363 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1366 def public_key_from_info(_), do: {:error, "not found key"}
1368 def get_public_key_for_ap_id(ap_id) do
1369 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1370 {:ok, public_key} <- public_key_from_info(user.info) do
1377 defp blank?(""), do: nil
1378 defp blank?(n), do: n
1380 def insert_or_update_user(data) do
1382 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1383 |> remote_user_creation()
1384 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1388 def ap_enabled?(%User{local: true}), do: true
1389 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1390 def ap_enabled?(_), do: false
1392 @doc "Gets or fetch a user by uri or nickname."
1393 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1394 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1395 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1397 # wait a period of time and return newest version of the User structs
1398 # this is because we have synchronous follow APIs and need to simulate them
1399 # with an async handshake
1400 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1401 with %User{} = a <- User.get_cached_by_id(a.id),
1402 %User{} = b <- User.get_cached_by_id(b.id) do
1410 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1411 with :ok <- :timer.sleep(timeout),
1412 %User{} = a <- User.get_cached_by_id(a.id),
1413 %User{} = b <- User.get_cached_by_id(b.id) do
1421 def parse_bio(bio) when is_binary(bio) and bio != "" do
1423 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1427 def parse_bio(_), do: ""
1429 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1430 # TODO: get profile URLs other than user.ap_id
1431 profile_urls = [user.ap_id]
1434 |> CommonUtils.format_input("text/plain",
1435 mentions_format: :full,
1436 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1441 def parse_bio(_, _), do: ""
1443 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1444 Repo.transaction(fn ->
1445 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1449 def tag(nickname, tags) when is_binary(nickname),
1450 do: tag(get_by_nickname(nickname), tags)
1452 def tag(%User{} = user, tags),
1453 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1455 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1456 Repo.transaction(fn ->
1457 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1461 def untag(nickname, tags) when is_binary(nickname),
1462 do: untag(get_by_nickname(nickname), tags)
1464 def untag(%User{} = user, tags),
1465 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1467 defp update_tags(%User{} = user, new_tags) do
1468 {:ok, updated_user} =
1470 |> change(%{tags: new_tags})
1471 |> update_and_set_cache()
1476 defp normalize_tags(tags) do
1479 |> Enum.map(&String.downcase(&1))
1482 defp local_nickname_regex do
1483 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1484 @extended_local_nickname_regex
1486 @strict_local_nickname_regex
1490 def local_nickname(nickname_or_mention) do
1493 |> String.split("@")
1497 def full_nickname(nickname_or_mention),
1498 do: String.trim_leading(nickname_or_mention, "@")
1500 def error_user(ap_id) do
1505 nickname: "erroruser@example.com",
1506 inserted_at: NaiveDateTime.utc_now()
1510 @spec all_superusers() :: [User.t()]
1511 def all_superusers do
1512 User.Query.build(%{super_users: true, local: true, deactivated: false})
1516 def showing_reblogs?(%User{} = user, %User{} = target) do
1517 target.ap_id not in user.info.muted_reblogs
1521 The function returns a query to get users with no activity for given interval of days.
1522 Inactive users are those who didn't read any notification, or had any activity where
1523 the user is the activity's actor, during `inactivity_threshold` days.
1524 Deactivated users will not appear in this list.
1528 iex> Pleroma.User.list_inactive_users()
1531 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1532 def list_inactive_users_query(inactivity_threshold \\ 7) do
1533 negative_inactivity_threshold = -inactivity_threshold
1534 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1535 # Subqueries are not supported in `where` clauses, join gets too complicated.
1536 has_read_notifications =
1537 from(n in Pleroma.Notification,
1538 where: n.seen == true,
1540 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1543 |> Pleroma.Repo.all()
1545 from(u in Pleroma.User,
1546 left_join: a in Pleroma.Activity,
1547 on: u.ap_id == a.actor,
1548 where: not is_nil(u.nickname),
1549 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1550 where: u.id not in ^has_read_notifications,
1553 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1554 is_nil(max(a.inserted_at))
1559 Enable or disable email notifications for user
1563 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1564 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1566 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1567 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1569 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1570 {:ok, t()} | {:error, Ecto.Changeset.t()}
1571 def switch_email_notifications(user, type, status) do
1572 info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})
1575 |> put_embed(:info, info)
1576 |> update_and_set_cache()
1580 Set `last_digest_emailed_at` value for the user to current time
1582 @spec touch_last_digest_emailed_at(t()) :: t()
1583 def touch_last_digest_emailed_at(user) do
1584 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1586 {:ok, updated_user} =
1588 |> change(%{last_digest_emailed_at: now})
1589 |> update_and_set_cache()
1594 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1595 def toggle_confirmation(%User{} = user) do
1596 need_confirmation? = !user.info.confirmation_pending
1599 User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
1603 |> put_embed(:info, info_changeset)
1604 |> update_and_set_cache()
1607 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1611 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1612 # use instance-default
1613 config = Pleroma.Config.get([:assets, :mascots])
1614 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1615 mascot = Keyword.get(config, default_mascot)
1618 "id" => "default-mascot",
1619 "url" => mascot[:url],
1620 "preview_url" => mascot[:url],
1622 "mime_type" => mascot[:mime_type]
1627 def ensure_keys_present(%User{info: info} = user) do
1631 {:ok, pem} = Keys.generate_rsa_pem()
1634 |> Ecto.Changeset.change()
1635 |> Ecto.Changeset.put_embed(:info, User.Info.set_keys(info, pem))
1636 |> update_and_set_cache()
1640 def get_ap_ids_by_nicknames(nicknames) do
1642 where: u.nickname in ^nicknames,
1648 defdelegate search(query, opts \\ []), to: User.Search
1650 defp put_password_hash(
1651 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1653 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1656 defp put_password_hash(changeset), do: changeset
1658 def is_internal_user?(%User{nickname: nil}), do: true
1659 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1660 def is_internal_user?(_), do: false
1662 # A hack because user delete activities have a fake id for whatever reason
1663 # TODO: Get rid of this
1664 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
1666 def get_delivered_users_by_object_id(object_id) do
1668 inner_join: delivery in assoc(u, :deliveries),
1669 where: delivery.object_id == ^object_id
1674 def change_email(user, email) do
1676 |> cast(%{email: email}, [:email])
1677 |> validate_required([:email])
1678 |> unique_constraint(:email)
1679 |> validate_format(:email, @email_regex)
1680 |> update_and_set_cache()