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, FlakeId.Ecto.CompatType, 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: "#{Web.base_url()}/users/#{nickname}"
111 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
112 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
114 @spec ap_following(User.t()) :: Sring.t()
115 def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
116 def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
118 def user_info(%User{} = user, args \\ %{}) do
120 Map.get(args, :following_count, user.info.following_count || following_count(user))
122 follower_count = Map.get(args, :follower_count, user.info.follower_count)
125 note_count: user.info.note_count,
126 locked: user.info.locked,
127 confirmation_pending: user.info.confirmation_pending,
128 default_scope: user.info.default_scope
130 |> Map.put(:following_count, following_count)
131 |> Map.put(:follower_count, follower_count)
134 def follow_state(%User{} = user, %User{} = target) do
135 case Utils.fetch_latest_follow(user, target) do
136 %{data: %{"state" => state}} -> state
137 # Ideally this would be nil, but then Cachex does not commit the value
142 def get_cached_follow_state(user, target) do
143 key = "follow_state:#{user.ap_id}|#{target.ap_id}"
144 Cachex.fetch!(:user_cache, key, fn _ -> {:commit, follow_state(user, target)} end)
147 @spec set_follow_state_cache(String.t(), String.t(), String.t()) :: {:ok | :error, boolean()}
148 def set_follow_state_cache(user_ap_id, target_ap_id, state) do
149 Cachex.put(:user_cache, "follow_state:#{user_ap_id}|#{target_ap_id}", state)
152 def set_info_cache(user, args) do
153 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user, args))
156 @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
157 def restrict_deactivated(query) do
159 where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info)
163 def following_count(%User{following: []}), do: 0
165 def following_count(%User{} = user) do
167 |> get_friends_query()
168 |> Repo.aggregate(:count, :id)
171 defp truncate_if_exists(params, key, max_length) do
172 if Map.has_key?(params, key) and is_binary(params[key]) do
173 {value, _chopped} = String.split_at(params[key], max_length)
174 Map.put(params, key, value)
180 def remote_user_creation(params) do
181 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
182 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
186 |> Map.put(:info, params[:info] || %{})
187 |> truncate_if_exists(:name, name_limit)
188 |> truncate_if_exists(:bio, bio_limit)
192 |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
193 |> validate_required([:name, :ap_id])
194 |> unique_constraint(:nickname)
195 |> validate_format(:nickname, @email_regex)
196 |> validate_length(:bio, max: bio_limit)
197 |> validate_length(:name, max: name_limit)
198 |> change_info(&User.Info.remote_user_creation(&1, params[:info]))
200 case params[:info][:source_data] do
201 %{"followers" => followers, "following" => following} ->
203 |> put_change(:follower_address, followers)
204 |> put_change(:following_address, following)
207 followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
208 put_change(changeset, :follower_address, followers)
212 def update_changeset(struct, params \\ %{}) do
213 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
214 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
217 |> cast(params, [:bio, :name, :avatar, :following])
218 |> unique_constraint(:nickname)
219 |> validate_format(:nickname, local_nickname_regex())
220 |> validate_length(:bio, max: bio_limit)
221 |> validate_length(:name, min: 1, max: name_limit)
224 def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
225 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
226 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
228 params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
239 |> unique_constraint(:nickname)
240 |> validate_format(:nickname, local_nickname_regex())
241 |> validate_length(:bio, max: bio_limit)
242 |> validate_length(:name, max: name_limit)
243 |> change_info(&User.Info.user_upgrade(&1, params[:info], remote?))
246 def password_update_changeset(struct, params) do
248 |> cast(params, [:password, :password_confirmation])
249 |> validate_required([:password, :password_confirmation])
250 |> validate_confirmation(:password)
252 |> put_embed(:info, User.Info.set_password_reset_pending(struct.info, false))
255 @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
256 def reset_password(%User{id: user_id} = user, data) do
259 |> Multi.update(:user, password_update_changeset(user, data))
260 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
261 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
263 case Repo.transaction(multi) do
264 {:ok, %{user: user} = _} -> set_cache(user)
265 {:error, _, changeset, _} -> {:error, changeset}
269 def force_password_reset_async(user) do
270 BackgroundWorker.enqueue("force_password_reset", %{"user_id" => user.id})
273 @spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
274 def force_password_reset(user) do
275 info_cng = User.Info.set_password_reset_pending(user.info, true)
279 |> put_embed(:info, info_cng)
280 |> update_and_set_cache()
283 def register_changeset(struct, params \\ %{}, opts \\ []) do
284 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
285 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
288 if is_nil(opts[:need_confirmation]) do
289 Pleroma.Config.get([:instance, :account_activation_required])
291 opts[:need_confirmation]
295 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
296 |> validate_required([:name, :nickname, :password, :password_confirmation])
297 |> validate_confirmation(:password)
298 |> unique_constraint(:email)
299 |> unique_constraint(:nickname)
300 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
301 |> validate_format(:nickname, local_nickname_regex())
302 |> validate_format(:email, @email_regex)
303 |> validate_length(:bio, max: bio_limit)
304 |> validate_length(:name, min: 1, max: name_limit)
305 |> change_info(&User.Info.confirmation_changeset(&1, need_confirmation: need_confirmation?))
306 |> maybe_validate_required_email(opts[:external])
309 |> unique_constraint(:ap_id)
310 |> put_following_and_follower_address()
313 def maybe_validate_required_email(changeset, true), do: changeset
314 def maybe_validate_required_email(changeset, _), do: validate_required(changeset, [:email])
316 defp put_ap_id(changeset) do
317 ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
318 put_change(changeset, :ap_id, ap_id)
321 defp put_following_and_follower_address(changeset) do
322 followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
325 |> put_change(:following, [followers])
326 |> put_change(:follower_address, followers)
329 defp autofollow_users(user) do
330 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
333 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
336 follow_all(user, autofollowed_users)
339 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
340 def register(%Ecto.Changeset{} = changeset) do
341 with {:ok, user} <- Repo.insert(changeset) do
342 post_register_action(user)
346 def post_register_action(%User{} = user) do
347 with {:ok, user} <- autofollow_users(user),
348 {:ok, user} <- set_cache(user),
349 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
350 {:ok, _} <- try_send_confirmation_email(user) do
355 def try_send_confirmation_email(%User{} = user) do
356 if user.info.confirmation_pending &&
357 Pleroma.Config.get([:instance, :account_activation_required]) do
359 |> Pleroma.Emails.UserEmail.account_confirmation_email()
360 |> Pleroma.Emails.Mailer.deliver_async()
368 def needs_update?(%User{local: true}), do: false
370 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
372 def needs_update?(%User{local: false} = user) do
373 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
376 def needs_update?(_), do: true
378 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
379 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
383 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
384 follow(follower, followed)
387 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
388 if not ap_enabled?(followed) do
389 follow(follower, followed)
395 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
396 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
397 def follow_all(follower, followeds) do
400 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
401 |> Enum.map(fn %{follower_address: fa} -> fa end)
405 where: u.id == ^follower.id,
410 "array(select distinct unnest (array_cat(?, ?)))",
419 {1, [follower]} = Repo.update_all(q, [])
421 Enum.each(followeds, &update_follower_count/1)
426 def follow(%User{} = follower, %User{info: info} = followed) do
427 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
428 ap_followers = followed.follower_address
432 {:error, "Could not follow user: You are deactivated."}
434 deny_follow_blocked and blocks?(followed, follower) ->
435 {:error, "Could not follow user: #{followed.nickname} blocked you."}
438 if !followed.local && follower.local && !ap_enabled?(followed) do
439 Websub.subscribe(follower, followed)
444 where: u.id == ^follower.id,
445 update: [push: [following: ^ap_followers]],
449 {1, [follower]} = Repo.update_all(q, [])
451 follower = maybe_update_following_count(follower)
453 {:ok, _} = update_follower_count(followed)
459 def unfollow(%User{} = follower, %User{} = followed) do
460 ap_followers = followed.follower_address
462 if following?(follower, followed) and follower.ap_id != followed.ap_id do
465 where: u.id == ^follower.id,
466 update: [pull: [following: ^ap_followers]],
470 {1, [follower]} = Repo.update_all(q, [])
472 follower = maybe_update_following_count(follower)
474 {:ok, followed} = update_follower_count(followed)
478 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
480 {:error, "Not subscribed!"}
484 @spec following?(User.t(), User.t()) :: boolean
485 def following?(%User{} = follower, %User{} = followed) do
486 Enum.member?(follower.following, followed.follower_address)
489 def locked?(%User{} = user) do
490 user.info.locked || false
494 Repo.get_by(User, id: id)
497 def get_by_ap_id(ap_id) do
498 Repo.get_by(User, ap_id: ap_id)
501 def get_all_by_ap_id(ap_ids) do
502 from(u in __MODULE__,
503 where: u.ap_id in ^ap_ids
508 def get_all_by_ids(ids) do
509 from(u in __MODULE__, where: u.id in ^ids)
513 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
514 # of the ap_id and the domain and tries to get that user
515 def get_by_guessed_nickname(ap_id) do
516 domain = URI.parse(ap_id).host
517 name = List.last(String.split(ap_id, "/"))
518 nickname = "#{name}@#{domain}"
520 get_cached_by_nickname(nickname)
523 def set_cache({:ok, user}), do: set_cache(user)
524 def set_cache({:error, err}), do: {:error, err}
526 def set_cache(%User{} = user) do
527 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
528 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
529 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
533 def update_and_set_cache(changeset) do
534 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
539 def invalidate_cache(user) do
540 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
541 Cachex.del(:user_cache, "nickname:#{user.nickname}")
542 Cachex.del(:user_cache, "user_info:#{user.id}")
545 def get_cached_by_ap_id(ap_id) do
546 key = "ap_id:#{ap_id}"
547 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
550 def get_cached_by_id(id) do
554 Cachex.fetch!(:user_cache, key, fn _ ->
558 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
559 {:commit, user.ap_id}
565 get_cached_by_ap_id(ap_id)
568 def get_cached_by_nickname(nickname) do
569 key = "nickname:#{nickname}"
571 Cachex.fetch!(:user_cache, key, fn ->
572 case get_or_fetch_by_nickname(nickname) do
573 {:ok, user} -> {:commit, user}
574 {:error, _error} -> {:ignore, nil}
579 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
580 restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
583 is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
584 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
586 restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
587 get_cached_by_nickname(nickname_or_id)
589 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
590 get_cached_by_nickname(nickname_or_id)
597 def get_by_nickname(nickname) do
598 Repo.get_by(User, nickname: nickname) ||
599 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
600 Repo.get_by(User, nickname: local_nickname(nickname))
604 def get_by_email(email), do: Repo.get_by(User, email: email)
606 def get_by_nickname_or_email(nickname_or_email) do
607 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
610 def get_cached_user_info(user) do
611 key = "user_info:#{user.id}"
612 Cachex.fetch!(:user_cache, key, fn -> user_info(user) end)
615 def fetch_by_nickname(nickname) do
616 case ActivityPub.make_user_from_nickname(nickname) do
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) do
642 BackgroundWorker.enqueue("fetch_initial_posts", %{"user_id" => user.id})
645 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
646 def get_followers_query(%User{} = user, nil) do
647 User.Query.build(%{followers: user, deactivated: false})
650 def get_followers_query(user, page) do
652 |> get_followers_query(nil)
653 |> User.Query.paginate(page, 20)
656 @spec get_followers_query(User.t()) :: Ecto.Query.t()
657 def get_followers_query(user), do: get_followers_query(user, nil)
659 @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
660 def get_followers(user, page \\ nil) do
662 |> get_followers_query(page)
666 @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
667 def get_external_followers(user, page \\ nil) do
669 |> get_followers_query(page)
670 |> User.Query.build(%{external: true})
674 def get_followers_ids(user, page \\ nil) do
676 |> get_followers_query(page)
681 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
682 def get_friends_query(%User{} = user, nil) do
683 User.Query.build(%{friends: user, deactivated: false})
686 def get_friends_query(user, page) do
688 |> get_friends_query(nil)
689 |> User.Query.paginate(page, 20)
692 @spec get_friends_query(User.t()) :: Ecto.Query.t()
693 def get_friends_query(user), do: get_friends_query(user, nil)
695 def get_friends(user, page \\ nil) do
697 |> get_friends_query(page)
701 def get_friends_ids(user, page \\ nil) do
703 |> get_friends_query(page)
708 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
709 def get_follow_requests(%User{} = user) do
711 |> Activity.follow_requests_for_actor()
712 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
713 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
714 |> group_by([a, u], u.id)
719 def increase_note_count(%User{} = user) do
721 |> where(id: ^user.id)
726 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
733 |> Repo.update_all([])
735 {1, [user]} -> set_cache(user)
740 def decrease_note_count(%User{} = user) do
742 |> where(id: ^user.id)
747 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
754 |> Repo.update_all([])
756 {1, [user]} -> set_cache(user)
761 def update_note_count(%User{} = user) do
765 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
770 update_info(user, &User.Info.set_note_count(&1, note_count))
773 def update_mascot(user, url) do
775 User.Info.mascot_update(
782 |> put_embed(:info, info_changeset)
783 |> update_and_set_cache()
786 @spec maybe_fetch_follow_information(User.t()) :: User.t()
787 def maybe_fetch_follow_information(user) do
788 with {:ok, user} <- fetch_follow_information(user) do
792 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
798 def fetch_follow_information(user) do
799 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
800 update_info(user, &User.Info.follow_information_update(&1, info))
804 def update_follower_count(%User{} = user) do
805 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
806 follower_count_query =
807 User.Query.build(%{followers: user, deactivated: false})
808 |> select([u], %{count: count(u.id)})
811 |> where(id: ^user.id)
812 |> join(:inner, [u], s in subquery(follower_count_query))
817 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
824 |> Repo.update_all([])
826 {1, [user]} -> set_cache(user)
830 {:ok, maybe_fetch_follow_information(user)}
834 @spec maybe_update_following_count(User.t()) :: User.t()
835 def maybe_update_following_count(%User{local: false} = user) do
836 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
837 maybe_fetch_follow_information(user)
843 def maybe_update_following_count(user), do: user
845 def remove_duplicated_following(%User{following: following} = user) do
846 uniq_following = Enum.uniq(following)
848 if length(following) == length(uniq_following) do
852 |> update_changeset(%{following: uniq_following})
853 |> update_and_set_cache()
857 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
858 def get_users_from_set(ap_ids, local_only \\ true) do
859 criteria = %{ap_id: ap_ids, deactivated: false}
860 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
862 User.Query.build(criteria)
866 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
867 def get_recipients_from_activity(%Activity{recipients: to}) do
868 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
872 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
873 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
874 update_info(muter, &User.Info.add_to_mutes(&1, ap_id, notifications?))
877 def unmute(muter, %{ap_id: ap_id}) do
878 update_info(muter, &User.Info.remove_from_mutes(&1, ap_id))
881 def subscribe(subscriber, %{ap_id: ap_id}) do
882 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
883 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
885 if blocks?(subscribed, subscriber) and deny_follow_blocked do
886 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
888 update_info(subscribed, &User.Info.add_to_subscribers(&1, subscriber.ap_id))
893 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
894 with %User{} = user <- get_cached_by_ap_id(ap_id) do
895 update_info(user, &User.Info.remove_from_subscribers(&1, unsubscriber.ap_id))
899 def block(blocker, %User{ap_id: ap_id} = blocked) do
900 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
902 if following?(blocker, blocked) do
903 {:ok, blocker, _} = unfollow(blocker, blocked)
909 # clear any requested follows as well
911 case CommonAPI.reject_follow_request(blocked, blocker) do
912 {:ok, %User{} = updated_blocked} -> updated_blocked
917 if subscribed_to?(blocked, blocker) do
918 {:ok, blocker} = unsubscribe(blocked, blocker)
924 if following?(blocked, blocker), do: unfollow(blocked, blocker)
926 {:ok, blocker} = update_follower_count(blocker)
928 update_info(blocker, &User.Info.add_to_block(&1, ap_id))
931 # helper to handle the block given only an actor's AP id
932 def block(blocker, %{ap_id: ap_id}) do
933 block(blocker, get_cached_by_ap_id(ap_id))
936 def unblock(blocker, %{ap_id: ap_id}) do
937 update_info(blocker, &User.Info.remove_from_block(&1, ap_id))
940 def mutes?(nil, _), do: false
941 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
943 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
944 def muted_notifications?(nil, _), do: false
946 def muted_notifications?(user, %{ap_id: ap_id}),
947 do: Enum.member?(user.info.muted_notifications, ap_id)
949 def blocks?(%User{} = user, %User{} = target) do
950 blocks_ap_id?(user, target) || blocks_domain?(user, target)
953 def blocks?(nil, _), do: false
955 def blocks_ap_id?(%User{} = user, %User{} = target) do
956 Enum.member?(user.info.blocks, target.ap_id)
959 def blocks_ap_id?(_, _), do: false
961 def blocks_domain?(%User{} = user, %User{} = target) do
962 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
963 %{host: host} = URI.parse(target.ap_id)
964 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
967 def blocks_domain?(_, _), do: false
969 def subscribed_to?(user, %{ap_id: ap_id}) do
970 with %User{} = target <- get_cached_by_ap_id(ap_id) do
971 Enum.member?(target.info.subscribers, user.ap_id)
975 @spec muted_users(User.t()) :: [User.t()]
976 def muted_users(user) do
977 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
981 @spec blocked_users(User.t()) :: [User.t()]
982 def blocked_users(user) do
983 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
987 @spec subscribers(User.t()) :: [User.t()]
988 def subscribers(user) do
989 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
993 def block_domain(user, domain) do
994 update_info(user, &User.Info.add_to_domain_block(&1, domain))
997 def unblock_domain(user, domain) do
998 update_info(user, &User.Info.remove_from_domain_block(&1, domain))
1001 def deactivate_async(user, status \\ true) do
1002 BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
1005 def deactivate(%User{} = user, status \\ true) do
1006 with {:ok, user} <- update_info(user, &User.Info.set_activation_status(&1, status)) do
1007 Enum.each(get_followers(user), &invalidate_cache/1)
1008 Enum.each(get_friends(user), &update_follower_count/1)
1014 def update_notification_settings(%User{} = user, settings \\ %{}) do
1015 update_info(user, &User.Info.update_notification_settings(&1, settings))
1018 def delete(%User{} = user) do
1019 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1022 def perform(:force_password_reset, user), do: force_password_reset(user)
1024 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1025 def perform(:delete, %User{} = user) do
1026 {:ok, _user} = ActivityPub.delete(user)
1028 # Remove all relationships
1031 |> Enum.each(fn follower ->
1032 ActivityPub.unfollow(follower, user)
1033 unfollow(follower, user)
1038 |> Enum.each(fn followed ->
1039 ActivityPub.unfollow(user, followed)
1040 unfollow(user, followed)
1043 delete_user_activities(user)
1044 invalidate_cache(user)
1048 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1049 def perform(:fetch_initial_posts, %User{} = user) do
1050 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1052 # Insert all the posts in reverse order, so they're in the right order on the timeline
1053 user.info.source_data["outbox"]
1054 |> Utils.fetch_ordered_collection(pages)
1056 |> Enum.each(&Pleroma.Web.Federator.incoming_ap_doc/1)
1059 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1061 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1062 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1063 when is_list(blocked_identifiers) do
1065 blocked_identifiers,
1066 fn blocked_identifier ->
1067 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1068 {:ok, blocker} <- block(blocker, blocked),
1069 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1073 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1080 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1081 def perform(:follow_import, %User{} = follower, followed_identifiers)
1082 when is_list(followed_identifiers) do
1084 followed_identifiers,
1085 fn followed_identifier ->
1086 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1087 {:ok, follower} <- maybe_direct_follow(follower, followed),
1088 {:ok, _} <- ActivityPub.follow(follower, followed) do
1092 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1099 @spec external_users_query() :: Ecto.Query.t()
1100 def external_users_query do
1108 @spec external_users(keyword()) :: [User.t()]
1109 def external_users(opts \\ []) do
1111 external_users_query()
1112 |> select([u], struct(u, [:id, :ap_id, :info]))
1116 do: where(query, [u], u.id > ^opts[:max_id]),
1121 do: limit(query, ^opts[:limit]),
1127 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1128 BackgroundWorker.enqueue("blocks_import", %{
1129 "blocker_id" => blocker.id,
1130 "blocked_identifiers" => blocked_identifiers
1134 def follow_import(%User{} = follower, followed_identifiers)
1135 when is_list(followed_identifiers) do
1136 BackgroundWorker.enqueue("follow_import", %{
1137 "follower_id" => follower.id,
1138 "followed_identifiers" => followed_identifiers
1142 def delete_user_activities(%User{ap_id: ap_id}) do
1144 |> Activity.Queries.by_actor()
1145 |> RepoStreamer.chunk_stream(50)
1146 |> Stream.each(fn activities -> Enum.each(activities, &delete_activity/1) end)
1150 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1152 |> Object.normalize()
1153 |> ActivityPub.delete()
1156 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1157 object = Object.normalize(activity)
1160 |> get_cached_by_ap_id()
1161 |> ActivityPub.unlike(object)
1164 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1165 object = Object.normalize(activity)
1168 |> get_cached_by_ap_id()
1169 |> ActivityPub.unannounce(object)
1172 defp delete_activity(_activity), do: "Doing nothing"
1174 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1175 Pleroma.HTML.Scrubber.TwitterText
1178 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1180 def fetch_by_ap_id(ap_id) do
1181 case ActivityPub.make_user_from_ap_id(ap_id) do
1186 case OStatus.make_user(ap_id) do
1187 {:ok, user} -> {:ok, user}
1188 _ -> {:error, "Could not fetch by AP id"}
1193 def get_or_fetch_by_ap_id(ap_id) do
1194 user = get_cached_by_ap_id(ap_id)
1196 if !is_nil(user) and !needs_update?(user) do
1199 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1200 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1202 resp = fetch_by_ap_id(ap_id)
1204 if should_fetch_initial do
1205 with {:ok, %User{} = user} <- resp do
1206 fetch_initial_posts(user)
1214 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1215 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1216 with %User{} = user <- get_cached_by_ap_id(uri) do
1221 %User{info: %User.Info{}}
1222 |> cast(%{}, [:ap_id, :nickname, :local])
1223 |> put_change(:ap_id, uri)
1224 |> put_change(:nickname, nickname)
1225 |> put_change(:local, true)
1226 |> put_change(:follower_address, uri <> "/followers")
1234 def public_key_from_info(%{
1235 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1239 |> :public_key.pem_decode()
1241 |> :public_key.pem_entry_decode()
1247 def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
1248 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1251 def public_key_from_info(_), do: {:error, "not found key"}
1253 def get_public_key_for_ap_id(ap_id) do
1254 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1255 {:ok, public_key} <- public_key_from_info(user.info) do
1262 defp blank?(""), do: nil
1263 defp blank?(n), do: n
1265 def insert_or_update_user(data) do
1267 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1268 |> remote_user_creation()
1269 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1273 def ap_enabled?(%User{local: true}), do: true
1274 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1275 def ap_enabled?(_), do: false
1277 @doc "Gets or fetch a user by uri or nickname."
1278 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1279 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1280 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1282 # wait a period of time and return newest version of the User structs
1283 # this is because we have synchronous follow APIs and need to simulate them
1284 # with an async handshake
1285 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1286 with %User{} = a <- get_cached_by_id(a.id),
1287 %User{} = b <- get_cached_by_id(b.id) do
1294 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1295 with :ok <- :timer.sleep(timeout),
1296 %User{} = a <- get_cached_by_id(a.id),
1297 %User{} = b <- get_cached_by_id(b.id) do
1304 def parse_bio(bio) when is_binary(bio) and bio != "" do
1306 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1310 def parse_bio(_), do: ""
1312 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1313 # TODO: get profile URLs other than user.ap_id
1314 profile_urls = [user.ap_id]
1317 |> CommonUtils.format_input("text/plain",
1318 mentions_format: :full,
1319 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1324 def parse_bio(_, _), do: ""
1326 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1327 Repo.transaction(fn ->
1328 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1332 def tag(nickname, tags) when is_binary(nickname),
1333 do: tag(get_by_nickname(nickname), tags)
1335 def tag(%User{} = user, tags),
1336 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1338 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1339 Repo.transaction(fn ->
1340 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1344 def untag(nickname, tags) when is_binary(nickname),
1345 do: untag(get_by_nickname(nickname), tags)
1347 def untag(%User{} = user, tags),
1348 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1350 defp update_tags(%User{} = user, new_tags) do
1351 {:ok, updated_user} =
1353 |> change(%{tags: new_tags})
1354 |> update_and_set_cache()
1359 defp normalize_tags(tags) do
1362 |> Enum.map(&String.downcase/1)
1365 defp local_nickname_regex do
1366 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1367 @extended_local_nickname_regex
1369 @strict_local_nickname_regex
1373 def local_nickname(nickname_or_mention) do
1376 |> String.split("@")
1380 def full_nickname(nickname_or_mention),
1381 do: String.trim_leading(nickname_or_mention, "@")
1383 def error_user(ap_id) do
1388 nickname: "erroruser@example.com",
1389 inserted_at: NaiveDateTime.utc_now()
1393 @spec all_superusers() :: [User.t()]
1394 def all_superusers do
1395 User.Query.build(%{super_users: true, local: true, deactivated: false})
1399 def showing_reblogs?(%User{} = user, %User{} = target) do
1400 target.ap_id not in user.info.muted_reblogs
1404 The function returns a query to get users with no activity for given interval of days.
1405 Inactive users are those who didn't read any notification, or had any activity where
1406 the user is the activity's actor, during `inactivity_threshold` days.
1407 Deactivated users will not appear in this list.
1411 iex> Pleroma.User.list_inactive_users()
1414 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1415 def list_inactive_users_query(inactivity_threshold \\ 7) do
1416 negative_inactivity_threshold = -inactivity_threshold
1417 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1418 # Subqueries are not supported in `where` clauses, join gets too complicated.
1419 has_read_notifications =
1420 from(n in Pleroma.Notification,
1421 where: n.seen == true,
1423 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1426 |> Pleroma.Repo.all()
1428 from(u in Pleroma.User,
1429 left_join: a in Pleroma.Activity,
1430 on: u.ap_id == a.actor,
1431 where: not is_nil(u.nickname),
1432 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1433 where: u.id not in ^has_read_notifications,
1436 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1437 is_nil(max(a.inserted_at))
1442 Enable or disable email notifications for user
1446 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1447 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1449 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1450 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1452 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1453 {:ok, t()} | {:error, Ecto.Changeset.t()}
1454 def switch_email_notifications(user, type, status) do
1455 update_info(user, &User.Info.update_email_notifications(&1, %{type => status}))
1459 Set `last_digest_emailed_at` value for the user to current time
1461 @spec touch_last_digest_emailed_at(t()) :: t()
1462 def touch_last_digest_emailed_at(user) do
1463 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1465 {:ok, updated_user} =
1467 |> change(%{last_digest_emailed_at: now})
1468 |> update_and_set_cache()
1473 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1474 def toggle_confirmation(%User{} = user) do
1475 need_confirmation? = !user.info.confirmation_pending
1478 |> update_info(&User.Info.confirmation_changeset(&1, need_confirmation: need_confirmation?))
1481 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1485 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1486 # use instance-default
1487 config = Pleroma.Config.get([:assets, :mascots])
1488 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1489 mascot = Keyword.get(config, default_mascot)
1492 "id" => "default-mascot",
1493 "url" => mascot[:url],
1494 "preview_url" => mascot[:url],
1496 "mime_type" => mascot[:mime_type]
1501 def ensure_keys_present(%{info: %{keys: keys}} = user) when not is_nil(keys), do: {:ok, user}
1503 def ensure_keys_present(%User{} = user) do
1504 with {:ok, pem} <- Keys.generate_rsa_pem() do
1505 update_info(user, &User.Info.set_keys(&1, pem))
1509 def get_ap_ids_by_nicknames(nicknames) do
1511 where: u.nickname in ^nicknames,
1517 defdelegate search(query, opts \\ []), to: User.Search
1519 defp put_password_hash(
1520 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1522 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1525 defp put_password_hash(changeset), do: changeset
1527 def is_internal_user?(%User{nickname: nil}), do: true
1528 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1529 def is_internal_user?(_), do: false
1531 # A hack because user delete activities have a fake id for whatever reason
1532 # TODO: Get rid of this
1533 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
1535 def get_delivered_users_by_object_id(object_id) do
1537 inner_join: delivery in assoc(u, :deliveries),
1538 where: delivery.object_id == ^object_id
1543 def change_email(user, email) do
1545 |> cast(%{email: email}, [:email])
1546 |> validate_required([:email])
1547 |> unique_constraint(:email)
1548 |> validate_format(:email, @email_regex)
1549 |> update_and_set_cache()
1553 Changes `user.info` and returns the user changeset.
1555 `fun` is called with the `user.info`.
1557 def change_info(user, fun) do
1558 changeset = change(user)
1559 info = get_field(changeset, :info) || %User.Info{}
1560 put_embed(changeset, :info, fun.(info))
1564 Updates `user.info` and sets cache.
1566 `fun` is called with the `user.info`.
1568 def update_info(user, fun) do
1571 |> update_and_set_cache()