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)
202 |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
203 |> validate_required([:name, :ap_id])
204 |> unique_constraint(:nickname)
205 |> validate_format(:nickname, @email_regex)
206 |> validate_length(:bio, max: bio_limit)
207 |> validate_length(:name, max: name_limit)
208 |> put_change(:local, false)
209 |> change_info(&User.Info.remote_user_creation(&1, params[:info]))
212 case info_cng.changes[:source_data] do
213 %{"followers" => followers, "following" => following} ->
215 |> put_change(:follower_address, followers)
216 |> put_change(:following_address, following)
219 followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
222 |> put_change(:follower_address, followers)
229 def update_changeset(struct, params \\ %{}) do
230 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
231 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
234 |> cast(params, [:bio, :name, :avatar, :following])
235 |> unique_constraint(:nickname)
236 |> validate_format(:nickname, local_nickname_regex())
237 |> validate_length(:bio, max: bio_limit)
238 |> validate_length(:name, min: 1, max: name_limit)
241 def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
242 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
243 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
245 params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
256 |> unique_constraint(:nickname)
257 |> validate_format(:nickname, local_nickname_regex())
258 |> validate_length(:bio, max: bio_limit)
259 |> validate_length(:name, max: name_limit)
260 |> change_info(&User.Info.user_upgrade(&1, params[:info], remote?))
263 def password_update_changeset(struct, params) do
265 |> cast(params, [:password, :password_confirmation])
266 |> validate_required([:password, :password_confirmation])
267 |> validate_confirmation(:password)
269 |> put_embed(:info, User.Info.set_password_reset_pending(struct.info, false))
272 @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
273 def reset_password(%User{id: user_id} = user, data) do
276 |> Multi.update(:user, password_update_changeset(user, data))
277 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
278 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
280 case Repo.transaction(multi) do
281 {:ok, %{user: user} = _} -> set_cache(user)
282 {:error, _, changeset, _} -> {:error, changeset}
286 def force_password_reset_async(user) do
287 BackgroundWorker.enqueue("force_password_reset", %{"user_id" => user.id})
290 @spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
291 def force_password_reset(user) do
292 info_cng = User.Info.set_password_reset_pending(user.info, true)
296 |> put_embed(:info, info_cng)
297 |> update_and_set_cache()
300 def register_changeset(struct, params \\ %{}, opts \\ []) do
301 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
302 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
305 if is_nil(opts[:need_confirmation]) do
306 Pleroma.Config.get([:instance, :account_activation_required])
308 opts[:need_confirmation]
312 User.Info.confirmation_changeset(%User.Info{}, need_confirmation: need_confirmation?)
316 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
317 |> validate_required([:name, :nickname, :password, :password_confirmation])
318 |> validate_confirmation(:password)
319 |> unique_constraint(:email)
320 |> unique_constraint(:nickname)
321 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
322 |> validate_format(:nickname, local_nickname_regex())
323 |> validate_format(:email, @email_regex)
324 |> validate_length(:bio, max: bio_limit)
325 |> validate_length(:name, min: 1, max: name_limit)
326 |> put_change(:info, info_change)
329 if opts[:external] do
332 validate_required(changeset, [:email])
335 if changeset.valid? do
336 ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
337 followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
341 |> put_change(:ap_id, ap_id)
342 |> unique_constraint(:ap_id)
343 |> put_change(:following, [followers])
344 |> put_change(:follower_address, followers)
350 defp autofollow_users(user) do
351 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
354 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
357 follow_all(user, autofollowed_users)
360 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
361 def register(%Ecto.Changeset{} = changeset) do
362 with {:ok, user} <- Repo.insert(changeset),
363 {:ok, user} <- post_register_action(user) do
368 def post_register_action(%User{} = user) do
369 with {:ok, user} <- autofollow_users(user),
370 {:ok, user} <- set_cache(user),
371 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
372 {:ok, _} <- try_send_confirmation_email(user) do
377 def try_send_confirmation_email(%User{} = user) do
378 if user.info.confirmation_pending &&
379 Pleroma.Config.get([:instance, :account_activation_required]) do
381 |> Pleroma.Emails.UserEmail.account_confirmation_email()
382 |> Pleroma.Emails.Mailer.deliver_async()
390 def needs_update?(%User{local: true}), do: false
392 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
394 def needs_update?(%User{local: false} = user) do
395 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
398 def needs_update?(_), do: true
400 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
401 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
405 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
406 follow(follower, followed)
409 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
410 if not User.ap_enabled?(followed) do
411 follow(follower, followed)
417 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
418 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
419 def follow_all(follower, followeds) do
422 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
423 |> Enum.map(fn %{follower_address: fa} -> fa end)
427 where: u.id == ^follower.id,
432 "array(select distinct unnest (array_cat(?, ?)))",
441 {1, [follower]} = Repo.update_all(q, [])
443 Enum.each(followeds, fn followed ->
444 update_follower_count(followed)
450 def follow(%User{} = follower, %User{info: info} = followed) do
451 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
452 ap_followers = followed.follower_address
456 {:error, "Could not follow user: You are deactivated."}
458 deny_follow_blocked and blocks?(followed, follower) ->
459 {:error, "Could not follow user: #{followed.nickname} blocked you."}
462 if !followed.local && follower.local && !ap_enabled?(followed) do
463 Websub.subscribe(follower, followed)
468 where: u.id == ^follower.id,
469 update: [push: [following: ^ap_followers]],
473 {1, [follower]} = Repo.update_all(q, [])
475 follower = maybe_update_following_count(follower)
477 {:ok, _} = update_follower_count(followed)
483 def unfollow(%User{} = follower, %User{} = followed) do
484 ap_followers = followed.follower_address
486 if following?(follower, followed) and follower.ap_id != followed.ap_id do
489 where: u.id == ^follower.id,
490 update: [pull: [following: ^ap_followers]],
494 {1, [follower]} = Repo.update_all(q, [])
496 follower = maybe_update_following_count(follower)
498 {:ok, followed} = update_follower_count(followed)
502 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
504 {:error, "Not subscribed!"}
508 @spec following?(User.t(), User.t()) :: boolean
509 def following?(%User{} = follower, %User{} = followed) do
510 Enum.member?(follower.following, followed.follower_address)
513 def locked?(%User{} = user) do
514 user.info.locked || false
518 Repo.get_by(User, id: id)
521 def get_by_ap_id(ap_id) do
522 Repo.get_by(User, ap_id: ap_id)
525 def get_all_by_ap_id(ap_ids) do
526 from(u in __MODULE__,
527 where: u.ap_id in ^ap_ids
532 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
533 # of the ap_id and the domain and tries to get that user
534 def get_by_guessed_nickname(ap_id) do
535 domain = URI.parse(ap_id).host
536 name = List.last(String.split(ap_id, "/"))
537 nickname = "#{name}@#{domain}"
539 get_cached_by_nickname(nickname)
542 def set_cache({:ok, user}), do: set_cache(user)
543 def set_cache({:error, err}), do: {:error, err}
545 def set_cache(%User{} = user) do
546 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
547 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
548 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
552 def update_and_set_cache(changeset) do
553 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
560 def invalidate_cache(user) do
561 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
562 Cachex.del(:user_cache, "nickname:#{user.nickname}")
563 Cachex.del(:user_cache, "user_info:#{user.id}")
566 def get_cached_by_ap_id(ap_id) do
567 key = "ap_id:#{ap_id}"
568 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
571 def get_cached_by_id(id) do
575 Cachex.fetch!(:user_cache, key, fn _ ->
579 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
580 {:commit, user.ap_id}
586 get_cached_by_ap_id(ap_id)
589 def get_cached_by_nickname(nickname) do
590 key = "nickname:#{nickname}"
592 Cachex.fetch!(:user_cache, key, fn ->
593 user_result = get_or_fetch_by_nickname(nickname)
596 {:ok, user} -> {:commit, user}
597 {:error, _error} -> {:ignore, nil}
602 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
603 restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
606 is_integer(nickname_or_id) or Pleroma.FlakeId.is_flake_id?(nickname_or_id) ->
607 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
609 restrict_to_local == false ->
610 get_cached_by_nickname(nickname_or_id)
612 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
613 get_cached_by_nickname(nickname_or_id)
620 def get_by_nickname(nickname) do
621 Repo.get_by(User, nickname: nickname) ||
622 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
623 Repo.get_by(User, nickname: local_nickname(nickname))
627 def get_by_email(email), do: Repo.get_by(User, email: email)
629 def get_by_nickname_or_email(nickname_or_email) do
630 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
633 def get_cached_user_info(user) do
634 key = "user_info:#{user.id}"
635 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
638 def fetch_by_nickname(nickname) do
639 ap_try = ActivityPub.make_user_from_nickname(nickname)
642 {:ok, user} -> {:ok, user}
643 _ -> OStatus.make_user(nickname)
647 def get_or_fetch_by_nickname(nickname) do
648 with %User{} = user <- get_by_nickname(nickname) do
652 with [_nick, _domain] <- String.split(nickname, "@"),
653 {:ok, user} <- fetch_by_nickname(nickname) do
654 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
655 fetch_initial_posts(user)
660 _e -> {:error, "not found " <> nickname}
665 @doc "Fetch some posts when the user has just been federated with"
666 def fetch_initial_posts(user) do
667 BackgroundWorker.enqueue("fetch_initial_posts", %{"user_id" => user.id})
670 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
671 def get_followers_query(%User{} = user, nil) do
672 User.Query.build(%{followers: user, deactivated: false})
675 def get_followers_query(user, page) do
676 from(u in get_followers_query(user, nil))
677 |> User.Query.paginate(page, 20)
680 @spec get_followers_query(User.t()) :: Ecto.Query.t()
681 def get_followers_query(user), do: get_followers_query(user, nil)
683 @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
684 def get_followers(user, page \\ nil) do
686 |> get_followers_query(page)
690 @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
691 def get_external_followers(user, page \\ nil) do
694 |> get_followers_query(page)
695 |> User.Query.build(%{external: true})
700 def get_followers_ids(user, page \\ nil) do
701 q = get_followers_query(user, page)
703 Repo.all(from(u in q, select: u.id))
706 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
707 def get_friends_query(%User{} = user, nil) do
708 User.Query.build(%{friends: user, deactivated: false})
711 def get_friends_query(user, page) do
712 from(u in get_friends_query(user, nil))
713 |> User.Query.paginate(page, 20)
716 @spec get_friends_query(User.t()) :: Ecto.Query.t()
717 def get_friends_query(user), do: get_friends_query(user, nil)
719 def get_friends(user, page \\ nil) do
721 |> get_friends_query(page)
725 def get_friends_ids(user, page \\ nil) do
726 q = get_friends_query(user, page)
728 Repo.all(from(u in q, select: u.id))
731 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
732 def get_follow_requests(%User{} = user) do
734 |> Activity.follow_requests_for_actor()
735 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
736 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
737 |> group_by([a, u], u.id)
742 def increase_note_count(%User{} = user) do
744 |> where(id: ^user.id)
749 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
756 |> Repo.update_all([])
758 {1, [user]} -> set_cache(user)
763 def decrease_note_count(%User{} = user) do
765 |> where(id: ^user.id)
770 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
777 |> Repo.update_all([])
779 {1, [user]} -> set_cache(user)
784 def update_note_count(%User{} = user) do
788 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
793 update_info(user, &User.Info.set_note_count(&1, note_count))
796 @spec maybe_fetch_follow_information(User.t()) :: User.t()
797 def maybe_fetch_follow_information(user) do
798 with {:ok, user} <- fetch_follow_information(user) do
802 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
808 def fetch_follow_information(user) do
809 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
810 update_info(user, &User.Info.follow_information_update(&1, info))
814 def update_follower_count(%User{} = user) do
815 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
816 follower_count_query =
817 User.Query.build(%{followers: user, deactivated: false})
818 |> select([u], %{count: count(u.id)})
821 |> where(id: ^user.id)
822 |> join(:inner, [u], s in subquery(follower_count_query))
827 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
834 |> Repo.update_all([])
836 {1, [user]} -> set_cache(user)
840 {:ok, maybe_fetch_follow_information(user)}
844 @spec maybe_update_following_count(User.t()) :: User.t()
845 def maybe_update_following_count(%User{local: false} = user) do
846 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
847 maybe_fetch_follow_information(user)
853 def maybe_update_following_count(user), do: user
855 def remove_duplicated_following(%User{following: following} = user) do
856 uniq_following = Enum.uniq(following)
858 if length(following) == length(uniq_following) do
862 |> update_changeset(%{following: uniq_following})
863 |> update_and_set_cache()
867 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
868 def get_users_from_set(ap_ids, local_only \\ true) do
869 criteria = %{ap_id: ap_ids, deactivated: false}
870 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
872 User.Query.build(criteria)
876 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
877 def get_recipients_from_activity(%Activity{recipients: to}) do
878 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
882 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
883 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
884 update_info(muter, &User.Info.add_to_mutes(&1, ap_id, notifications?))
887 def unmute(muter, %{ap_id: ap_id}) do
888 update_info(muter, &User.Info.remove_from_mutes(&1, ap_id))
891 def subscribe(subscriber, %{ap_id: ap_id}) do
892 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
894 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
895 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
898 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
900 update_info(subscribed, &User.Info.add_to_subscribers(&1, subscriber.ap_id))
905 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
906 with %User{} = user <- get_cached_by_ap_id(ap_id) do
907 update_info(user, &User.Info.remove_from_subscribers(&1, unsubscriber.ap_id))
911 def block(blocker, %User{ap_id: ap_id} = blocked) do
912 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
914 if following?(blocker, blocked) do
915 {:ok, blocker, _} = unfollow(blocker, blocked)
921 # clear any requested follows as well
923 case CommonAPI.reject_follow_request(blocked, blocker) do
924 {:ok, %User{} = updated_blocked} -> updated_blocked
929 if subscribed_to?(blocked, blocker) do
930 {:ok, blocker} = unsubscribe(blocked, blocker)
936 if following?(blocked, blocker) do
937 unfollow(blocked, blocker)
940 {:ok, blocker} = update_follower_count(blocker)
942 update_info(blocker, &User.Info.add_to_block(&1, ap_id))
945 # helper to handle the block given only an actor's AP id
946 def block(blocker, %{ap_id: ap_id}) do
947 block(blocker, get_cached_by_ap_id(ap_id))
950 def unblock(blocker, %{ap_id: ap_id}) do
951 update_info(blocker, &User.Info.remove_from_block(&1, ap_id))
954 def mutes?(nil, _), do: false
955 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
957 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
958 def muted_notifications?(nil, _), do: false
960 def muted_notifications?(user, %{ap_id: ap_id}),
961 do: Enum.member?(user.info.muted_notifications, ap_id)
963 def blocks?(%User{} = user, %User{} = target) do
964 blocks_ap_id?(user, target) || blocks_domain?(user, target)
967 def blocks?(nil, _), do: false
969 def blocks_ap_id?(%User{} = user, %User{} = target) do
970 Enum.member?(user.info.blocks, target.ap_id)
973 def blocks_ap_id?(_, _), do: false
975 def blocks_domain?(%User{} = user, %User{} = target) do
976 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
977 %{host: host} = URI.parse(target.ap_id)
978 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
981 def blocks_domain?(_, _), do: false
983 def subscribed_to?(user, %{ap_id: ap_id}) do
984 with %User{} = target <- get_cached_by_ap_id(ap_id) do
985 Enum.member?(target.info.subscribers, user.ap_id)
989 @spec muted_users(User.t()) :: [User.t()]
990 def muted_users(user) do
991 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
995 @spec blocked_users(User.t()) :: [User.t()]
996 def blocked_users(user) do
997 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
1001 @spec subscribers(User.t()) :: [User.t()]
1002 def subscribers(user) do
1003 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
1007 def block_domain(user, domain) do
1008 update_info(user, &User.Info.add_to_domain_block(&1, domain))
1011 def unblock_domain(user, domain) do
1012 update_info(user, &User.Info.remove_from_domain_block(&1, domain))
1015 def deactivate_async(user, status \\ true) do
1016 BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
1019 def deactivate(%User{} = user, status \\ true) do
1020 with {:ok, user} <- update_info(user, &User.Info.set_activation_status(&1, status)) do
1021 Enum.each(get_followers(user), &invalidate_cache/1)
1022 Enum.each(get_friends(user), &update_follower_count/1)
1028 def update_notification_settings(%User{} = user, settings \\ %{}) do
1029 update_info(user, &User.Info.update_notification_settings(&1, settings))
1032 def delete(%User{} = user) do
1033 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1036 def perform(:force_password_reset, user), do: force_password_reset(user)
1038 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1039 def perform(:delete, %User{} = user) do
1040 {:ok, _user} = ActivityPub.delete(user)
1042 # Remove all relationships
1045 |> Enum.each(fn follower ->
1046 ActivityPub.unfollow(follower, user)
1047 unfollow(follower, user)
1052 |> Enum.each(fn followed ->
1053 ActivityPub.unfollow(user, followed)
1054 unfollow(user, followed)
1057 delete_user_activities(user)
1058 invalidate_cache(user)
1062 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1063 def perform(:fetch_initial_posts, %User{} = user) do
1064 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1066 # Insert all the posts in reverse order, so they're in the right order on the timeline
1067 user.info.source_data["outbox"]
1068 |> Utils.fetch_ordered_collection(pages)
1070 |> Enum.each(&Pleroma.Web.Federator.incoming_ap_doc/1)
1073 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1075 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1076 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1077 when is_list(blocked_identifiers) do
1079 blocked_identifiers,
1080 fn blocked_identifier ->
1081 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1082 {:ok, blocker} <- block(blocker, blocked),
1083 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1087 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1094 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1095 def perform(:follow_import, %User{} = follower, followed_identifiers)
1096 when is_list(followed_identifiers) do
1098 followed_identifiers,
1099 fn followed_identifier ->
1100 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1101 {:ok, follower} <- maybe_direct_follow(follower, followed),
1102 {:ok, _} <- ActivityPub.follow(follower, followed) do
1106 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1113 @spec external_users_query() :: Ecto.Query.t()
1114 def external_users_query do
1122 @spec external_users(keyword()) :: [User.t()]
1123 def external_users(opts \\ []) do
1125 external_users_query()
1126 |> select([u], struct(u, [:id, :ap_id, :info]))
1130 do: where(query, [u], u.id > ^opts[:max_id]),
1135 do: limit(query, ^opts[:limit]),
1141 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1142 BackgroundWorker.enqueue("blocks_import", %{
1143 "blocker_id" => blocker.id,
1144 "blocked_identifiers" => blocked_identifiers
1148 def follow_import(%User{} = follower, followed_identifiers)
1149 when is_list(followed_identifiers) do
1150 BackgroundWorker.enqueue("follow_import", %{
1151 "follower_id" => follower.id,
1152 "followed_identifiers" => followed_identifiers
1156 def delete_user_activities(%User{ap_id: ap_id}) do
1158 |> Activity.Queries.by_actor()
1159 |> RepoStreamer.chunk_stream(50)
1160 |> Stream.each(fn activities -> Enum.each(activities, &delete_activity/1) end)
1164 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1166 |> Object.normalize()
1167 |> ActivityPub.delete()
1170 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1171 user = get_cached_by_ap_id(activity.actor)
1172 object = Object.normalize(activity)
1174 ActivityPub.unlike(user, object)
1177 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1178 user = get_cached_by_ap_id(activity.actor)
1179 object = Object.normalize(activity)
1181 ActivityPub.unannounce(user, object)
1184 defp delete_activity(_activity), do: "Doing nothing"
1186 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1187 Pleroma.HTML.Scrubber.TwitterText
1190 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1192 def fetch_by_ap_id(ap_id) do
1193 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1200 case OStatus.make_user(ap_id) do
1201 {:ok, user} -> {:ok, user}
1202 _ -> {:error, "Could not fetch by AP id"}
1207 def get_or_fetch_by_ap_id(ap_id) do
1208 user = get_cached_by_ap_id(ap_id)
1210 if !is_nil(user) and !User.needs_update?(user) do
1213 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1214 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1216 resp = fetch_by_ap_id(ap_id)
1218 if should_fetch_initial do
1219 with {:ok, %User{} = user} <- resp do
1220 fetch_initial_posts(user)
1228 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1229 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1230 if user = get_cached_by_ap_id(uri) do
1234 %User{info: %User.Info{}}
1235 |> cast(%{}, [:ap_id, :nickname, :local])
1236 |> put_change(:ap_id, uri)
1237 |> put_change(:nickname, nickname)
1238 |> put_change(:local, true)
1239 |> put_change(:follower_address, uri <> "/followers")
1241 {:ok, user} = Repo.insert(changes)
1247 def public_key_from_info(%{
1248 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1252 |> :public_key.pem_decode()
1254 |> :public_key.pem_entry_decode()
1260 def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
1261 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1264 def public_key_from_info(_), do: {:error, "not found key"}
1266 def get_public_key_for_ap_id(ap_id) do
1267 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1268 {:ok, public_key} <- public_key_from_info(user.info) do
1275 defp blank?(""), do: nil
1276 defp blank?(n), do: n
1278 def insert_or_update_user(data) do
1280 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1281 |> remote_user_creation()
1282 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1286 def ap_enabled?(%User{local: true}), do: true
1287 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1288 def ap_enabled?(_), do: false
1290 @doc "Gets or fetch a user by uri or nickname."
1291 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1292 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1293 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1295 # wait a period of time and return newest version of the User structs
1296 # this is because we have synchronous follow APIs and need to simulate them
1297 # with an async handshake
1298 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1299 with %User{} = a <- User.get_cached_by_id(a.id),
1300 %User{} = b <- User.get_cached_by_id(b.id) do
1308 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1309 with :ok <- :timer.sleep(timeout),
1310 %User{} = a <- User.get_cached_by_id(a.id),
1311 %User{} = b <- User.get_cached_by_id(b.id) do
1319 def parse_bio(bio) when is_binary(bio) and bio != "" do
1321 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1325 def parse_bio(_), do: ""
1327 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1328 # TODO: get profile URLs other than user.ap_id
1329 profile_urls = [user.ap_id]
1332 |> CommonUtils.format_input("text/plain",
1333 mentions_format: :full,
1334 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1339 def parse_bio(_, _), do: ""
1341 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1342 Repo.transaction(fn ->
1343 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1347 def tag(nickname, tags) when is_binary(nickname),
1348 do: tag(get_by_nickname(nickname), tags)
1350 def tag(%User{} = user, tags),
1351 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1353 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1354 Repo.transaction(fn ->
1355 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1359 def untag(nickname, tags) when is_binary(nickname),
1360 do: untag(get_by_nickname(nickname), tags)
1362 def untag(%User{} = user, tags),
1363 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1365 defp update_tags(%User{} = user, new_tags) do
1366 {:ok, updated_user} =
1368 |> change(%{tags: new_tags})
1369 |> update_and_set_cache()
1374 defp normalize_tags(tags) do
1377 |> Enum.map(&String.downcase(&1))
1380 defp local_nickname_regex do
1381 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1382 @extended_local_nickname_regex
1384 @strict_local_nickname_regex
1388 def local_nickname(nickname_or_mention) do
1391 |> String.split("@")
1395 def full_nickname(nickname_or_mention),
1396 do: String.trim_leading(nickname_or_mention, "@")
1398 def error_user(ap_id) do
1403 nickname: "erroruser@example.com",
1404 inserted_at: NaiveDateTime.utc_now()
1408 @spec all_superusers() :: [User.t()]
1409 def all_superusers do
1410 User.Query.build(%{super_users: true, local: true, deactivated: false})
1414 def showing_reblogs?(%User{} = user, %User{} = target) do
1415 target.ap_id not in user.info.muted_reblogs
1419 The function returns a query to get users with no activity for given interval of days.
1420 Inactive users are those who didn't read any notification, or had any activity where
1421 the user is the activity's actor, during `inactivity_threshold` days.
1422 Deactivated users will not appear in this list.
1426 iex> Pleroma.User.list_inactive_users()
1429 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1430 def list_inactive_users_query(inactivity_threshold \\ 7) do
1431 negative_inactivity_threshold = -inactivity_threshold
1432 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1433 # Subqueries are not supported in `where` clauses, join gets too complicated.
1434 has_read_notifications =
1435 from(n in Pleroma.Notification,
1436 where: n.seen == true,
1438 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1441 |> Pleroma.Repo.all()
1443 from(u in Pleroma.User,
1444 left_join: a in Pleroma.Activity,
1445 on: u.ap_id == a.actor,
1446 where: not is_nil(u.nickname),
1447 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1448 where: u.id not in ^has_read_notifications,
1451 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1452 is_nil(max(a.inserted_at))
1457 Enable or disable email notifications for user
1461 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1462 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1464 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1465 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1467 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1468 {:ok, t()} | {:error, Ecto.Changeset.t()}
1469 def switch_email_notifications(user, type, status) do
1470 update_info(user, &User.Info.update_email_notifications(&1, %{type => status}))
1474 Set `last_digest_emailed_at` value for the user to current time
1476 @spec touch_last_digest_emailed_at(t()) :: t()
1477 def touch_last_digest_emailed_at(user) do
1478 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1480 {:ok, updated_user} =
1482 |> change(%{last_digest_emailed_at: now})
1483 |> update_and_set_cache()
1488 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1489 def toggle_confirmation(%User{} = user) do
1490 need_confirmation? = !user.info.confirmation_pending
1493 |> update_info(&User.Info.confirmation_changeset(&1, need_confirmation: need_confirmation?))
1496 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1500 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1501 # use instance-default
1502 config = Pleroma.Config.get([:assets, :mascots])
1503 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1504 mascot = Keyword.get(config, default_mascot)
1507 "id" => "default-mascot",
1508 "url" => mascot[:url],
1509 "preview_url" => mascot[:url],
1511 "mime_type" => mascot[:mime_type]
1516 def ensure_keys_present(%{info: %{keys: keys}} = user) when not is_nil(keys), do: {:ok, user}
1518 def ensure_keys_present(%User{} = user) do
1519 with {:ok, pem} <- Keys.generate_rsa_pem() do
1520 update_info(user, &User.Info.set_keys(&1, pem))
1524 def get_ap_ids_by_nicknames(nicknames) do
1526 where: u.nickname in ^nicknames,
1532 defdelegate search(query, opts \\ []), to: User.Search
1534 defp put_password_hash(
1535 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1537 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1540 defp put_password_hash(changeset), do: changeset
1542 def is_internal_user?(%User{nickname: nil}), do: true
1543 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1544 def is_internal_user?(_), do: false
1546 # A hack because user delete activities have a fake id for whatever reason
1547 # TODO: Get rid of this
1548 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
1550 def get_delivered_users_by_object_id(object_id) do
1552 inner_join: delivery in assoc(u, :deliveries),
1553 where: delivery.object_id == ^object_id
1558 def change_email(user, email) do
1560 |> cast(%{email: email}, [:email])
1561 |> validate_required([:email])
1562 |> unique_constraint(:email)
1563 |> validate_format(:email, @email_regex)
1564 |> update_and_set_cache()
1568 Changes `user.info` and returns the user changeset.
1570 `fun` is called with the `user.info`.
1572 def change_info(user, fun) do
1573 changeset = change(user)
1574 info = get_field(changeset, :info) || %User.Info{}
1575 put_embed(changeset, :info, fun.(info))
1579 Updates `user.info` and sets cache.
1581 `fun` is called with the `user.info`.
1583 def update_info(user, fun) do
1586 |> update_and_set_cache()