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)
272 |> put_embed(:info, User.Info.set_password_reset_pending(struct.info, false))
275 @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
276 def reset_password(%User{id: user_id} = user, data) do
279 |> Multi.update(:user, password_update_changeset(user, data))
280 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
281 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
283 case Repo.transaction(multi) do
284 {:ok, %{user: user} = _} -> set_cache(user)
285 {:error, _, changeset, _} -> {:error, changeset}
289 def force_password_reset_async(user) do
290 BackgroundWorker.enqueue("force_password_reset", %{"user_id" => user.id})
293 @spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
294 def force_password_reset(user) do
295 info_cng = User.Info.set_password_reset_pending(user.info, true)
299 |> put_embed(:info, info_cng)
300 |> update_and_set_cache()
303 def register_changeset(struct, params \\ %{}, opts \\ []) do
304 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
305 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
308 if is_nil(opts[:need_confirmation]) do
309 Pleroma.Config.get([:instance, :account_activation_required])
311 opts[:need_confirmation]
315 User.Info.confirmation_changeset(%User.Info{}, need_confirmation: need_confirmation?)
319 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
320 |> validate_required([:name, :nickname, :password, :password_confirmation])
321 |> validate_confirmation(:password)
322 |> unique_constraint(:email)
323 |> unique_constraint(:nickname)
324 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
325 |> validate_format(:nickname, local_nickname_regex())
326 |> validate_format(:email, @email_regex)
327 |> validate_length(:bio, max: bio_limit)
328 |> validate_length(:name, min: 1, max: name_limit)
329 |> put_change(:info, info_change)
332 if opts[:external] do
335 validate_required(changeset, [:email])
338 if changeset.valid? do
339 ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
340 followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
344 |> put_change(:ap_id, ap_id)
345 |> unique_constraint(:ap_id)
346 |> put_change(:following, [followers])
347 |> put_change(:follower_address, followers)
353 defp autofollow_users(user) do
354 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
357 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
360 follow_all(user, autofollowed_users)
363 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
364 def register(%Ecto.Changeset{} = changeset) do
365 with {:ok, user} <- Repo.insert(changeset),
366 {:ok, user} <- post_register_action(user) do
371 def post_register_action(%User{} = user) do
372 with {:ok, user} <- autofollow_users(user),
373 {:ok, user} <- set_cache(user),
374 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
375 {:ok, _} <- try_send_confirmation_email(user) do
380 def try_send_confirmation_email(%User{} = user) do
381 if user.info.confirmation_pending &&
382 Pleroma.Config.get([:instance, :account_activation_required]) do
384 |> Pleroma.Emails.UserEmail.account_confirmation_email()
385 |> Pleroma.Emails.Mailer.deliver_async()
393 def needs_update?(%User{local: true}), do: false
395 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
397 def needs_update?(%User{local: false} = user) do
398 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
401 def needs_update?(_), do: true
403 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
404 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
408 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
409 follow(follower, followed)
412 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
413 if not User.ap_enabled?(followed) do
414 follow(follower, followed)
420 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
421 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
422 def follow_all(follower, followeds) do
425 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
426 |> Enum.map(fn %{follower_address: fa} -> fa end)
430 where: u.id == ^follower.id,
435 "array(select distinct unnest (array_cat(?, ?)))",
444 {1, [follower]} = Repo.update_all(q, [])
446 Enum.each(followeds, fn followed ->
447 update_follower_count(followed)
453 def follow(%User{} = follower, %User{info: info} = followed) do
454 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
455 ap_followers = followed.follower_address
459 {:error, "Could not follow user: You are deactivated."}
461 deny_follow_blocked and blocks?(followed, follower) ->
462 {:error, "Could not follow user: #{followed.nickname} blocked you."}
465 if !followed.local && follower.local && !ap_enabled?(followed) do
466 Websub.subscribe(follower, followed)
471 where: u.id == ^follower.id,
472 update: [push: [following: ^ap_followers]],
476 {1, [follower]} = Repo.update_all(q, [])
478 follower = maybe_update_following_count(follower)
480 {:ok, _} = update_follower_count(followed)
486 def unfollow(%User{} = follower, %User{} = followed) do
487 ap_followers = followed.follower_address
489 if following?(follower, followed) and follower.ap_id != followed.ap_id do
492 where: u.id == ^follower.id,
493 update: [pull: [following: ^ap_followers]],
497 {1, [follower]} = Repo.update_all(q, [])
499 follower = maybe_update_following_count(follower)
501 {:ok, followed} = update_follower_count(followed)
505 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
507 {:error, "Not subscribed!"}
511 @spec following?(User.t(), User.t()) :: boolean
512 def following?(%User{} = follower, %User{} = followed) do
513 Enum.member?(follower.following, followed.follower_address)
516 def locked?(%User{} = user) do
517 user.info.locked || false
521 Repo.get_by(User, id: id)
524 def get_by_ap_id(ap_id) do
525 Repo.get_by(User, ap_id: ap_id)
528 def get_all_by_ap_id(ap_ids) do
529 from(u in __MODULE__,
530 where: u.ap_id in ^ap_ids
535 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
536 # of the ap_id and the domain and tries to get that user
537 def get_by_guessed_nickname(ap_id) do
538 domain = URI.parse(ap_id).host
539 name = List.last(String.split(ap_id, "/"))
540 nickname = "#{name}@#{domain}"
542 get_cached_by_nickname(nickname)
545 def set_cache({:ok, user}), do: set_cache(user)
546 def set_cache({:error, err}), do: {:error, err}
548 def set_cache(%User{} = user) do
549 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
550 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
551 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
555 def update_and_set_cache(changeset) do
556 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
563 def invalidate_cache(user) do
564 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
565 Cachex.del(:user_cache, "nickname:#{user.nickname}")
566 Cachex.del(:user_cache, "user_info:#{user.id}")
569 def get_cached_by_ap_id(ap_id) do
570 key = "ap_id:#{ap_id}"
571 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
574 def get_cached_by_id(id) do
578 Cachex.fetch!(:user_cache, key, fn _ ->
582 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
583 {:commit, user.ap_id}
589 get_cached_by_ap_id(ap_id)
592 def get_cached_by_nickname(nickname) do
593 key = "nickname:#{nickname}"
595 Cachex.fetch!(:user_cache, key, fn ->
596 user_result = get_or_fetch_by_nickname(nickname)
599 {:ok, user} -> {:commit, user}
600 {:error, _error} -> {:ignore, nil}
605 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
606 restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
609 is_integer(nickname_or_id) or Pleroma.FlakeId.is_flake_id?(nickname_or_id) ->
610 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
612 restrict_to_local == false ->
613 get_cached_by_nickname(nickname_or_id)
615 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
616 get_cached_by_nickname(nickname_or_id)
623 def get_by_nickname(nickname) do
624 Repo.get_by(User, nickname: nickname) ||
625 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
626 Repo.get_by(User, nickname: local_nickname(nickname))
630 def get_by_email(email), do: Repo.get_by(User, email: email)
632 def get_by_nickname_or_email(nickname_or_email) do
633 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
636 def get_cached_user_info(user) do
637 key = "user_info:#{user.id}"
638 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
641 def fetch_by_nickname(nickname) do
642 ap_try = ActivityPub.make_user_from_nickname(nickname)
645 {:ok, user} -> {:ok, user}
646 _ -> OStatus.make_user(nickname)
650 def get_or_fetch_by_nickname(nickname) do
651 with %User{} = user <- get_by_nickname(nickname) do
655 with [_nick, _domain] <- String.split(nickname, "@"),
656 {:ok, user} <- fetch_by_nickname(nickname) do
657 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
658 fetch_initial_posts(user)
663 _e -> {:error, "not found " <> nickname}
668 @doc "Fetch some posts when the user has just been federated with"
669 def fetch_initial_posts(user) do
670 BackgroundWorker.enqueue("fetch_initial_posts", %{"user_id" => user.id})
673 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
674 def get_followers_query(%User{} = user, nil) do
675 User.Query.build(%{followers: user, deactivated: false})
678 def get_followers_query(user, page) do
679 from(u in get_followers_query(user, nil))
680 |> User.Query.paginate(page, 20)
683 @spec get_followers_query(User.t()) :: Ecto.Query.t()
684 def get_followers_query(user), do: get_followers_query(user, nil)
686 @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
687 def get_followers(user, page \\ nil) do
689 |> get_followers_query(page)
693 @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
694 def get_external_followers(user, page \\ nil) do
697 |> get_followers_query(page)
698 |> User.Query.build(%{external: true})
703 def get_followers_ids(user, page \\ nil) do
704 q = get_followers_query(user, page)
706 Repo.all(from(u in q, select: u.id))
709 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
710 def get_friends_query(%User{} = user, nil) do
711 User.Query.build(%{friends: user, deactivated: false})
714 def get_friends_query(user, page) do
715 from(u in get_friends_query(user, nil))
716 |> User.Query.paginate(page, 20)
719 @spec get_friends_query(User.t()) :: Ecto.Query.t()
720 def get_friends_query(user), do: get_friends_query(user, nil)
722 def get_friends(user, page \\ nil) do
724 |> get_friends_query(page)
728 def get_friends_ids(user, page \\ nil) do
729 q = get_friends_query(user, page)
731 Repo.all(from(u in q, select: u.id))
734 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
735 def get_follow_requests(%User{} = user) do
737 |> Activity.follow_requests_for_actor()
738 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
739 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
740 |> group_by([a, u], u.id)
745 def increase_note_count(%User{} = user) do
747 |> where(id: ^user.id)
752 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
759 |> Repo.update_all([])
761 {1, [user]} -> set_cache(user)
766 def decrease_note_count(%User{} = user) do
768 |> where(id: ^user.id)
773 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
780 |> Repo.update_all([])
782 {1, [user]} -> set_cache(user)
787 def update_note_count(%User{} = user) do
791 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
795 note_count = Repo.one(note_count_query)
797 info_cng = User.Info.set_note_count(user.info, note_count)
801 |> put_embed(:info, info_cng)
802 |> update_and_set_cache()
805 @spec maybe_fetch_follow_information(User.t()) :: User.t()
806 def maybe_fetch_follow_information(user) do
807 with {:ok, user} <- fetch_follow_information(user) do
811 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
817 def fetch_follow_information(user) do
818 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
819 info_cng = User.Info.follow_information_update(user.info, info)
824 |> put_embed(:info, info_cng)
826 update_and_set_cache(changeset)
833 def update_follower_count(%User{} = user) do
834 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
835 follower_count_query =
836 User.Query.build(%{followers: user, deactivated: false})
837 |> select([u], %{count: count(u.id)})
840 |> where(id: ^user.id)
841 |> join(:inner, [u], s in subquery(follower_count_query))
846 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
853 |> Repo.update_all([])
855 {1, [user]} -> set_cache(user)
859 {:ok, maybe_fetch_follow_information(user)}
863 @spec maybe_update_following_count(User.t()) :: User.t()
864 def maybe_update_following_count(%User{local: false} = user) do
865 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
866 maybe_fetch_follow_information(user)
872 def maybe_update_following_count(user), do: user
874 def remove_duplicated_following(%User{following: following} = user) do
875 uniq_following = Enum.uniq(following)
877 if length(following) == length(uniq_following) do
881 |> update_changeset(%{following: uniq_following})
882 |> update_and_set_cache()
886 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
887 def get_users_from_set(ap_ids, local_only \\ true) do
888 criteria = %{ap_id: ap_ids, deactivated: false}
889 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
891 User.Query.build(criteria)
895 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
896 def get_recipients_from_activity(%Activity{recipients: to}) do
897 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
901 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
902 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
906 User.Info.add_to_mutes(info, ap_id)
907 |> User.Info.add_to_muted_notifications(info, ap_id, notifications?)
911 |> put_embed(:info, info_cng)
913 update_and_set_cache(cng)
916 def unmute(muter, %{ap_id: ap_id}) do
920 User.Info.remove_from_mutes(info, ap_id)
921 |> User.Info.remove_from_muted_notifications(info, ap_id)
925 |> put_embed(:info, info_cng)
927 update_and_set_cache(cng)
930 def subscribe(subscriber, %{ap_id: ap_id}) do
931 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
933 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
934 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
937 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
941 |> User.Info.add_to_subscribers(subscriber.ap_id)
944 |> put_embed(:info, info_cng)
945 |> update_and_set_cache()
950 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
951 with %User{} = user <- get_cached_by_ap_id(ap_id) do
954 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
957 |> put_embed(:info, info_cng)
958 |> update_and_set_cache()
962 def block(blocker, %User{ap_id: ap_id} = blocked) do
963 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
965 if following?(blocker, blocked) do
966 {:ok, blocker, _} = unfollow(blocker, blocked)
972 # clear any requested follows as well
974 case CommonAPI.reject_follow_request(blocked, blocker) do
975 {:ok, %User{} = updated_blocked} -> updated_blocked
980 if subscribed_to?(blocked, blocker) do
981 {:ok, blocker} = unsubscribe(blocked, blocker)
987 if following?(blocked, blocker) do
988 unfollow(blocked, blocker)
991 {:ok, blocker} = update_follower_count(blocker)
995 |> User.Info.add_to_block(ap_id)
999 |> put_embed(:info, info_cng)
1001 update_and_set_cache(cng)
1004 # helper to handle the block given only an actor's AP id
1005 def block(blocker, %{ap_id: ap_id}) do
1006 block(blocker, get_cached_by_ap_id(ap_id))
1009 def unblock(blocker, %{ap_id: ap_id}) do
1012 |> User.Info.remove_from_block(ap_id)
1016 |> put_embed(:info, info_cng)
1018 update_and_set_cache(cng)
1021 def mutes?(nil, _), do: false
1022 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
1024 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
1025 def muted_notifications?(nil, _), do: false
1027 def muted_notifications?(user, %{ap_id: ap_id}),
1028 do: Enum.member?(user.info.muted_notifications, ap_id)
1030 def blocks?(%User{} = user, %User{} = target) do
1031 blocks_ap_id?(user, target) || blocks_domain?(user, target)
1034 def blocks?(nil, _), do: false
1036 def blocks_ap_id?(%User{} = user, %User{} = target) do
1037 Enum.member?(user.info.blocks, target.ap_id)
1040 def blocks_ap_id?(_, _), do: false
1042 def blocks_domain?(%User{} = user, %User{} = target) do
1043 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
1044 %{host: host} = URI.parse(target.ap_id)
1045 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1048 def blocks_domain?(_, _), do: false
1050 def subscribed_to?(user, %{ap_id: ap_id}) do
1051 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1052 Enum.member?(target.info.subscribers, user.ap_id)
1056 @spec muted_users(User.t()) :: [User.t()]
1057 def muted_users(user) do
1058 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
1062 @spec blocked_users(User.t()) :: [User.t()]
1063 def blocked_users(user) do
1064 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
1068 @spec subscribers(User.t()) :: [User.t()]
1069 def subscribers(user) do
1070 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
1074 def block_domain(user, domain) do
1077 |> User.Info.add_to_domain_block(domain)
1081 |> put_embed(:info, info_cng)
1083 update_and_set_cache(cng)
1086 def unblock_domain(user, domain) do
1089 |> User.Info.remove_from_domain_block(domain)
1093 |> put_embed(:info, info_cng)
1095 update_and_set_cache(cng)
1098 def deactivate_async(user, status \\ true) do
1099 BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
1102 def deactivate(%User{} = user, status \\ true) do
1103 info_cng = User.Info.set_activation_status(user.info, status)
1108 |> put_embed(:info, info_cng)
1109 |> update_and_set_cache() do
1110 Enum.each(get_followers(user), &invalidate_cache/1)
1111 Enum.each(get_friends(user), &update_follower_count/1)
1117 def update_notification_settings(%User{} = user, settings \\ %{}) do
1118 info_changeset = User.Info.update_notification_settings(user.info, settings)
1121 |> put_embed(:info, info_changeset)
1122 |> update_and_set_cache()
1125 def delete(%User{} = user) do
1126 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1129 def perform(:force_password_reset, user), do: force_password_reset(user)
1131 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1132 def perform(:delete, %User{} = user) do
1133 {:ok, _user} = ActivityPub.delete(user)
1135 # Remove all relationships
1138 |> Enum.each(fn follower ->
1139 ActivityPub.unfollow(follower, user)
1140 unfollow(follower, user)
1145 |> Enum.each(fn followed ->
1146 ActivityPub.unfollow(user, followed)
1147 unfollow(user, followed)
1150 delete_user_activities(user)
1151 invalidate_cache(user)
1155 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1156 def perform(:fetch_initial_posts, %User{} = user) do
1157 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1159 # Insert all the posts in reverse order, so they're in the right order on the timeline
1160 user.info.source_data["outbox"]
1161 |> Utils.fetch_ordered_collection(pages)
1163 |> Enum.each(&Pleroma.Web.Federator.incoming_ap_doc/1)
1166 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1168 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1169 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1170 when is_list(blocked_identifiers) do
1172 blocked_identifiers,
1173 fn blocked_identifier ->
1174 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1175 {:ok, blocker} <- block(blocker, blocked),
1176 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1180 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1187 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1188 def perform(:follow_import, %User{} = follower, followed_identifiers)
1189 when is_list(followed_identifiers) do
1191 followed_identifiers,
1192 fn followed_identifier ->
1193 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1194 {:ok, follower} <- maybe_direct_follow(follower, followed),
1195 {:ok, _} <- ActivityPub.follow(follower, followed) do
1199 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1206 @spec external_users_query() :: Ecto.Query.t()
1207 def external_users_query do
1215 @spec external_users(keyword()) :: [User.t()]
1216 def external_users(opts \\ []) do
1218 external_users_query()
1219 |> select([u], struct(u, [:id, :ap_id, :info]))
1223 do: where(query, [u], u.id > ^opts[:max_id]),
1228 do: limit(query, ^opts[:limit]),
1234 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1235 BackgroundWorker.enqueue("blocks_import", %{
1236 "blocker_id" => blocker.id,
1237 "blocked_identifiers" => blocked_identifiers
1241 def follow_import(%User{} = follower, followed_identifiers)
1242 when is_list(followed_identifiers) do
1243 BackgroundWorker.enqueue("follow_import", %{
1244 "follower_id" => follower.id,
1245 "followed_identifiers" => followed_identifiers
1249 def delete_user_activities(%User{ap_id: ap_id}) do
1251 |> Activity.Queries.by_actor()
1252 |> RepoStreamer.chunk_stream(50)
1253 |> Stream.each(fn activities -> Enum.each(activities, &delete_activity/1) end)
1257 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1259 |> Object.normalize()
1260 |> ActivityPub.delete()
1263 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1264 user = get_cached_by_ap_id(activity.actor)
1265 object = Object.normalize(activity)
1267 ActivityPub.unlike(user, object)
1270 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1271 user = get_cached_by_ap_id(activity.actor)
1272 object = Object.normalize(activity)
1274 ActivityPub.unannounce(user, object)
1277 defp delete_activity(_activity), do: "Doing nothing"
1279 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1280 Pleroma.HTML.Scrubber.TwitterText
1283 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1285 def fetch_by_ap_id(ap_id) do
1286 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1293 case OStatus.make_user(ap_id) do
1294 {:ok, user} -> {:ok, user}
1295 _ -> {:error, "Could not fetch by AP id"}
1300 def get_or_fetch_by_ap_id(ap_id) do
1301 user = get_cached_by_ap_id(ap_id)
1303 if !is_nil(user) and !User.needs_update?(user) do
1306 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1307 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1309 resp = fetch_by_ap_id(ap_id)
1311 if should_fetch_initial do
1312 with {:ok, %User{} = user} <- resp do
1313 fetch_initial_posts(user)
1321 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1322 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1323 if user = get_cached_by_ap_id(uri) do
1327 %User{info: %User.Info{}}
1328 |> cast(%{}, [:ap_id, :nickname, :local])
1329 |> put_change(:ap_id, uri)
1330 |> put_change(:nickname, nickname)
1331 |> put_change(:local, true)
1332 |> put_change(:follower_address, uri <> "/followers")
1334 {:ok, user} = Repo.insert(changes)
1340 def public_key_from_info(%{
1341 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1345 |> :public_key.pem_decode()
1347 |> :public_key.pem_entry_decode()
1353 def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
1354 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1357 def public_key_from_info(_), do: {:error, "not found key"}
1359 def get_public_key_for_ap_id(ap_id) do
1360 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1361 {:ok, public_key} <- public_key_from_info(user.info) do
1368 defp blank?(""), do: nil
1369 defp blank?(n), do: n
1371 def insert_or_update_user(data) do
1373 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1374 |> remote_user_creation()
1375 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1379 def ap_enabled?(%User{local: true}), do: true
1380 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1381 def ap_enabled?(_), do: false
1383 @doc "Gets or fetch a user by uri or nickname."
1384 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1385 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1386 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1388 # wait a period of time and return newest version of the User structs
1389 # this is because we have synchronous follow APIs and need to simulate them
1390 # with an async handshake
1391 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1392 with %User{} = a <- User.get_cached_by_id(a.id),
1393 %User{} = b <- User.get_cached_by_id(b.id) do
1401 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1402 with :ok <- :timer.sleep(timeout),
1403 %User{} = a <- User.get_cached_by_id(a.id),
1404 %User{} = b <- User.get_cached_by_id(b.id) do
1412 def parse_bio(bio) when is_binary(bio) and bio != "" do
1414 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1418 def parse_bio(_), do: ""
1420 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1421 # TODO: get profile URLs other than user.ap_id
1422 profile_urls = [user.ap_id]
1425 |> CommonUtils.format_input("text/plain",
1426 mentions_format: :full,
1427 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1432 def parse_bio(_, _), do: ""
1434 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1435 Repo.transaction(fn ->
1436 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1440 def tag(nickname, tags) when is_binary(nickname),
1441 do: tag(get_by_nickname(nickname), tags)
1443 def tag(%User{} = user, tags),
1444 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1446 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1447 Repo.transaction(fn ->
1448 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1452 def untag(nickname, tags) when is_binary(nickname),
1453 do: untag(get_by_nickname(nickname), tags)
1455 def untag(%User{} = user, tags),
1456 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1458 defp update_tags(%User{} = user, new_tags) do
1459 {:ok, updated_user} =
1461 |> change(%{tags: new_tags})
1462 |> update_and_set_cache()
1467 defp normalize_tags(tags) do
1470 |> Enum.map(&String.downcase(&1))
1473 defp local_nickname_regex do
1474 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1475 @extended_local_nickname_regex
1477 @strict_local_nickname_regex
1481 def local_nickname(nickname_or_mention) do
1484 |> String.split("@")
1488 def full_nickname(nickname_or_mention),
1489 do: String.trim_leading(nickname_or_mention, "@")
1491 def error_user(ap_id) do
1496 nickname: "erroruser@example.com",
1497 inserted_at: NaiveDateTime.utc_now()
1501 @spec all_superusers() :: [User.t()]
1502 def all_superusers do
1503 User.Query.build(%{super_users: true, local: true, deactivated: false})
1507 def showing_reblogs?(%User{} = user, %User{} = target) do
1508 target.ap_id not in user.info.muted_reblogs
1512 The function returns a query to get users with no activity for given interval of days.
1513 Inactive users are those who didn't read any notification, or had any activity where
1514 the user is the activity's actor, during `inactivity_threshold` days.
1515 Deactivated users will not appear in this list.
1519 iex> Pleroma.User.list_inactive_users()
1522 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1523 def list_inactive_users_query(inactivity_threshold \\ 7) do
1524 negative_inactivity_threshold = -inactivity_threshold
1525 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1526 # Subqueries are not supported in `where` clauses, join gets too complicated.
1527 has_read_notifications =
1528 from(n in Pleroma.Notification,
1529 where: n.seen == true,
1531 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1534 |> Pleroma.Repo.all()
1536 from(u in Pleroma.User,
1537 left_join: a in Pleroma.Activity,
1538 on: u.ap_id == a.actor,
1539 where: not is_nil(u.nickname),
1540 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1541 where: u.id not in ^has_read_notifications,
1544 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1545 is_nil(max(a.inserted_at))
1550 Enable or disable email notifications for user
1554 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1555 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1557 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1558 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1560 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1561 {:ok, t()} | {:error, Ecto.Changeset.t()}
1562 def switch_email_notifications(user, type, status) do
1563 info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})
1566 |> put_embed(:info, info)
1567 |> update_and_set_cache()
1571 Set `last_digest_emailed_at` value for the user to current time
1573 @spec touch_last_digest_emailed_at(t()) :: t()
1574 def touch_last_digest_emailed_at(user) do
1575 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1577 {:ok, updated_user} =
1579 |> change(%{last_digest_emailed_at: now})
1580 |> update_and_set_cache()
1585 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1586 def toggle_confirmation(%User{} = user) do
1587 need_confirmation? = !user.info.confirmation_pending
1590 User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
1594 |> put_embed(:info, info_changeset)
1595 |> update_and_set_cache()
1598 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1602 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1603 # use instance-default
1604 config = Pleroma.Config.get([:assets, :mascots])
1605 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1606 mascot = Keyword.get(config, default_mascot)
1609 "id" => "default-mascot",
1610 "url" => mascot[:url],
1611 "preview_url" => mascot[:url],
1613 "mime_type" => mascot[:mime_type]
1618 def ensure_keys_present(%User{info: info} = user) do
1622 {:ok, pem} = Keys.generate_rsa_pem()
1625 |> Ecto.Changeset.change()
1626 |> Ecto.Changeset.put_embed(:info, User.Info.set_keys(info, pem))
1627 |> update_and_set_cache()
1631 def get_ap_ids_by_nicknames(nicknames) do
1633 where: u.nickname in ^nicknames,
1639 defdelegate search(query, opts \\ []), to: User.Search
1641 defp put_password_hash(
1642 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1644 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1647 defp put_password_hash(changeset), do: changeset
1649 def is_internal_user?(%User{nickname: nil}), do: true
1650 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1651 def is_internal_user?(_), do: false
1653 # A hack because user delete activities have a fake id for whatever reason
1654 # TODO: Get rid of this
1655 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
1657 def get_delivered_users_by_object_id(object_id) do
1659 inner_join: delivery in assoc(u, :deliveries),
1660 where: delivery.object_id == ^object_id
1665 def change_email(user, email) do
1667 |> cast(%{email: email}, [:email])
1668 |> validate_required([:email])
1669 |> unique_constraint(:email)
1670 |> validate_format(:email, @email_regex)
1671 |> update_and_set_cache()