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.Conversation.Participation
15 alias Pleroma.Delivery
16 alias Pleroma.FollowingRelationship
18 alias Pleroma.Notification
20 alias Pleroma.Registration
22 alias Pleroma.RepoStreamer
25 alias Pleroma.Web.ActivityPub.ActivityPub
26 alias Pleroma.Web.ActivityPub.Utils
27 alias Pleroma.Web.CommonAPI
28 alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
29 alias Pleroma.Web.OAuth
30 alias Pleroma.Web.RelMe
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)
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 defdelegate following_count(user), to: FollowingRelationship
165 defp truncate_if_exists(params, key, max_length) do
166 if Map.has_key?(params, key) and is_binary(params[key]) do
167 {value, _chopped} = String.split_at(params[key], max_length)
168 Map.put(params, key, value)
174 def remote_user_creation(params) do
175 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
176 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
180 |> Map.put(:info, params[:info] || %{})
181 |> truncate_if_exists(:name, name_limit)
182 |> truncate_if_exists(:bio, bio_limit)
186 |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
187 |> validate_required([:name, :ap_id])
188 |> unique_constraint(:nickname)
189 |> validate_format(:nickname, @email_regex)
190 |> validate_length(:bio, max: bio_limit)
191 |> validate_length(:name, max: name_limit)
192 |> change_info(&User.Info.remote_user_creation(&1, params[:info]))
194 case params[:info][:source_data] do
195 %{"followers" => followers, "following" => following} ->
197 |> put_change(:follower_address, followers)
198 |> put_change(:following_address, following)
201 followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
202 put_change(changeset, :follower_address, followers)
206 def update_changeset(struct, params \\ %{}) do
207 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
208 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
211 |> cast(params, [:bio, :name, :avatar])
212 |> unique_constraint(:nickname)
213 |> validate_format(:nickname, local_nickname_regex())
214 |> validate_length(:bio, max: bio_limit)
215 |> validate_length(:name, min: 1, max: name_limit)
218 def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
219 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
220 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
222 params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
233 |> unique_constraint(:nickname)
234 |> validate_format(:nickname, local_nickname_regex())
235 |> validate_length(:bio, max: bio_limit)
236 |> validate_length(:name, max: name_limit)
237 |> change_info(&User.Info.user_upgrade(&1, params[:info], remote?))
240 def password_update_changeset(struct, params) do
242 |> cast(params, [:password, :password_confirmation])
243 |> validate_required([:password, :password_confirmation])
244 |> validate_confirmation(:password)
246 |> put_embed(:info, User.Info.set_password_reset_pending(struct.info, false))
249 @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
250 def reset_password(%User{id: user_id} = user, data) do
253 |> Multi.update(:user, password_update_changeset(user, data))
254 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
255 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
257 case Repo.transaction(multi) do
258 {:ok, %{user: user} = _} -> set_cache(user)
259 {:error, _, changeset, _} -> {:error, changeset}
263 def force_password_reset_async(user) do
264 BackgroundWorker.enqueue("force_password_reset", %{"user_id" => user.id})
267 @spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
268 def force_password_reset(user) do
269 info_cng = User.Info.set_password_reset_pending(user.info, true)
273 |> put_embed(:info, info_cng)
274 |> update_and_set_cache()
277 def register_changeset(struct, params \\ %{}, opts \\ []) do
278 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
279 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
282 if is_nil(opts[:need_confirmation]) do
283 Pleroma.Config.get([:instance, :account_activation_required])
285 opts[:need_confirmation]
289 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
290 |> validate_required([:name, :nickname, :password, :password_confirmation])
291 |> validate_confirmation(:password)
292 |> unique_constraint(:email)
293 |> unique_constraint(:nickname)
294 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
295 |> validate_format(:nickname, local_nickname_regex())
296 |> validate_format(:email, @email_regex)
297 |> validate_length(:bio, max: bio_limit)
298 |> validate_length(:name, min: 1, max: name_limit)
299 |> change_info(&User.Info.confirmation_changeset(&1, need_confirmation: need_confirmation?))
300 |> maybe_validate_required_email(opts[:external])
303 |> unique_constraint(:ap_id)
304 |> put_following_and_follower_address()
307 def maybe_validate_required_email(changeset, true), do: changeset
308 def maybe_validate_required_email(changeset, _), do: validate_required(changeset, [:email])
310 defp put_ap_id(changeset) do
311 ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
312 put_change(changeset, :ap_id, ap_id)
315 defp put_following_and_follower_address(changeset) do
316 followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
319 |> put_change(:follower_address, followers)
322 defp autofollow_users(user) do
323 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
326 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
329 follow_all(user, autofollowed_users)
332 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
333 def register(%Ecto.Changeset{} = changeset) do
334 with {:ok, user} <- Repo.insert(changeset) do
335 post_register_action(user)
339 def post_register_action(%User{} = user) do
340 with {:ok, user} <- autofollow_users(user),
341 {:ok, user} <- set_cache(user),
342 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
343 {:ok, _} <- try_send_confirmation_email(user) do
348 def try_send_confirmation_email(%User{} = user) do
349 if user.info.confirmation_pending &&
350 Pleroma.Config.get([:instance, :account_activation_required]) do
352 |> Pleroma.Emails.UserEmail.account_confirmation_email()
353 |> Pleroma.Emails.Mailer.deliver_async()
361 def needs_update?(%User{local: true}), do: false
363 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
365 def needs_update?(%User{local: false} = user) do
366 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
369 def needs_update?(_), do: true
371 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
372 def maybe_direct_follow(
374 %User{local: true, info: %{locked: true}} = followed
376 follow(follower, followed, "pending")
379 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
380 follow(follower, followed)
383 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
384 if not ap_enabled?(followed) do
385 follow(follower, followed)
391 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
392 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
393 def follow_all(follower, followeds) do
395 Enum.reject(followeds, fn followed ->
396 blocks?(follower, followed) || blocks?(followed, follower)
399 Enum.each(followeds, &follow(follower, &1, "accept"))
401 Enum.each(followeds, &update_follower_count/1)
406 defdelegate following(user), to: FollowingRelationship
408 def follow(%User{} = follower, %User{info: info} = followed, state \\ "accept") do
409 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
413 {:error, "Could not follow user: You are deactivated."}
415 deny_follow_blocked and blocks?(followed, follower) ->
416 {:error, "Could not follow user: #{followed.nickname} blocked you."}
419 FollowingRelationship.follow(follower, followed, state)
421 follower = maybe_update_following_count(follower)
423 {:ok, _} = update_follower_count(followed)
429 def unfollow(%User{} = follower, %User{} = followed) do
430 if following?(follower, followed) and follower.ap_id != followed.ap_id do
431 FollowingRelationship.unfollow(follower, followed)
433 follower = maybe_update_following_count(follower)
435 {:ok, followed} = update_follower_count(followed)
439 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
441 {:error, "Not subscribed!"}
445 defdelegate following?(follower, followed), to: FollowingRelationship
447 def locked?(%User{} = user) do
448 user.info.locked || false
452 Repo.get_by(User, id: id)
455 def get_by_ap_id(ap_id) do
456 Repo.get_by(User, ap_id: ap_id)
459 def get_all_by_ap_id(ap_ids) do
460 from(u in __MODULE__,
461 where: u.ap_id in ^ap_ids
466 def get_all_by_ids(ids) do
467 from(u in __MODULE__, where: u.id in ^ids)
471 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
472 # of the ap_id and the domain and tries to get that user
473 def get_by_guessed_nickname(ap_id) do
474 domain = URI.parse(ap_id).host
475 name = List.last(String.split(ap_id, "/"))
476 nickname = "#{name}@#{domain}"
478 get_cached_by_nickname(nickname)
481 def set_cache({:ok, user}), do: set_cache(user)
482 def set_cache({:error, err}), do: {:error, err}
484 def set_cache(%User{} = user) do
485 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
486 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
487 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
491 def update_and_set_cache(changeset) do
492 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
497 def invalidate_cache(user) do
498 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
499 Cachex.del(:user_cache, "nickname:#{user.nickname}")
500 Cachex.del(:user_cache, "user_info:#{user.id}")
503 def get_cached_by_ap_id(ap_id) do
504 key = "ap_id:#{ap_id}"
505 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
508 def get_cached_by_id(id) do
512 Cachex.fetch!(:user_cache, key, fn _ ->
516 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
517 {:commit, user.ap_id}
523 get_cached_by_ap_id(ap_id)
526 def get_cached_by_nickname(nickname) do
527 key = "nickname:#{nickname}"
529 Cachex.fetch!(:user_cache, key, fn ->
530 case get_or_fetch_by_nickname(nickname) do
531 {:ok, user} -> {:commit, user}
532 {:error, _error} -> {:ignore, nil}
537 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
538 restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
541 is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
542 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
544 restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
545 get_cached_by_nickname(nickname_or_id)
547 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
548 get_cached_by_nickname(nickname_or_id)
555 def get_by_nickname(nickname) do
556 Repo.get_by(User, nickname: nickname) ||
557 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
558 Repo.get_by(User, nickname: local_nickname(nickname))
562 def get_by_email(email), do: Repo.get_by(User, email: email)
564 def get_by_nickname_or_email(nickname_or_email) do
565 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
568 def get_cached_user_info(user) do
569 key = "user_info:#{user.id}"
570 Cachex.fetch!(:user_cache, key, fn -> user_info(user) end)
573 def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname)
575 def get_or_fetch_by_nickname(nickname) do
576 with %User{} = user <- get_by_nickname(nickname) do
580 with [_nick, _domain] <- String.split(nickname, "@"),
581 {:ok, user} <- fetch_by_nickname(nickname) do
582 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
583 fetch_initial_posts(user)
588 _e -> {:error, "not found " <> nickname}
593 @doc "Fetch some posts when the user has just been federated with"
594 def fetch_initial_posts(user) do
595 BackgroundWorker.enqueue("fetch_initial_posts", %{"user_id" => user.id})
598 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
599 def get_followers_query(%User{} = user, nil) do
600 User.Query.build(%{followers: user, deactivated: false})
603 def get_followers_query(user, page) do
605 |> get_followers_query(nil)
606 |> User.Query.paginate(page, 20)
609 @spec get_followers_query(User.t()) :: Ecto.Query.t()
610 def get_followers_query(user), do: get_followers_query(user, nil)
612 @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
613 def get_followers(user, page \\ nil) do
615 |> get_followers_query(page)
619 @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
620 def get_external_followers(user, page \\ nil) do
622 |> get_followers_query(page)
623 |> User.Query.build(%{external: true})
627 def get_followers_ids(user, page \\ nil) do
629 |> get_followers_query(page)
634 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
635 def get_friends_query(%User{} = user, nil) do
636 User.Query.build(%{friends: user, deactivated: false})
639 def get_friends_query(user, page) do
641 |> get_friends_query(nil)
642 |> User.Query.paginate(page, 20)
645 @spec get_friends_query(User.t()) :: Ecto.Query.t()
646 def get_friends_query(user), do: get_friends_query(user, nil)
648 def get_friends(user, page \\ nil) do
650 |> get_friends_query(page)
654 def get_friends_ids(user, page \\ nil) do
656 |> get_friends_query(page)
661 defdelegate get_follow_requests(user), to: FollowingRelationship
663 def increase_note_count(%User{} = user) do
665 |> where(id: ^user.id)
670 "safe_jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
677 |> Repo.update_all([])
679 {1, [user]} -> set_cache(user)
684 def decrease_note_count(%User{} = user) do
686 |> where(id: ^user.id)
691 "safe_jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
698 |> Repo.update_all([])
700 {1, [user]} -> set_cache(user)
705 def update_note_count(%User{} = user) do
709 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
714 update_info(user, &User.Info.set_note_count(&1, note_count))
717 def update_mascot(user, url) do
719 User.Info.mascot_update(
726 |> put_embed(:info, info_changeset)
727 |> update_and_set_cache()
730 @spec maybe_fetch_follow_information(User.t()) :: User.t()
731 def maybe_fetch_follow_information(user) do
732 with {:ok, user} <- fetch_follow_information(user) do
736 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
742 def fetch_follow_information(user) do
743 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
744 update_info(user, &User.Info.follow_information_update(&1, info))
748 def update_follower_count(%User{} = user) do
749 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
750 follower_count_query =
751 User.Query.build(%{followers: user, deactivated: false})
752 |> select([u], %{count: count(u.id)})
755 |> where(id: ^user.id)
756 |> join(:inner, [u], s in subquery(follower_count_query))
761 "safe_jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
768 |> Repo.update_all([])
770 {1, [user]} -> set_cache(user)
774 {:ok, maybe_fetch_follow_information(user)}
778 @spec maybe_update_following_count(User.t()) :: User.t()
779 def maybe_update_following_count(%User{local: false} = user) do
780 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
781 maybe_fetch_follow_information(user)
787 def maybe_update_following_count(user), do: user
789 def set_unread_conversation_count(%User{local: true} = user) do
790 unread_query = Participation.unread_conversation_count_for_user(user)
793 |> join(:inner, [u], p in subquery(unread_query))
798 "jsonb_set(?, '{unread_conversation_count}', ?::varchar::jsonb, true)",
804 |> where([u], u.id == ^user.id)
806 |> Repo.update_all([])
808 {1, [user]} -> set_cache(user)
813 def set_unread_conversation_count(_), do: :noop
815 def increment_unread_conversation_count(conversation, %User{local: true} = user) do
817 Participation.unread_conversation_count_for_user(user)
818 |> where([p], p.conversation_id == ^conversation.id)
821 |> join(:inner, [u], p in subquery(unread_query))
826 "jsonb_set(?, '{unread_conversation_count}', (coalesce((?->>'unread_conversation_count')::int, 0) + 1)::varchar::jsonb, true)",
832 |> where([u], u.id == ^user.id)
833 |> where([u, p], p.count == 0)
835 |> Repo.update_all([])
837 {1, [user]} -> set_cache(user)
842 def increment_unread_conversation_count(_, _), do: :noop
844 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
845 def get_users_from_set(ap_ids, local_only \\ true) do
846 criteria = %{ap_id: ap_ids, deactivated: false}
847 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
849 User.Query.build(criteria)
853 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
854 def get_recipients_from_activity(%Activity{recipients: to}) do
855 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
859 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
860 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
861 update_info(muter, &User.Info.add_to_mutes(&1, ap_id, notifications?))
864 def unmute(muter, %{ap_id: ap_id}) do
865 update_info(muter, &User.Info.remove_from_mutes(&1, ap_id))
868 def subscribe(subscriber, %{ap_id: ap_id}) do
869 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
870 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
872 if blocks?(subscribed, subscriber) and deny_follow_blocked do
873 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
875 update_info(subscribed, &User.Info.add_to_subscribers(&1, subscriber.ap_id))
880 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
881 with %User{} = user <- get_cached_by_ap_id(ap_id) do
882 update_info(user, &User.Info.remove_from_subscribers(&1, unsubscriber.ap_id))
886 def block(blocker, %User{ap_id: ap_id} = blocked) do
887 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
889 if following?(blocker, blocked) do
890 {:ok, blocker, _} = unfollow(blocker, blocked)
896 # clear any requested follows as well
898 case CommonAPI.reject_follow_request(blocked, blocker) do
899 {:ok, %User{} = updated_blocked} -> updated_blocked
904 if subscribed_to?(blocked, blocker) do
905 {:ok, blocker} = unsubscribe(blocked, blocker)
911 if following?(blocked, blocker), do: unfollow(blocked, blocker)
913 {:ok, blocker} = update_follower_count(blocker)
915 update_info(blocker, &User.Info.add_to_block(&1, ap_id))
918 # helper to handle the block given only an actor's AP id
919 def block(blocker, %{ap_id: ap_id}) do
920 block(blocker, get_cached_by_ap_id(ap_id))
923 def unblock(blocker, %{ap_id: ap_id}) do
924 update_info(blocker, &User.Info.remove_from_block(&1, ap_id))
927 def mutes?(nil, _), do: false
928 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
930 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
931 def muted_notifications?(nil, _), do: false
933 def muted_notifications?(user, %{ap_id: ap_id}),
934 do: Enum.member?(user.info.muted_notifications, ap_id)
936 def blocks?(%User{} = user, %User{} = target) do
937 blocks_ap_id?(user, target) || blocks_domain?(user, target)
940 def blocks?(nil, _), do: false
942 def blocks_ap_id?(%User{} = user, %User{} = target) do
943 Enum.member?(user.info.blocks, target.ap_id)
946 def blocks_ap_id?(_, _), do: false
948 def blocks_domain?(%User{} = user, %User{} = target) do
949 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
950 %{host: host} = URI.parse(target.ap_id)
951 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
954 def blocks_domain?(_, _), do: false
956 def subscribed_to?(user, %{ap_id: ap_id}) do
957 with %User{} = target <- get_cached_by_ap_id(ap_id) do
958 Enum.member?(target.info.subscribers, user.ap_id)
962 @spec muted_users(User.t()) :: [User.t()]
963 def muted_users(user) do
964 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
968 @spec blocked_users(User.t()) :: [User.t()]
969 def blocked_users(user) do
970 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
974 @spec subscribers(User.t()) :: [User.t()]
975 def subscribers(user) do
976 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
980 def block_domain(user, domain) do
981 update_info(user, &User.Info.add_to_domain_block(&1, domain))
984 def unblock_domain(user, domain) do
985 update_info(user, &User.Info.remove_from_domain_block(&1, domain))
988 def deactivate_async(user, status \\ true) do
989 BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
992 def deactivate(user, status \\ true)
994 def deactivate(users, status) when is_list(users) do
995 Repo.transaction(fn ->
996 for user <- users, do: deactivate(user, status)
1000 def deactivate(%User{} = user, status) do
1001 with {:ok, user} <- update_info(user, &User.Info.set_activation_status(&1, status)) do
1002 Enum.each(get_followers(user), &invalidate_cache/1)
1003 Enum.each(get_friends(user), &update_follower_count/1)
1009 def update_notification_settings(%User{} = user, settings \\ %{}) do
1010 update_info(user, &User.Info.update_notification_settings(&1, settings))
1013 def delete(users) when is_list(users) do
1014 for user <- users, do: delete(user)
1017 def delete(%User{} = user) do
1018 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1021 def perform(:force_password_reset, user), do: force_password_reset(user)
1023 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1024 def perform(:delete, %User{} = user) do
1025 {:ok, _user} = ActivityPub.delete(user)
1027 # Remove all relationships
1030 |> Enum.each(fn follower ->
1031 ActivityPub.unfollow(follower, user)
1032 unfollow(follower, user)
1037 |> Enum.each(fn followed ->
1038 ActivityPub.unfollow(user, followed)
1039 unfollow(user, followed)
1042 delete_user_activities(user)
1043 invalidate_cache(user)
1047 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1048 def perform(:fetch_initial_posts, %User{} = user) do
1049 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1051 # Insert all the posts in reverse order, so they're in the right order on the timeline
1052 user.info.source_data["outbox"]
1053 |> Utils.fetch_ordered_collection(pages)
1055 |> Enum.each(&Pleroma.Web.Federator.incoming_ap_doc/1)
1058 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1060 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1061 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1062 when is_list(blocked_identifiers) do
1064 blocked_identifiers,
1065 fn blocked_identifier ->
1066 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1067 {:ok, blocker} <- block(blocker, blocked),
1068 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1072 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1079 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1080 def perform(:follow_import, %User{} = follower, followed_identifiers)
1081 when is_list(followed_identifiers) do
1083 followed_identifiers,
1084 fn followed_identifier ->
1085 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1086 {:ok, follower} <- maybe_direct_follow(follower, followed),
1087 {:ok, _} <- ActivityPub.follow(follower, followed) do
1091 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1098 @spec external_users_query() :: Ecto.Query.t()
1099 def external_users_query do
1107 @spec external_users(keyword()) :: [User.t()]
1108 def external_users(opts \\ []) do
1110 external_users_query()
1111 |> select([u], struct(u, [:id, :ap_id, :info]))
1115 do: where(query, [u], u.id > ^opts[:max_id]),
1120 do: limit(query, ^opts[:limit]),
1126 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1127 BackgroundWorker.enqueue("blocks_import", %{
1128 "blocker_id" => blocker.id,
1129 "blocked_identifiers" => blocked_identifiers
1133 def follow_import(%User{} = follower, followed_identifiers)
1134 when is_list(followed_identifiers) do
1135 BackgroundWorker.enqueue("follow_import", %{
1136 "follower_id" => follower.id,
1137 "followed_identifiers" => followed_identifiers
1141 def delete_user_activities(%User{ap_id: ap_id}) do
1143 |> Activity.Queries.by_actor()
1144 |> RepoStreamer.chunk_stream(50)
1145 |> Stream.each(fn activities -> Enum.each(activities, &delete_activity/1) end)
1149 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1151 |> Object.normalize()
1152 |> ActivityPub.delete()
1155 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1156 object = Object.normalize(activity)
1159 |> get_cached_by_ap_id()
1160 |> ActivityPub.unlike(object)
1163 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1164 object = Object.normalize(activity)
1167 |> get_cached_by_ap_id()
1168 |> ActivityPub.unannounce(object)
1171 defp delete_activity(_activity), do: "Doing nothing"
1173 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1174 Pleroma.HTML.Scrubber.TwitterText
1177 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1179 def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
1181 def get_or_fetch_by_ap_id(ap_id) do
1182 user = get_cached_by_ap_id(ap_id)
1184 if !is_nil(user) and !needs_update?(user) do
1187 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1188 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1190 resp = fetch_by_ap_id(ap_id)
1192 if should_fetch_initial do
1193 with {:ok, %User{} = user} <- resp do
1194 fetch_initial_posts(user)
1202 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1203 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1204 with %User{} = user <- get_cached_by_ap_id(uri) do
1209 %User{info: %User.Info{}}
1210 |> cast(%{}, [:ap_id, :nickname, :local])
1211 |> put_change(:ap_id, uri)
1212 |> put_change(:nickname, nickname)
1213 |> put_change(:local, true)
1214 |> put_change(:follower_address, uri <> "/followers")
1222 def public_key_from_info(%{
1223 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1227 |> :public_key.pem_decode()
1229 |> :public_key.pem_entry_decode()
1234 def public_key_from_info(_), do: {:error, "not found key"}
1236 def get_public_key_for_ap_id(ap_id) do
1237 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1238 {:ok, public_key} <- public_key_from_info(user.info) do
1245 defp blank?(""), do: nil
1246 defp blank?(n), do: n
1248 def insert_or_update_user(data) do
1250 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1251 |> remote_user_creation()
1252 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1256 def ap_enabled?(%User{local: true}), do: true
1257 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1258 def ap_enabled?(_), do: false
1260 @doc "Gets or fetch a user by uri or nickname."
1261 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1262 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1263 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1265 # wait a period of time and return newest version of the User structs
1266 # this is because we have synchronous follow APIs and need to simulate them
1267 # with an async handshake
1268 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1269 with %User{} = a <- get_cached_by_id(a.id),
1270 %User{} = b <- get_cached_by_id(b.id) do
1277 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1278 with :ok <- :timer.sleep(timeout),
1279 %User{} = a <- get_cached_by_id(a.id),
1280 %User{} = b <- get_cached_by_id(b.id) do
1287 def parse_bio(bio) when is_binary(bio) and bio != "" do
1289 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1293 def parse_bio(_), do: ""
1295 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1296 # TODO: get profile URLs other than user.ap_id
1297 profile_urls = [user.ap_id]
1300 |> CommonUtils.format_input("text/plain",
1301 mentions_format: :full,
1302 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1307 def parse_bio(_, _), do: ""
1309 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1310 Repo.transaction(fn ->
1311 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1315 def tag(nickname, tags) when is_binary(nickname),
1316 do: tag(get_by_nickname(nickname), tags)
1318 def tag(%User{} = user, tags),
1319 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1321 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1322 Repo.transaction(fn ->
1323 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1327 def untag(nickname, tags) when is_binary(nickname),
1328 do: untag(get_by_nickname(nickname), tags)
1330 def untag(%User{} = user, tags),
1331 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1333 defp update_tags(%User{} = user, new_tags) do
1334 {:ok, updated_user} =
1336 |> change(%{tags: new_tags})
1337 |> update_and_set_cache()
1342 defp normalize_tags(tags) do
1345 |> Enum.map(&String.downcase/1)
1348 defp local_nickname_regex do
1349 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1350 @extended_local_nickname_regex
1352 @strict_local_nickname_regex
1356 def local_nickname(nickname_or_mention) do
1359 |> String.split("@")
1363 def full_nickname(nickname_or_mention),
1364 do: String.trim_leading(nickname_or_mention, "@")
1366 def error_user(ap_id) do
1371 nickname: "erroruser@example.com",
1372 inserted_at: NaiveDateTime.utc_now()
1376 @spec all_superusers() :: [User.t()]
1377 def all_superusers do
1378 User.Query.build(%{super_users: true, local: true, deactivated: false})
1382 def showing_reblogs?(%User{} = user, %User{} = target) do
1383 target.ap_id not in user.info.muted_reblogs
1387 The function returns a query to get users with no activity for given interval of days.
1388 Inactive users are those who didn't read any notification, or had any activity where
1389 the user is the activity's actor, during `inactivity_threshold` days.
1390 Deactivated users will not appear in this list.
1394 iex> Pleroma.User.list_inactive_users()
1397 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1398 def list_inactive_users_query(inactivity_threshold \\ 7) do
1399 negative_inactivity_threshold = -inactivity_threshold
1400 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1401 # Subqueries are not supported in `where` clauses, join gets too complicated.
1402 has_read_notifications =
1403 from(n in Pleroma.Notification,
1404 where: n.seen == true,
1406 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1409 |> Pleroma.Repo.all()
1411 from(u in Pleroma.User,
1412 left_join: a in Pleroma.Activity,
1413 on: u.ap_id == a.actor,
1414 where: not is_nil(u.nickname),
1415 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1416 where: u.id not in ^has_read_notifications,
1419 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1420 is_nil(max(a.inserted_at))
1425 Enable or disable email notifications for user
1429 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1430 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1432 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1433 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1435 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1436 {:ok, t()} | {:error, Ecto.Changeset.t()}
1437 def switch_email_notifications(user, type, status) do
1438 update_info(user, &User.Info.update_email_notifications(&1, %{type => status}))
1442 Set `last_digest_emailed_at` value for the user to current time
1444 @spec touch_last_digest_emailed_at(t()) :: t()
1445 def touch_last_digest_emailed_at(user) do
1446 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1448 {:ok, updated_user} =
1450 |> change(%{last_digest_emailed_at: now})
1451 |> update_and_set_cache()
1456 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1457 def toggle_confirmation(%User{} = user) do
1458 need_confirmation? = !user.info.confirmation_pending
1461 |> update_info(&User.Info.confirmation_changeset(&1, need_confirmation: need_confirmation?))
1464 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1468 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1469 # use instance-default
1470 config = Pleroma.Config.get([:assets, :mascots])
1471 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1472 mascot = Keyword.get(config, default_mascot)
1475 "id" => "default-mascot",
1476 "url" => mascot[:url],
1477 "preview_url" => mascot[:url],
1479 "mime_type" => mascot[:mime_type]
1484 def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
1486 def ensure_keys_present(%User{} = user) do
1487 with {:ok, pem} <- Keys.generate_rsa_pem() do
1489 |> cast(%{keys: pem}, [:keys])
1490 |> validate_required([:keys])
1491 |> update_and_set_cache()
1495 def get_ap_ids_by_nicknames(nicknames) do
1497 where: u.nickname in ^nicknames,
1503 defdelegate search(query, opts \\ []), to: User.Search
1505 defp put_password_hash(
1506 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1508 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1511 defp put_password_hash(changeset), do: changeset
1513 def is_internal_user?(%User{nickname: nil}), do: true
1514 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1515 def is_internal_user?(_), do: false
1517 # A hack because user delete activities have a fake id for whatever reason
1518 # TODO: Get rid of this
1519 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
1521 def get_delivered_users_by_object_id(object_id) do
1523 inner_join: delivery in assoc(u, :deliveries),
1524 where: delivery.object_id == ^object_id
1529 def change_email(user, email) do
1531 |> cast(%{email: email}, [:email])
1532 |> validate_required([:email])
1533 |> unique_constraint(:email)
1534 |> validate_format(:email, @email_regex)
1535 |> update_and_set_cache()
1539 Changes `user.info` and returns the user changeset.
1541 `fun` is called with the `user.info`.
1543 def change_info(user, fun) do
1544 changeset = change(user)
1545 info = get_field(changeset, :info) || %User.Info{}
1546 put_embed(changeset, :info, fun.(info))
1550 Updates `user.info` and sets cache.
1552 `fun` is called with the `user.info`.
1554 def update_info(users, fun) when is_list(users) do
1555 Repo.transaction(fn ->
1556 for user <- users, do: update_info(user, fun)
1560 def update_info(user, fun) do
1563 |> update_and_set_cache()