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 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
509 # of the ap_id and the domain and tries to get that user
510 def get_by_guessed_nickname(ap_id) do
511 domain = URI.parse(ap_id).host
512 name = List.last(String.split(ap_id, "/"))
513 nickname = "#{name}@#{domain}"
515 get_cached_by_nickname(nickname)
518 def set_cache({:ok, user}), do: set_cache(user)
519 def set_cache({:error, err}), do: {:error, err}
521 def set_cache(%User{} = user) do
522 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
523 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
524 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
528 def update_and_set_cache(changeset) do
529 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
534 def invalidate_cache(user) do
535 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
536 Cachex.del(:user_cache, "nickname:#{user.nickname}")
537 Cachex.del(:user_cache, "user_info:#{user.id}")
540 def get_cached_by_ap_id(ap_id) do
541 key = "ap_id:#{ap_id}"
542 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
545 def get_cached_by_id(id) do
549 Cachex.fetch!(:user_cache, key, fn _ ->
553 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
554 {:commit, user.ap_id}
560 get_cached_by_ap_id(ap_id)
563 def get_cached_by_nickname(nickname) do
564 key = "nickname:#{nickname}"
566 Cachex.fetch!(:user_cache, key, fn ->
567 case get_or_fetch_by_nickname(nickname) do
568 {:ok, user} -> {:commit, user}
569 {:error, _error} -> {:ignore, nil}
574 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
575 restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
578 is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
579 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
581 restrict_to_local == false ->
582 get_cached_by_nickname(nickname_or_id)
584 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
585 get_cached_by_nickname(nickname_or_id)
592 def get_by_nickname(nickname) do
593 Repo.get_by(User, nickname: nickname) ||
594 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
595 Repo.get_by(User, nickname: local_nickname(nickname))
599 def get_by_email(email), do: Repo.get_by(User, email: email)
601 def get_by_nickname_or_email(nickname_or_email) do
602 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
605 def get_cached_user_info(user) do
606 key = "user_info:#{user.id}"
607 Cachex.fetch!(:user_cache, key, fn -> user_info(user) end)
610 def fetch_by_nickname(nickname) do
611 case ActivityPub.make_user_from_nickname(nickname) do
612 {:ok, user} -> {:ok, user}
613 _ -> OStatus.make_user(nickname)
617 def get_or_fetch_by_nickname(nickname) do
618 with %User{} = user <- get_by_nickname(nickname) do
622 with [_nick, _domain] <- String.split(nickname, "@"),
623 {:ok, user} <- fetch_by_nickname(nickname) do
624 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
625 fetch_initial_posts(user)
630 _e -> {:error, "not found " <> nickname}
635 @doc "Fetch some posts when the user has just been federated with"
636 def fetch_initial_posts(user) do
637 BackgroundWorker.enqueue("fetch_initial_posts", %{"user_id" => user.id})
640 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
641 def get_followers_query(%User{} = user, nil) do
642 User.Query.build(%{followers: user, deactivated: false})
645 def get_followers_query(user, page) do
647 |> get_followers_query(nil)
648 |> User.Query.paginate(page, 20)
651 @spec get_followers_query(User.t()) :: Ecto.Query.t()
652 def get_followers_query(user), do: get_followers_query(user, nil)
654 @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
655 def get_followers(user, page \\ nil) do
657 |> get_followers_query(page)
661 @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
662 def get_external_followers(user, page \\ nil) do
664 |> get_followers_query(page)
665 |> User.Query.build(%{external: true})
669 def get_followers_ids(user, page \\ nil) do
671 |> get_followers_query(page)
676 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
677 def get_friends_query(%User{} = user, nil) do
678 User.Query.build(%{friends: user, deactivated: false})
681 def get_friends_query(user, page) do
683 |> get_friends_query(nil)
684 |> User.Query.paginate(page, 20)
687 @spec get_friends_query(User.t()) :: Ecto.Query.t()
688 def get_friends_query(user), do: get_friends_query(user, nil)
690 def get_friends(user, page \\ nil) do
692 |> get_friends_query(page)
696 def get_friends_ids(user, page \\ nil) do
698 |> get_friends_query(page)
703 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
704 def get_follow_requests(%User{} = user) do
706 |> Activity.follow_requests_for_actor()
707 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
708 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
709 |> group_by([a, u], u.id)
714 def increase_note_count(%User{} = user) do
716 |> where(id: ^user.id)
721 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
728 |> Repo.update_all([])
730 {1, [user]} -> set_cache(user)
735 def decrease_note_count(%User{} = user) do
737 |> where(id: ^user.id)
742 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
749 |> Repo.update_all([])
751 {1, [user]} -> set_cache(user)
756 def update_note_count(%User{} = user) do
760 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
765 update_info(user, &User.Info.set_note_count(&1, note_count))
768 @spec maybe_fetch_follow_information(User.t()) :: User.t()
769 def maybe_fetch_follow_information(user) do
770 with {:ok, user} <- fetch_follow_information(user) do
774 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
780 def fetch_follow_information(user) do
781 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
782 update_info(user, &User.Info.follow_information_update(&1, info))
786 def update_follower_count(%User{} = user) do
787 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
788 follower_count_query =
789 User.Query.build(%{followers: user, deactivated: false})
790 |> select([u], %{count: count(u.id)})
793 |> where(id: ^user.id)
794 |> join(:inner, [u], s in subquery(follower_count_query))
799 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
806 |> Repo.update_all([])
808 {1, [user]} -> set_cache(user)
812 {:ok, maybe_fetch_follow_information(user)}
816 @spec maybe_update_following_count(User.t()) :: User.t()
817 def maybe_update_following_count(%User{local: false} = user) do
818 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
819 maybe_fetch_follow_information(user)
825 def maybe_update_following_count(user), do: user
827 def remove_duplicated_following(%User{following: following} = user) do
828 uniq_following = Enum.uniq(following)
830 if length(following) == length(uniq_following) do
834 |> update_changeset(%{following: uniq_following})
835 |> update_and_set_cache()
839 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
840 def get_users_from_set(ap_ids, local_only \\ true) do
841 criteria = %{ap_id: ap_ids, deactivated: false}
842 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
844 User.Query.build(criteria)
848 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
849 def get_recipients_from_activity(%Activity{recipients: to}) do
850 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
854 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
855 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
856 update_info(muter, &User.Info.add_to_mutes(&1, ap_id, notifications?))
859 def unmute(muter, %{ap_id: ap_id}) do
860 update_info(muter, &User.Info.remove_from_mutes(&1, ap_id))
863 def subscribe(subscriber, %{ap_id: ap_id}) do
864 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
865 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
867 if blocks?(subscribed, subscriber) and deny_follow_blocked do
868 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
870 update_info(subscribed, &User.Info.add_to_subscribers(&1, subscriber.ap_id))
875 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
876 with %User{} = user <- get_cached_by_ap_id(ap_id) do
877 update_info(user, &User.Info.remove_from_subscribers(&1, unsubscriber.ap_id))
881 def block(blocker, %User{ap_id: ap_id} = blocked) do
882 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
884 if following?(blocker, blocked) do
885 {:ok, blocker, _} = unfollow(blocker, blocked)
891 # clear any requested follows as well
893 case CommonAPI.reject_follow_request(blocked, blocker) do
894 {:ok, %User{} = updated_blocked} -> updated_blocked
899 if subscribed_to?(blocked, blocker) do
900 {:ok, blocker} = unsubscribe(blocked, blocker)
906 if following?(blocked, blocker), do: unfollow(blocked, blocker)
908 {:ok, blocker} = update_follower_count(blocker)
910 update_info(blocker, &User.Info.add_to_block(&1, ap_id))
913 # helper to handle the block given only an actor's AP id
914 def block(blocker, %{ap_id: ap_id}) do
915 block(blocker, get_cached_by_ap_id(ap_id))
918 def unblock(blocker, %{ap_id: ap_id}) do
919 update_info(blocker, &User.Info.remove_from_block(&1, ap_id))
922 def mutes?(nil, _), do: false
923 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
925 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
926 def muted_notifications?(nil, _), do: false
928 def muted_notifications?(user, %{ap_id: ap_id}),
929 do: Enum.member?(user.info.muted_notifications, ap_id)
931 def blocks?(%User{} = user, %User{} = target) do
932 blocks_ap_id?(user, target) || blocks_domain?(user, target)
935 def blocks?(nil, _), do: false
937 def blocks_ap_id?(%User{} = user, %User{} = target) do
938 Enum.member?(user.info.blocks, target.ap_id)
941 def blocks_ap_id?(_, _), do: false
943 def blocks_domain?(%User{} = user, %User{} = target) do
944 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
945 %{host: host} = URI.parse(target.ap_id)
946 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
949 def blocks_domain?(_, _), do: false
951 def subscribed_to?(user, %{ap_id: ap_id}) do
952 with %User{} = target <- get_cached_by_ap_id(ap_id) do
953 Enum.member?(target.info.subscribers, user.ap_id)
957 @spec muted_users(User.t()) :: [User.t()]
958 def muted_users(user) do
959 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
963 @spec blocked_users(User.t()) :: [User.t()]
964 def blocked_users(user) do
965 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
969 @spec subscribers(User.t()) :: [User.t()]
970 def subscribers(user) do
971 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
975 def block_domain(user, domain) do
976 update_info(user, &User.Info.add_to_domain_block(&1, domain))
979 def unblock_domain(user, domain) do
980 update_info(user, &User.Info.remove_from_domain_block(&1, domain))
983 def deactivate_async(user, status \\ true) do
984 BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
987 def deactivate(%User{} = user, status \\ true) do
988 with {:ok, user} <- update_info(user, &User.Info.set_activation_status(&1, status)) do
989 Enum.each(get_followers(user), &invalidate_cache/1)
990 Enum.each(get_friends(user), &update_follower_count/1)
996 def update_notification_settings(%User{} = user, settings \\ %{}) do
997 update_info(user, &User.Info.update_notification_settings(&1, settings))
1000 def delete(%User{} = user) do
1001 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1004 def perform(:force_password_reset, user), do: force_password_reset(user)
1006 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1007 def perform(:delete, %User{} = user) do
1008 {:ok, _user} = ActivityPub.delete(user)
1010 # Remove all relationships
1013 |> Enum.each(fn follower ->
1014 ActivityPub.unfollow(follower, user)
1015 unfollow(follower, user)
1020 |> Enum.each(fn followed ->
1021 ActivityPub.unfollow(user, followed)
1022 unfollow(user, followed)
1025 delete_user_activities(user)
1026 invalidate_cache(user)
1030 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1031 def perform(:fetch_initial_posts, %User{} = user) do
1032 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1034 # Insert all the posts in reverse order, so they're in the right order on the timeline
1035 user.info.source_data["outbox"]
1036 |> Utils.fetch_ordered_collection(pages)
1038 |> Enum.each(&Pleroma.Web.Federator.incoming_ap_doc/1)
1041 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1043 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1044 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1045 when is_list(blocked_identifiers) do
1047 blocked_identifiers,
1048 fn blocked_identifier ->
1049 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1050 {:ok, blocker} <- block(blocker, blocked),
1051 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1055 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1062 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1063 def perform(:follow_import, %User{} = follower, followed_identifiers)
1064 when is_list(followed_identifiers) do
1066 followed_identifiers,
1067 fn followed_identifier ->
1068 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1069 {:ok, follower} <- maybe_direct_follow(follower, followed),
1070 {:ok, _} <- ActivityPub.follow(follower, followed) do
1074 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1081 @spec external_users_query() :: Ecto.Query.t()
1082 def external_users_query do
1090 @spec external_users(keyword()) :: [User.t()]
1091 def external_users(opts \\ []) do
1093 external_users_query()
1094 |> select([u], struct(u, [:id, :ap_id, :info]))
1098 do: where(query, [u], u.id > ^opts[:max_id]),
1103 do: limit(query, ^opts[:limit]),
1109 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1110 BackgroundWorker.enqueue("blocks_import", %{
1111 "blocker_id" => blocker.id,
1112 "blocked_identifiers" => blocked_identifiers
1116 def follow_import(%User{} = follower, followed_identifiers)
1117 when is_list(followed_identifiers) do
1118 BackgroundWorker.enqueue("follow_import", %{
1119 "follower_id" => follower.id,
1120 "followed_identifiers" => followed_identifiers
1124 def delete_user_activities(%User{ap_id: ap_id}) do
1126 |> Activity.Queries.by_actor()
1127 |> RepoStreamer.chunk_stream(50)
1128 |> Stream.each(fn activities -> Enum.each(activities, &delete_activity/1) end)
1132 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1134 |> Object.normalize()
1135 |> ActivityPub.delete()
1138 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1139 object = Object.normalize(activity)
1142 |> get_cached_by_ap_id()
1143 |> ActivityPub.unlike(object)
1146 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1147 object = Object.normalize(activity)
1150 |> get_cached_by_ap_id()
1151 |> ActivityPub.unannounce(object)
1154 defp delete_activity(_activity), do: "Doing nothing"
1156 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1157 Pleroma.HTML.Scrubber.TwitterText
1160 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1162 def fetch_by_ap_id(ap_id) do
1163 case ActivityPub.make_user_from_ap_id(ap_id) do
1168 case OStatus.make_user(ap_id) do
1169 {:ok, user} -> {:ok, user}
1170 _ -> {:error, "Could not fetch by AP id"}
1175 def get_or_fetch_by_ap_id(ap_id) do
1176 user = get_cached_by_ap_id(ap_id)
1178 if !is_nil(user) and !needs_update?(user) do
1181 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1182 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1184 resp = fetch_by_ap_id(ap_id)
1186 if should_fetch_initial do
1187 with {:ok, %User{} = user} <- resp do
1188 fetch_initial_posts(user)
1196 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1197 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1198 with %User{} = user <- get_cached_by_ap_id(uri) do
1203 %User{info: %User.Info{}}
1204 |> cast(%{}, [:ap_id, :nickname, :local])
1205 |> put_change(:ap_id, uri)
1206 |> put_change(:nickname, nickname)
1207 |> put_change(:local, true)
1208 |> put_change(:follower_address, uri <> "/followers")
1216 def public_key_from_info(%{
1217 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1221 |> :public_key.pem_decode()
1223 |> :public_key.pem_entry_decode()
1229 def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
1230 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1233 def public_key_from_info(_), do: {:error, "not found key"}
1235 def get_public_key_for_ap_id(ap_id) do
1236 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1237 {:ok, public_key} <- public_key_from_info(user.info) do
1244 defp blank?(""), do: nil
1245 defp blank?(n), do: n
1247 def insert_or_update_user(data) do
1249 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1250 |> remote_user_creation()
1251 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1255 def ap_enabled?(%User{local: true}), do: true
1256 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1257 def ap_enabled?(_), do: false
1259 @doc "Gets or fetch a user by uri or nickname."
1260 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1261 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1262 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1264 # wait a period of time and return newest version of the User structs
1265 # this is because we have synchronous follow APIs and need to simulate them
1266 # with an async handshake
1267 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1268 with %User{} = a <- get_cached_by_id(a.id),
1269 %User{} = b <- get_cached_by_id(b.id) do
1276 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1277 with :ok <- :timer.sleep(timeout),
1278 %User{} = a <- get_cached_by_id(a.id),
1279 %User{} = b <- get_cached_by_id(b.id) do
1286 def parse_bio(bio) when is_binary(bio) and bio != "" do
1288 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1292 def parse_bio(_), do: ""
1294 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1295 # TODO: get profile URLs other than user.ap_id
1296 profile_urls = [user.ap_id]
1299 |> CommonUtils.format_input("text/plain",
1300 mentions_format: :full,
1301 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1306 def parse_bio(_, _), do: ""
1308 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1309 Repo.transaction(fn ->
1310 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1314 def tag(nickname, tags) when is_binary(nickname),
1315 do: tag(get_by_nickname(nickname), tags)
1317 def tag(%User{} = user, tags),
1318 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1320 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1321 Repo.transaction(fn ->
1322 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1326 def untag(nickname, tags) when is_binary(nickname),
1327 do: untag(get_by_nickname(nickname), tags)
1329 def untag(%User{} = user, tags),
1330 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1332 defp update_tags(%User{} = user, new_tags) do
1333 {:ok, updated_user} =
1335 |> change(%{tags: new_tags})
1336 |> update_and_set_cache()
1341 defp normalize_tags(tags) do
1344 |> Enum.map(&String.downcase/1)
1347 defp local_nickname_regex do
1348 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1349 @extended_local_nickname_regex
1351 @strict_local_nickname_regex
1355 def local_nickname(nickname_or_mention) do
1358 |> String.split("@")
1362 def full_nickname(nickname_or_mention),
1363 do: String.trim_leading(nickname_or_mention, "@")
1365 def error_user(ap_id) do
1370 nickname: "erroruser@example.com",
1371 inserted_at: NaiveDateTime.utc_now()
1375 @spec all_superusers() :: [User.t()]
1376 def all_superusers do
1377 User.Query.build(%{super_users: true, local: true, deactivated: false})
1381 def showing_reblogs?(%User{} = user, %User{} = target) do
1382 target.ap_id not in user.info.muted_reblogs
1386 The function returns a query to get users with no activity for given interval of days.
1387 Inactive users are those who didn't read any notification, or had any activity where
1388 the user is the activity's actor, during `inactivity_threshold` days.
1389 Deactivated users will not appear in this list.
1393 iex> Pleroma.User.list_inactive_users()
1396 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1397 def list_inactive_users_query(inactivity_threshold \\ 7) do
1398 negative_inactivity_threshold = -inactivity_threshold
1399 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1400 # Subqueries are not supported in `where` clauses, join gets too complicated.
1401 has_read_notifications =
1402 from(n in Pleroma.Notification,
1403 where: n.seen == true,
1405 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1408 |> Pleroma.Repo.all()
1410 from(u in Pleroma.User,
1411 left_join: a in Pleroma.Activity,
1412 on: u.ap_id == a.actor,
1413 where: not is_nil(u.nickname),
1414 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1415 where: u.id not in ^has_read_notifications,
1418 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1419 is_nil(max(a.inserted_at))
1424 Enable or disable email notifications for user
1428 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1429 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1431 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1432 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1434 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1435 {:ok, t()} | {:error, Ecto.Changeset.t()}
1436 def switch_email_notifications(user, type, status) do
1437 update_info(user, &User.Info.update_email_notifications(&1, %{type => status}))
1441 Set `last_digest_emailed_at` value for the user to current time
1443 @spec touch_last_digest_emailed_at(t()) :: t()
1444 def touch_last_digest_emailed_at(user) do
1445 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1447 {:ok, updated_user} =
1449 |> change(%{last_digest_emailed_at: now})
1450 |> update_and_set_cache()
1455 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1456 def toggle_confirmation(%User{} = user) do
1457 need_confirmation? = !user.info.confirmation_pending
1460 |> update_info(&User.Info.confirmation_changeset(&1, need_confirmation: need_confirmation?))
1463 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1467 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1468 # use instance-default
1469 config = Pleroma.Config.get([:assets, :mascots])
1470 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1471 mascot = Keyword.get(config, default_mascot)
1474 "id" => "default-mascot",
1475 "url" => mascot[:url],
1476 "preview_url" => mascot[:url],
1478 "mime_type" => mascot[:mime_type]
1483 def ensure_keys_present(%{info: %{keys: keys}} = user) when not is_nil(keys), do: {:ok, user}
1485 def ensure_keys_present(%User{} = user) do
1486 with {:ok, pem} <- Keys.generate_rsa_pem() do
1487 update_info(user, &User.Info.set_keys(&1, pem))
1491 def get_ap_ids_by_nicknames(nicknames) do
1493 where: u.nickname in ^nicknames,
1499 defdelegate search(query, opts \\ []), to: User.Search
1501 defp put_password_hash(
1502 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1504 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1507 defp put_password_hash(changeset), do: changeset
1509 def is_internal_user?(%User{nickname: nil}), do: true
1510 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1511 def is_internal_user?(_), do: false
1513 # A hack because user delete activities have a fake id for whatever reason
1514 # TODO: Get rid of this
1515 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
1517 def get_delivered_users_by_object_id(object_id) do
1519 inner_join: delivery in assoc(u, :deliveries),
1520 where: delivery.object_id == ^object_id
1525 def change_email(user, email) do
1527 |> cast(%{email: email}, [:email])
1528 |> validate_required([:email])
1529 |> unique_constraint(:email)
1530 |> validate_format(:email, @email_regex)
1531 |> update_and_set_cache()
1535 Changes `user.info` and returns the user changeset.
1537 `fun` is called with the `user.info`.
1539 def change_info(user, fun) do
1540 changeset = change(user)
1541 info = get_field(changeset, :info) || %User.Info{}
1542 put_embed(changeset, :info, fun.(info))
1546 Updates `user.info` and sets cache.
1548 `fun` is called with the `user.info`.
1550 def update_info(user, fun) do
1553 |> update_and_set_cache()