1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.User do
10 import Ecto, only: [assoc: 2]
14 alias Pleroma.Activity
16 alias Pleroma.Conversation.Participation
17 alias Pleroma.Delivery
18 alias Pleroma.FollowingRelationship
21 alias Pleroma.Notification
23 alias Pleroma.Registration
25 alias Pleroma.RepoStreamer
27 alias Pleroma.UserRelationship
29 alias Pleroma.Web.ActivityPub.ActivityPub
30 alias Pleroma.Web.ActivityPub.Utils
31 alias Pleroma.Web.CommonAPI
32 alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
33 alias Pleroma.Web.OAuth
34 alias Pleroma.Web.RelMe
35 alias Pleroma.Workers.BackgroundWorker
39 @type t :: %__MODULE__{}
40 @type account_status :: :active | :deactivated | :password_reset_pending | :confirmation_pending
41 @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
43 # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
44 @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])?)*$/
46 @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
47 @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
49 # AP ID user relationships (blocks, mutes etc.)
50 # Format: [rel_type: [outgoing_rel: :outgoing_rel_target, incoming_rel: :incoming_rel_source]]
51 @user_relationships_config [
53 blocker_blocks: :blocked_users,
54 blockee_blocks: :blocker_users
57 muter_mutes: :muted_users,
58 mutee_mutes: :muter_users
61 reblog_muter_mutes: :reblog_muted_users,
62 reblog_mutee_mutes: :reblog_muter_users
65 notification_muter_mutes: :notification_muted_users,
66 notification_mutee_mutes: :notification_muter_users
68 # Note: `inverse_subscription` relationship is inverse: subscriber acts as relationship target
69 inverse_subscription: [
70 subscribee_subscriptions: :subscriber_users,
71 subscriber_subscriptions: :subscribee_users
77 field(:email, :string)
79 field(:nickname, :string)
80 field(:password_hash, :string)
81 field(:password, :string, virtual: true)
82 field(:password_confirmation, :string, virtual: true)
84 field(:ap_id, :string)
86 field(:local, :boolean, default: true)
87 field(:follower_address, :string)
88 field(:following_address, :string)
89 field(:search_rank, :float, virtual: true)
90 field(:search_type, :integer, virtual: true)
91 field(:tags, {:array, :string}, default: [])
92 field(:last_refreshed_at, :naive_datetime_usec)
93 field(:last_digest_emailed_at, :naive_datetime)
94 field(:banner, :map, default: %{})
95 field(:background, :map, default: %{})
96 field(:source_data, :map, default: %{})
97 field(:note_count, :integer, default: 0)
98 field(:follower_count, :integer, default: 0)
99 field(:following_count, :integer, default: 0)
100 field(:locked, :boolean, default: false)
101 field(:confirmation_pending, :boolean, default: false)
102 field(:password_reset_pending, :boolean, default: false)
103 field(:confirmation_token, :string, default: nil)
104 field(:default_scope, :string, default: "public")
105 field(:domain_blocks, {:array, :string}, default: [])
106 field(:deactivated, :boolean, default: false)
107 field(:no_rich_text, :boolean, default: false)
108 field(:ap_enabled, :boolean, default: false)
109 field(:is_moderator, :boolean, default: false)
110 field(:is_admin, :boolean, default: false)
111 field(:show_role, :boolean, default: true)
112 field(:settings, :map, default: nil)
113 field(:magic_key, :string, default: nil)
114 field(:uri, :string, default: nil)
115 field(:hide_followers_count, :boolean, default: false)
116 field(:hide_follows_count, :boolean, default: false)
117 field(:hide_followers, :boolean, default: false)
118 field(:hide_follows, :boolean, default: false)
119 field(:hide_favorites, :boolean, default: true)
120 field(:unread_conversation_count, :integer, default: 0)
121 field(:pinned_activities, {:array, :string}, default: [])
122 field(:email_notifications, :map, default: %{"digest" => false})
123 field(:mascot, :map, default: nil)
124 field(:emoji, {:array, :map}, default: [])
125 field(:pleroma_settings_store, :map, default: %{})
126 field(:fields, {:array, :map}, default: [])
127 field(:raw_fields, {:array, :map}, default: [])
128 field(:discoverable, :boolean, default: false)
129 field(:invisible, :boolean, default: false)
130 field(:allow_following_move, :boolean, default: true)
131 field(:skip_thread_containment, :boolean, default: false)
132 field(:actor_type, :string, default: "Person")
133 field(:also_known_as, {:array, :string}, default: [])
136 :notification_settings,
137 Pleroma.User.NotificationSetting,
141 has_many(:notifications, Notification)
142 has_many(:registrations, Registration)
143 has_many(:deliveries, Delivery)
145 has_many(:outgoing_relationships, UserRelationship, foreign_key: :source_id)
146 has_many(:incoming_relationships, UserRelationship, foreign_key: :target_id)
148 for {relationship_type,
150 {outgoing_relation, outgoing_relation_target},
151 {incoming_relation, incoming_relation_source}
152 ]} <- @user_relationships_config do
153 # Definitions of `has_many :blocker_blocks`, `has_many :muter_mutes` etc.
154 has_many(outgoing_relation, UserRelationship,
155 foreign_key: :source_id,
156 where: [relationship_type: relationship_type]
159 # Definitions of `has_many :blockee_blocks`, `has_many :mutee_mutes` etc.
160 has_many(incoming_relation, UserRelationship,
161 foreign_key: :target_id,
162 where: [relationship_type: relationship_type]
165 # Definitions of `has_many :blocked_users`, `has_many :muted_users` etc.
166 has_many(outgoing_relation_target, through: [outgoing_relation, :target])
168 # Definitions of `has_many :blocker_users`, `has_many :muter_users` etc.
169 has_many(incoming_relation_source, through: [incoming_relation, :source])
172 # `:blocks` is deprecated (replaced with `blocked_users` relation)
173 field(:blocks, {:array, :string}, default: [])
174 # `:mutes` is deprecated (replaced with `muted_users` relation)
175 field(:mutes, {:array, :string}, default: [])
176 # `:muted_reblogs` is deprecated (replaced with `reblog_muted_users` relation)
177 field(:muted_reblogs, {:array, :string}, default: [])
178 # `:muted_notifications` is deprecated (replaced with `notification_muted_users` relation)
179 field(:muted_notifications, {:array, :string}, default: [])
180 # `:subscribers` is deprecated (replaced with `subscriber_users` relation)
181 field(:subscribers, {:array, :string}, default: [])
186 for {_relationship_type, [{_outgoing_relation, outgoing_relation_target}, _]} <-
187 @user_relationships_config do
188 # Definitions of `blocked_users_relation/1`, `muted_users_relation/1`, etc.
189 def unquote(:"#{outgoing_relation_target}_relation")(user, restrict_deactivated? \\ false) do
190 target_users_query = assoc(user, unquote(outgoing_relation_target))
192 if restrict_deactivated? do
193 restrict_deactivated(target_users_query)
199 # Definitions of `blocked_users/1`, `muted_users/1`, etc.
200 def unquote(outgoing_relation_target)(user, restrict_deactivated? \\ false) do
202 |> apply(unquote(:"#{outgoing_relation_target}_relation"), [
204 restrict_deactivated?
209 # Definitions of `blocked_users_ap_ids/1`, `muted_users_ap_ids/1`, etc.
210 def unquote(:"#{outgoing_relation_target}_ap_ids")(user, restrict_deactivated? \\ false) do
212 |> apply(unquote(:"#{outgoing_relation_target}_relation"), [
214 restrict_deactivated?
216 |> select([u], u.ap_id)
221 @doc "Returns status account"
222 @spec account_status(User.t()) :: account_status()
223 def account_status(%User{deactivated: true}), do: :deactivated
224 def account_status(%User{password_reset_pending: true}), do: :password_reset_pending
226 def account_status(%User{confirmation_pending: true}) do
227 case Config.get([:instance, :account_activation_required]) do
228 true -> :confirmation_pending
233 def account_status(%User{}), do: :active
235 @spec visible_for?(User.t(), User.t() | nil) :: boolean()
236 def visible_for?(user, for_user \\ nil)
238 def visible_for?(%User{invisible: true}, _), do: false
240 def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
242 def visible_for?(%User{} = user, for_user) do
243 account_status(user) == :active || superuser?(for_user)
246 def visible_for?(_, _), do: false
248 @spec superuser?(User.t()) :: boolean()
249 def superuser?(%User{local: true, is_admin: true}), do: true
250 def superuser?(%User{local: true, is_moderator: true}), do: true
251 def superuser?(_), do: false
253 @spec invisible?(User.t()) :: boolean()
254 def invisible?(%User{invisible: true}), do: true
255 def invisible?(_), do: false
257 def avatar_url(user, options \\ []) do
259 %{"url" => [%{"href" => href} | _]} -> href
260 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
264 def banner_url(user, options \\ []) do
266 %{"url" => [%{"href" => href} | _]} -> href
267 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
271 def profile_url(%User{source_data: %{"url" => url}}), do: url
272 def profile_url(%User{ap_id: ap_id}), do: ap_id
273 def profile_url(_), do: nil
275 def ap_id(%User{nickname: nickname}), do: "#{Web.base_url()}/users/#{nickname}"
277 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
278 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
280 @spec ap_following(User.t()) :: Sring.t()
281 def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
282 def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
284 def follow_state(%User{} = user, %User{} = target) do
285 case Utils.fetch_latest_follow(user, target) do
286 %{data: %{"state" => state}} -> state
287 # Ideally this would be nil, but then Cachex does not commit the value
292 def get_cached_follow_state(user, target) do
293 key = "follow_state:#{user.ap_id}|#{target.ap_id}"
294 Cachex.fetch!(:user_cache, key, fn _ -> {:commit, follow_state(user, target)} end)
297 @spec set_follow_state_cache(String.t(), String.t(), String.t()) :: {:ok | :error, boolean()}
298 def set_follow_state_cache(user_ap_id, target_ap_id, state) do
299 Cachex.put(:user_cache, "follow_state:#{user_ap_id}|#{target_ap_id}", state)
302 @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
303 def restrict_deactivated(query) do
304 from(u in query, where: u.deactivated != ^true)
307 defdelegate following_count(user), to: FollowingRelationship
309 defp truncate_fields_param(params) do
310 if Map.has_key?(params, :fields) do
311 Map.put(params, :fields, Enum.map(params[:fields], &truncate_field/1))
317 defp truncate_if_exists(params, key, max_length) do
318 if Map.has_key?(params, key) and is_binary(params[key]) do
319 {value, _chopped} = String.split_at(params[key], max_length)
320 Map.put(params, key, value)
326 def remote_user_creation(params) do
327 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
328 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
332 |> truncate_if_exists(:name, name_limit)
333 |> truncate_if_exists(:bio, bio_limit)
334 |> truncate_fields_param()
354 :hide_followers_count,
365 |> validate_required([:name, :ap_id])
366 |> unique_constraint(:nickname)
367 |> validate_format(:nickname, @email_regex)
368 |> validate_length(:bio, max: bio_limit)
369 |> validate_length(:name, max: name_limit)
370 |> validate_fields(true)
372 case params[:source_data] do
373 %{"followers" => followers, "following" => following} ->
375 |> put_change(:follower_address, followers)
376 |> put_change(:following_address, following)
379 followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
380 put_change(changeset, :follower_address, followers)
384 def update_changeset(struct, params \\ %{}) do
385 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
386 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
401 :hide_followers_count,
404 :allow_following_move,
407 :skip_thread_containment,
410 :pleroma_settings_store,
416 |> unique_constraint(:nickname)
417 |> validate_format(:nickname, local_nickname_regex())
418 |> validate_length(:bio, max: bio_limit)
419 |> validate_length(:name, min: 1, max: name_limit)
420 |> validate_fields(false)
423 def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
424 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
425 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
427 params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
429 params = if remote?, do: truncate_fields_param(params), else: params
451 :allow_following_move,
453 :hide_followers_count,
459 |> unique_constraint(:nickname)
460 |> validate_format(:nickname, local_nickname_regex())
461 |> validate_length(:bio, max: bio_limit)
462 |> validate_length(:name, max: name_limit)
463 |> validate_fields(remote?)
466 def password_update_changeset(struct, params) do
468 |> cast(params, [:password, :password_confirmation])
469 |> validate_required([:password, :password_confirmation])
470 |> validate_confirmation(:password)
471 |> put_password_hash()
472 |> put_change(:password_reset_pending, false)
475 @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
476 def reset_password(%User{id: user_id} = user, data) do
479 |> Multi.update(:user, password_update_changeset(user, data))
480 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
481 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
483 case Repo.transaction(multi) do
484 {:ok, %{user: user} = _} -> set_cache(user)
485 {:error, _, changeset, _} -> {:error, changeset}
489 def update_password_reset_pending(user, value) do
492 |> put_change(:password_reset_pending, value)
493 |> update_and_set_cache()
496 def force_password_reset_async(user) do
497 BackgroundWorker.enqueue("force_password_reset", %{"user_id" => user.id})
500 @spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
501 def force_password_reset(user), do: update_password_reset_pending(user, true)
503 def register_changeset(struct, params \\ %{}, opts \\ []) do
504 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
505 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
508 if is_nil(opts[:need_confirmation]) do
509 Pleroma.Config.get([:instance, :account_activation_required])
511 opts[:need_confirmation]
515 |> confirmation_changeset(need_confirmation: need_confirmation?)
516 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
517 |> validate_required([:name, :nickname, :password, :password_confirmation])
518 |> validate_confirmation(:password)
519 |> unique_constraint(:email)
520 |> unique_constraint(:nickname)
521 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
522 |> validate_format(:nickname, local_nickname_regex())
523 |> validate_format(:email, @email_regex)
524 |> validate_length(:bio, max: bio_limit)
525 |> validate_length(:name, min: 1, max: name_limit)
526 |> maybe_validate_required_email(opts[:external])
529 |> unique_constraint(:ap_id)
530 |> put_following_and_follower_address()
533 def maybe_validate_required_email(changeset, true), do: changeset
535 def maybe_validate_required_email(changeset, _) do
536 if Pleroma.Config.get([:instance, :account_activation_required]) do
537 validate_required(changeset, [:email])
543 defp put_ap_id(changeset) do
544 ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
545 put_change(changeset, :ap_id, ap_id)
548 defp put_following_and_follower_address(changeset) do
549 followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
552 |> put_change(:follower_address, followers)
555 defp autofollow_users(user) do
556 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
559 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
562 follow_all(user, autofollowed_users)
565 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
566 def register(%Ecto.Changeset{} = changeset) do
567 with {:ok, user} <- Repo.insert(changeset) do
568 post_register_action(user)
572 def post_register_action(%User{} = user) do
573 with {:ok, user} <- autofollow_users(user),
574 {:ok, user} <- set_cache(user),
575 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
576 {:ok, _} <- try_send_confirmation_email(user) do
581 def try_send_confirmation_email(%User{} = user) do
582 if user.confirmation_pending &&
583 Pleroma.Config.get([:instance, :account_activation_required]) do
585 |> Pleroma.Emails.UserEmail.account_confirmation_email()
586 |> Pleroma.Emails.Mailer.deliver_async()
594 def try_send_confirmation_email(users) do
595 Enum.each(users, &try_send_confirmation_email/1)
598 def needs_update?(%User{local: true}), do: false
600 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
602 def needs_update?(%User{local: false} = user) do
603 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
606 def needs_update?(_), do: true
608 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
609 def maybe_direct_follow(%User{} = follower, %User{local: true, locked: true} = followed) do
610 follow(follower, followed, "pending")
613 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
614 follow(follower, followed)
617 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
618 if not ap_enabled?(followed) do
619 follow(follower, followed)
625 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
626 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
627 def follow_all(follower, followeds) do
629 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
630 |> Enum.each(&follow(follower, &1, "accept"))
635 defdelegate following(user), to: FollowingRelationship
637 def follow(%User{} = follower, %User{} = followed, state \\ "accept") do
638 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
641 followed.deactivated ->
642 {:error, "Could not follow user: #{followed.nickname} is deactivated."}
644 deny_follow_blocked and blocks?(followed, follower) ->
645 {:error, "Could not follow user: #{followed.nickname} blocked you."}
648 FollowingRelationship.follow(follower, followed, state)
650 {:ok, _} = update_follower_count(followed)
653 |> update_following_count()
658 def unfollow(%User{ap_id: ap_id}, %User{ap_id: ap_id}) do
659 {:error, "Not subscribed!"}
662 def unfollow(%User{} = follower, %User{} = followed) do
663 case get_follow_state(follower, followed) do
664 state when state in ["accept", "pending"] ->
665 FollowingRelationship.unfollow(follower, followed)
666 {:ok, followed} = update_follower_count(followed)
670 |> update_following_count()
673 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
676 {:error, "Not subscribed!"}
680 defdelegate following?(follower, followed), to: FollowingRelationship
682 def get_follow_state(%User{} = follower, %User{} = following) do
683 following_relationship = FollowingRelationship.get(follower, following)
685 case {following_relationship, following.local} do
687 case Utils.fetch_latest_follow(follower, following) do
688 %{data: %{"state" => state}} when state in ["pending", "accept"] -> state
692 {%{state: state}, _} ->
700 def locked?(%User{} = user) do
705 Repo.get_by(User, id: id)
708 def get_by_ap_id(ap_id) do
709 Repo.get_by(User, ap_id: ap_id)
712 def get_all_by_ap_id(ap_ids) do
713 from(u in __MODULE__,
714 where: u.ap_id in ^ap_ids
719 def get_all_by_ids(ids) do
720 from(u in __MODULE__, where: u.id in ^ids)
724 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
725 # of the ap_id and the domain and tries to get that user
726 def get_by_guessed_nickname(ap_id) do
727 domain = URI.parse(ap_id).host
728 name = List.last(String.split(ap_id, "/"))
729 nickname = "#{name}@#{domain}"
731 get_cached_by_nickname(nickname)
734 def set_cache({:ok, user}), do: set_cache(user)
735 def set_cache({:error, err}), do: {:error, err}
737 def set_cache(%User{} = user) do
738 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
739 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
743 def update_and_set_cache(struct, params) do
745 |> update_changeset(params)
746 |> update_and_set_cache()
749 def update_and_set_cache(changeset) do
750 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
755 def invalidate_cache(user) do
756 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
757 Cachex.del(:user_cache, "nickname:#{user.nickname}")
760 @spec get_cached_by_ap_id(String.t()) :: User.t() | nil
761 def get_cached_by_ap_id(ap_id) do
762 key = "ap_id:#{ap_id}"
764 with {:ok, nil} <- Cachex.get(:user_cache, key),
765 user when not is_nil(user) <- get_by_ap_id(ap_id),
766 {:ok, true} <- Cachex.put(:user_cache, key, user) do
774 def get_cached_by_id(id) do
778 Cachex.fetch!(:user_cache, key, fn _ ->
782 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
783 {:commit, user.ap_id}
789 get_cached_by_ap_id(ap_id)
792 def get_cached_by_nickname(nickname) do
793 key = "nickname:#{nickname}"
795 Cachex.fetch!(:user_cache, key, fn ->
796 case get_or_fetch_by_nickname(nickname) do
797 {:ok, user} -> {:commit, user}
798 {:error, _error} -> {:ignore, nil}
803 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
804 restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
807 is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
808 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
810 restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
811 get_cached_by_nickname(nickname_or_id)
813 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
814 get_cached_by_nickname(nickname_or_id)
821 def get_by_nickname(nickname) do
822 Repo.get_by(User, nickname: nickname) ||
823 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
824 Repo.get_by(User, nickname: local_nickname(nickname))
828 def get_by_email(email), do: Repo.get_by(User, email: email)
830 def get_by_nickname_or_email(nickname_or_email) do
831 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
834 def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname)
836 def get_or_fetch_by_nickname(nickname) do
837 with %User{} = user <- get_by_nickname(nickname) do
841 with [_nick, _domain] <- String.split(nickname, "@"),
842 {:ok, user} <- fetch_by_nickname(nickname) do
845 _e -> {:error, "not found " <> nickname}
850 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
851 def get_followers_query(%User{} = user, nil) do
852 User.Query.build(%{followers: user, deactivated: false})
855 def get_followers_query(user, page) do
857 |> get_followers_query(nil)
858 |> User.Query.paginate(page, 20)
861 @spec get_followers_query(User.t()) :: Ecto.Query.t()
862 def get_followers_query(user), do: get_followers_query(user, nil)
864 @spec get_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
865 def get_followers(user, page \\ nil) do
867 |> get_followers_query(page)
871 @spec get_external_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
872 def get_external_followers(user, page \\ nil) do
874 |> get_followers_query(page)
875 |> User.Query.build(%{external: true})
879 def get_followers_ids(user, page \\ nil) do
881 |> get_followers_query(page)
886 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
887 def get_friends_query(%User{} = user, nil) do
888 User.Query.build(%{friends: user, deactivated: false})
891 def get_friends_query(user, page) do
893 |> get_friends_query(nil)
894 |> User.Query.paginate(page, 20)
897 @spec get_friends_query(User.t()) :: Ecto.Query.t()
898 def get_friends_query(user), do: get_friends_query(user, nil)
900 def get_friends(user, page \\ nil) do
902 |> get_friends_query(page)
906 def get_friends_ap_ids(user) do
908 |> get_friends_query(nil)
909 |> select([u], u.ap_id)
913 def get_friends_ids(user, page \\ nil) do
915 |> get_friends_query(page)
920 defdelegate get_follow_requests(user), to: FollowingRelationship
922 def increase_note_count(%User{} = user) do
924 |> where(id: ^user.id)
925 |> update([u], inc: [note_count: 1])
927 |> Repo.update_all([])
929 {1, [user]} -> set_cache(user)
934 def decrease_note_count(%User{} = user) do
936 |> where(id: ^user.id)
939 note_count: fragment("greatest(0, note_count - 1)")
943 |> Repo.update_all([])
945 {1, [user]} -> set_cache(user)
950 def update_note_count(%User{} = user, note_count \\ nil) do
955 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
961 |> cast(%{note_count: note_count}, [:note_count])
962 |> update_and_set_cache()
965 @spec maybe_fetch_follow_information(User.t()) :: User.t()
966 def maybe_fetch_follow_information(user) do
967 with {:ok, user} <- fetch_follow_information(user) do
971 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
977 def fetch_follow_information(user) do
978 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
980 |> follow_information_changeset(info)
981 |> update_and_set_cache()
985 defp follow_information_changeset(user, params) do
992 :hide_followers_count,
997 def update_follower_count(%User{} = user) do
998 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
999 follower_count_query =
1000 User.Query.build(%{followers: user, deactivated: false})
1001 |> select([u], %{count: count(u.id)})
1004 |> where(id: ^user.id)
1005 |> join(:inner, [u], s in subquery(follower_count_query))
1007 set: [follower_count: s.count]
1010 |> Repo.update_all([])
1012 {1, [user]} -> set_cache(user)
1016 {:ok, maybe_fetch_follow_information(user)}
1020 @spec update_following_count(User.t()) :: User.t()
1021 def update_following_count(%User{local: false} = user) do
1022 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
1023 maybe_fetch_follow_information(user)
1029 def update_following_count(%User{local: true} = user) do
1030 following_count = FollowingRelationship.following_count(user)
1033 |> follow_information_changeset(%{following_count: following_count})
1037 def set_unread_conversation_count(%User{local: true} = user) do
1038 unread_query = Participation.unread_conversation_count_for_user(user)
1041 |> join(:inner, [u], p in subquery(unread_query))
1043 set: [unread_conversation_count: p.count]
1045 |> where([u], u.id == ^user.id)
1047 |> Repo.update_all([])
1049 {1, [user]} -> set_cache(user)
1054 def set_unread_conversation_count(user), do: {:ok, user}
1056 def increment_unread_conversation_count(conversation, %User{local: true} = user) do
1058 Participation.unread_conversation_count_for_user(user)
1059 |> where([p], p.conversation_id == ^conversation.id)
1062 |> join(:inner, [u], p in subquery(unread_query))
1064 inc: [unread_conversation_count: 1]
1066 |> where([u], u.id == ^user.id)
1067 |> where([u, p], p.count == 0)
1069 |> Repo.update_all([])
1071 {1, [user]} -> set_cache(user)
1076 def increment_unread_conversation_count(_, user), do: {:ok, user}
1078 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
1079 def get_users_from_set(ap_ids, local_only \\ true) do
1080 criteria = %{ap_id: ap_ids, deactivated: false}
1081 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
1083 User.Query.build(criteria)
1087 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
1088 def get_recipients_from_activity(%Activity{recipients: to}) do
1089 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
1093 @spec mute(User.t(), User.t(), boolean()) ::
1094 {:ok, list(UserRelationship.t())} | {:error, String.t()}
1095 def mute(%User{} = muter, %User{} = mutee, notifications? \\ true) do
1096 add_to_mutes(muter, mutee, notifications?)
1099 def unmute(%User{} = muter, %User{} = mutee) do
1100 remove_from_mutes(muter, mutee)
1103 def subscribe(%User{} = subscriber, %User{} = target) do
1104 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
1106 if blocks?(target, subscriber) and deny_follow_blocked do
1107 {:error, "Could not subscribe: #{target.nickname} is blocking you"}
1109 # Note: the relationship is inverse: subscriber acts as relationship target
1110 UserRelationship.create_inverse_subscription(target, subscriber)
1114 def subscribe(%User{} = subscriber, %{ap_id: ap_id}) do
1115 with %User{} = subscribee <- get_cached_by_ap_id(ap_id) do
1116 subscribe(subscriber, subscribee)
1120 def unsubscribe(%User{} = unsubscriber, %User{} = target) do
1121 # Note: the relationship is inverse: subscriber acts as relationship target
1122 UserRelationship.delete_inverse_subscription(target, unsubscriber)
1125 def unsubscribe(%User{} = unsubscriber, %{ap_id: ap_id}) do
1126 with %User{} = user <- get_cached_by_ap_id(ap_id) do
1127 unsubscribe(unsubscriber, user)
1131 def block(%User{} = blocker, %User{} = blocked) do
1132 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
1134 if following?(blocker, blocked) do
1135 {:ok, blocker, _} = unfollow(blocker, blocked)
1141 # clear any requested follows as well
1143 case CommonAPI.reject_follow_request(blocked, blocker) do
1144 {:ok, %User{} = updated_blocked} -> updated_blocked
1148 unsubscribe(blocked, blocker)
1150 if following?(blocked, blocker), do: unfollow(blocked, blocker)
1152 {:ok, blocker} = update_follower_count(blocker)
1153 {:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
1154 add_to_block(blocker, blocked)
1157 # helper to handle the block given only an actor's AP id
1158 def block(%User{} = blocker, %{ap_id: ap_id}) do
1159 block(blocker, get_cached_by_ap_id(ap_id))
1162 def unblock(%User{} = blocker, %User{} = blocked) do
1163 remove_from_block(blocker, blocked)
1166 # helper to handle the block given only an actor's AP id
1167 def unblock(%User{} = blocker, %{ap_id: ap_id}) do
1168 unblock(blocker, get_cached_by_ap_id(ap_id))
1171 def mutes?(nil, _), do: false
1172 def mutes?(%User{} = user, %User{} = target), do: mutes_user?(user, target)
1174 def mutes_user?(%User{} = user, %User{} = target) do
1175 UserRelationship.mute_exists?(user, target)
1178 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
1179 def muted_notifications?(nil, _), do: false
1181 def muted_notifications?(%User{} = user, %User{} = target),
1182 do: UserRelationship.notification_mute_exists?(user, target)
1184 def blocks?(nil, _), do: false
1186 def blocks?(%User{} = user, %User{} = target) do
1187 blocks_user?(user, target) ||
1188 (!User.following?(user, target) && blocks_domain?(user, target))
1191 def blocks_user?(%User{} = user, %User{} = target) do
1192 UserRelationship.block_exists?(user, target)
1195 def blocks_user?(_, _), do: false
1197 def blocks_domain?(%User{} = user, %User{} = target) do
1198 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
1199 %{host: host} = URI.parse(target.ap_id)
1200 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1203 def blocks_domain?(_, _), do: false
1205 def subscribed_to?(%User{} = user, %User{} = target) do
1206 # Note: the relationship is inverse: subscriber acts as relationship target
1207 UserRelationship.inverse_subscription_exists?(target, user)
1210 def subscribed_to?(%User{} = user, %{ap_id: ap_id}) do
1211 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1212 subscribed_to?(user, target)
1217 Returns map of outgoing (blocked, muted etc.) relations' user AP IDs by relation type.
1218 E.g. `outgoing_relations_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}`
1220 @spec outgoing_relations_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())}
1221 def outgoing_relations_ap_ids(_, []), do: %{}
1223 def outgoing_relations_ap_ids(%User{} = user, relationship_types)
1224 when is_list(relationship_types) do
1227 |> assoc(:outgoing_relationships)
1228 |> join(:inner, [user_rel], u in assoc(user_rel, :target))
1229 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1230 |> select([user_rel, u], [user_rel.relationship_type, fragment("array_agg(?)", u.ap_id)])
1231 |> group_by([user_rel, u], user_rel.relationship_type)
1233 |> Enum.into(%{}, fn [k, v] -> {k, v} end)
1238 fn rel_type -> {rel_type, db_result[rel_type] || []} end
1242 def deactivate_async(user, status \\ true) do
1243 BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
1246 def deactivate(user, status \\ true)
1248 def deactivate(users, status) when is_list(users) do
1249 Repo.transaction(fn ->
1250 for user <- users, do: deactivate(user, status)
1254 def deactivate(%User{} = user, status) do
1255 with {:ok, user} <- set_activation_status(user, status) do
1258 |> Enum.filter(& &1.local)
1259 |> Enum.each(fn follower ->
1260 follower |> update_following_count() |> set_cache()
1263 # Only update local user counts, remote will be update during the next pull.
1266 |> Enum.filter(& &1.local)
1267 |> Enum.each(&update_follower_count/1)
1273 def update_notification_settings(%User{} = user, settings) do
1275 |> cast(%{notification_settings: settings}, [])
1276 |> cast_embed(:notification_settings)
1277 |> validate_required([:notification_settings])
1278 |> update_and_set_cache()
1281 def delete(users) when is_list(users) do
1282 for user <- users, do: delete(user)
1285 def delete(%User{} = user) do
1286 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1289 def perform(:force_password_reset, user), do: force_password_reset(user)
1291 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1292 def perform(:delete, %User{} = user) do
1293 {:ok, _user} = ActivityPub.delete(user)
1295 # Remove all relationships
1298 |> Enum.each(fn follower ->
1299 ActivityPub.unfollow(follower, user)
1300 unfollow(follower, user)
1305 |> Enum.each(fn followed ->
1306 ActivityPub.unfollow(user, followed)
1307 unfollow(user, followed)
1310 delete_user_activities(user)
1311 invalidate_cache(user)
1315 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1317 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1318 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1319 when is_list(blocked_identifiers) do
1321 blocked_identifiers,
1322 fn blocked_identifier ->
1323 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1324 {:ok, _user_block} <- block(blocker, blocked),
1325 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1329 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1336 def perform(:follow_import, %User{} = follower, followed_identifiers)
1337 when is_list(followed_identifiers) do
1339 followed_identifiers,
1340 fn followed_identifier ->
1341 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1342 {:ok, follower} <- maybe_direct_follow(follower, followed),
1343 {:ok, _} <- ActivityPub.follow(follower, followed) do
1347 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1354 @spec external_users_query() :: Ecto.Query.t()
1355 def external_users_query do
1363 @spec external_users(keyword()) :: [User.t()]
1364 def external_users(opts \\ []) do
1366 external_users_query()
1367 |> select([u], struct(u, [:id, :ap_id]))
1371 do: where(query, [u], u.id > ^opts[:max_id]),
1376 do: limit(query, ^opts[:limit]),
1382 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1383 BackgroundWorker.enqueue("blocks_import", %{
1384 "blocker_id" => blocker.id,
1385 "blocked_identifiers" => blocked_identifiers
1389 def follow_import(%User{} = follower, followed_identifiers)
1390 when is_list(followed_identifiers) do
1391 BackgroundWorker.enqueue("follow_import", %{
1392 "follower_id" => follower.id,
1393 "followed_identifiers" => followed_identifiers
1397 def delete_user_activities(%User{ap_id: ap_id}) do
1399 |> Activity.Queries.by_actor()
1400 |> RepoStreamer.chunk_stream(50)
1401 |> Stream.each(fn activities -> Enum.each(activities, &delete_activity/1) end)
1405 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1407 |> Object.normalize()
1408 |> ActivityPub.delete()
1411 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1412 object = Object.normalize(activity)
1415 |> get_cached_by_ap_id()
1416 |> ActivityPub.unlike(object)
1419 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1420 object = Object.normalize(activity)
1423 |> get_cached_by_ap_id()
1424 |> ActivityPub.unannounce(object)
1427 defp delete_activity(_activity), do: "Doing nothing"
1429 def html_filter_policy(%User{no_rich_text: true}) do
1430 Pleroma.HTML.Scrubber.TwitterText
1433 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1435 def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
1437 def get_or_fetch_by_ap_id(ap_id) do
1438 user = get_cached_by_ap_id(ap_id)
1440 if !is_nil(user) and !needs_update?(user) do
1443 fetch_by_ap_id(ap_id)
1448 Creates an internal service actor by URI if missing.
1449 Optionally takes nickname for addressing.
1451 @spec get_or_create_service_actor_by_ap_id(String.t(), String.t()) :: User.t() | nil
1452 def get_or_create_service_actor_by_ap_id(uri, nickname) do
1454 case get_cached_by_ap_id(uri) do
1456 with {:error, %{errors: errors}} <- create_service_actor(uri, nickname) do
1457 Logger.error("Cannot create service actor: #{uri}/.\n#{inspect(errors)}")
1461 %User{invisible: false} = user ->
1471 @spec set_invisible(User.t()) :: {:ok, User.t()}
1472 defp set_invisible(user) do
1474 |> change(%{invisible: true})
1475 |> update_and_set_cache()
1478 @spec create_service_actor(String.t(), String.t()) ::
1479 {:ok, User.t()} | {:error, Ecto.Changeset.t()}
1480 defp create_service_actor(uri, nickname) do
1486 follower_address: uri <> "/followers"
1489 |> unique_constraint(:nickname)
1495 def public_key(%{source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}}) do
1498 |> :public_key.pem_decode()
1500 |> :public_key.pem_entry_decode()
1505 def public_key(_), do: {:error, "not found key"}
1507 def get_public_key_for_ap_id(ap_id) do
1508 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1509 {:ok, public_key} <- public_key(user) do
1516 defp blank?(""), do: nil
1517 defp blank?(n), do: n
1519 def insert_or_update_user(data) do
1521 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1522 |> remote_user_creation()
1523 |> Repo.insert(on_conflict: {:replace_all_except, [:id]}, conflict_target: :nickname)
1527 def ap_enabled?(%User{local: true}), do: true
1528 def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled
1529 def ap_enabled?(_), do: false
1531 @doc "Gets or fetch a user by uri or nickname."
1532 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1533 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1534 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1536 # wait a period of time and return newest version of the User structs
1537 # this is because we have synchronous follow APIs and need to simulate them
1538 # with an async handshake
1539 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1540 with %User{} = a <- get_cached_by_id(a.id),
1541 %User{} = b <- get_cached_by_id(b.id) do
1548 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1549 with :ok <- :timer.sleep(timeout),
1550 %User{} = a <- get_cached_by_id(a.id),
1551 %User{} = b <- get_cached_by_id(b.id) do
1558 def parse_bio(bio) when is_binary(bio) and bio != "" do
1560 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1564 def parse_bio(_), do: ""
1566 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1567 # TODO: get profile URLs other than user.ap_id
1568 profile_urls = [user.ap_id]
1571 |> CommonUtils.format_input("text/plain",
1572 mentions_format: :full,
1573 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1578 def parse_bio(_, _), do: ""
1580 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1581 Repo.transaction(fn ->
1582 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1586 def tag(nickname, tags) when is_binary(nickname),
1587 do: tag(get_by_nickname(nickname), tags)
1589 def tag(%User{} = user, tags),
1590 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1592 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1593 Repo.transaction(fn ->
1594 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1598 def untag(nickname, tags) when is_binary(nickname),
1599 do: untag(get_by_nickname(nickname), tags)
1601 def untag(%User{} = user, tags),
1602 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1604 defp update_tags(%User{} = user, new_tags) do
1605 {:ok, updated_user} =
1607 |> change(%{tags: new_tags})
1608 |> update_and_set_cache()
1613 defp normalize_tags(tags) do
1616 |> Enum.map(&String.downcase/1)
1619 defp local_nickname_regex do
1620 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1621 @extended_local_nickname_regex
1623 @strict_local_nickname_regex
1627 def local_nickname(nickname_or_mention) do
1630 |> String.split("@")
1634 def full_nickname(nickname_or_mention),
1635 do: String.trim_leading(nickname_or_mention, "@")
1637 def error_user(ap_id) do
1641 nickname: "erroruser@example.com",
1642 inserted_at: NaiveDateTime.utc_now()
1646 @spec all_superusers() :: [User.t()]
1647 def all_superusers do
1648 User.Query.build(%{super_users: true, local: true, deactivated: false})
1652 def showing_reblogs?(%User{} = user, %User{} = target) do
1653 not UserRelationship.reblog_mute_exists?(user, target)
1657 The function returns a query to get users with no activity for given interval of days.
1658 Inactive users are those who didn't read any notification, or had any activity where
1659 the user is the activity's actor, during `inactivity_threshold` days.
1660 Deactivated users will not appear in this list.
1664 iex> Pleroma.User.list_inactive_users()
1667 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1668 def list_inactive_users_query(inactivity_threshold \\ 7) do
1669 negative_inactivity_threshold = -inactivity_threshold
1670 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1671 # Subqueries are not supported in `where` clauses, join gets too complicated.
1672 has_read_notifications =
1673 from(n in Pleroma.Notification,
1674 where: n.seen == true,
1676 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1679 |> Pleroma.Repo.all()
1681 from(u in Pleroma.User,
1682 left_join: a in Pleroma.Activity,
1683 on: u.ap_id == a.actor,
1684 where: not is_nil(u.nickname),
1685 where: u.deactivated != ^true,
1686 where: u.id not in ^has_read_notifications,
1689 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1690 is_nil(max(a.inserted_at))
1695 Enable or disable email notifications for user
1699 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => false}}, "digest", true)
1700 Pleroma.User{email_notifications: %{"digest" => true}}
1702 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => true}}, "digest", false)
1703 Pleroma.User{email_notifications: %{"digest" => false}}
1705 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1706 {:ok, t()} | {:error, Ecto.Changeset.t()}
1707 def switch_email_notifications(user, type, status) do
1708 User.update_email_notifications(user, %{type => status})
1712 Set `last_digest_emailed_at` value for the user to current time
1714 @spec touch_last_digest_emailed_at(t()) :: t()
1715 def touch_last_digest_emailed_at(user) do
1716 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1718 {:ok, updated_user} =
1720 |> change(%{last_digest_emailed_at: now})
1721 |> update_and_set_cache()
1726 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1727 def toggle_confirmation(%User{} = user) do
1729 |> confirmation_changeset(need_confirmation: !user.confirmation_pending)
1730 |> update_and_set_cache()
1733 @spec toggle_confirmation([User.t()]) :: [{:ok, User.t()} | {:error, Changeset.t()}]
1734 def toggle_confirmation(users) do
1735 Enum.map(users, &toggle_confirmation/1)
1738 def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
1742 def get_mascot(%{mascot: mascot}) when is_nil(mascot) do
1743 # use instance-default
1744 config = Pleroma.Config.get([:assets, :mascots])
1745 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1746 mascot = Keyword.get(config, default_mascot)
1749 "id" => "default-mascot",
1750 "url" => mascot[:url],
1751 "preview_url" => mascot[:url],
1753 "mime_type" => mascot[:mime_type]
1758 def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
1760 def ensure_keys_present(%User{} = user) do
1761 with {:ok, pem} <- Keys.generate_rsa_pem() do
1763 |> cast(%{keys: pem}, [:keys])
1764 |> validate_required([:keys])
1765 |> update_and_set_cache()
1769 def get_ap_ids_by_nicknames(nicknames) do
1771 where: u.nickname in ^nicknames,
1777 defdelegate search(query, opts \\ []), to: User.Search
1779 defp put_password_hash(
1780 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1782 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1785 defp put_password_hash(changeset), do: changeset
1787 def is_internal_user?(%User{nickname: nil}), do: true
1788 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1789 def is_internal_user?(_), do: false
1791 # A hack because user delete activities have a fake id for whatever reason
1792 # TODO: Get rid of this
1793 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
1795 def get_delivered_users_by_object_id(object_id) do
1797 inner_join: delivery in assoc(u, :deliveries),
1798 where: delivery.object_id == ^object_id
1803 def change_email(user, email) do
1805 |> cast(%{email: email}, [:email])
1806 |> validate_required([:email])
1807 |> unique_constraint(:email)
1808 |> validate_format(:email, @email_regex)
1809 |> update_and_set_cache()
1812 # Internal function; public one is `deactivate/2`
1813 defp set_activation_status(user, deactivated) do
1815 |> cast(%{deactivated: deactivated}, [:deactivated])
1816 |> update_and_set_cache()
1819 def update_banner(user, banner) do
1821 |> cast(%{banner: banner}, [:banner])
1822 |> update_and_set_cache()
1825 def update_background(user, background) do
1827 |> cast(%{background: background}, [:background])
1828 |> update_and_set_cache()
1831 def update_source_data(user, source_data) do
1833 |> cast(%{source_data: source_data}, [:source_data])
1834 |> update_and_set_cache()
1837 def roles(%{is_moderator: is_moderator, is_admin: is_admin}) do
1840 moderator: is_moderator
1844 # ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``.
1845 # For example: [{"name": "Pronoun", "value": "she/her"}, …]
1846 def fields(%{fields: nil, source_data: %{"attachment" => attachment}}) do
1847 limit = Pleroma.Config.get([:instance, :max_remote_account_fields], 0)
1850 |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
1851 |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
1855 def fields(%{fields: nil}), do: []
1857 def fields(%{fields: fields}), do: fields
1859 def validate_fields(changeset, remote? \\ false) do
1860 limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
1861 limit = Pleroma.Config.get([:instance, limit_name], 0)
1864 |> validate_length(:fields, max: limit)
1865 |> validate_change(:fields, fn :fields, fields ->
1866 if Enum.all?(fields, &valid_field?/1) do
1874 defp valid_field?(%{"name" => name, "value" => value}) do
1875 name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255)
1876 value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255)
1878 is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
1879 String.length(value) <= value_limit
1882 defp valid_field?(_), do: false
1884 defp truncate_field(%{"name" => name, "value" => value}) do
1886 String.split_at(name, Pleroma.Config.get([:instance, :account_field_name_length], 255))
1889 String.split_at(value, Pleroma.Config.get([:instance, :account_field_value_length], 255))
1891 %{"name" => name, "value" => value}
1894 def admin_api_update(user, params) do
1901 |> update_and_set_cache()
1904 @doc "Signs user out of all applications"
1905 def global_sign_out(user) do
1906 OAuth.Authorization.delete_user_authorizations(user)
1907 OAuth.Token.delete_user_tokens(user)
1910 def mascot_update(user, url) do
1912 |> cast(%{mascot: url}, [:mascot])
1913 |> validate_required([:mascot])
1914 |> update_and_set_cache()
1917 def mastodon_settings_update(user, settings) do
1919 |> cast(%{settings: settings}, [:settings])
1920 |> validate_required([:settings])
1921 |> update_and_set_cache()
1924 @spec confirmation_changeset(User.t(), keyword()) :: Changeset.t()
1925 def confirmation_changeset(user, need_confirmation: need_confirmation?) do
1927 if need_confirmation? do
1929 confirmation_pending: true,
1930 confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
1934 confirmation_pending: false,
1935 confirmation_token: nil
1939 cast(user, params, [:confirmation_pending, :confirmation_token])
1942 def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do
1943 if id not in user.pinned_activities do
1944 max_pinned_statuses = Pleroma.Config.get([:instance, :max_pinned_statuses], 0)
1945 params = %{pinned_activities: user.pinned_activities ++ [id]}
1948 |> cast(params, [:pinned_activities])
1949 |> validate_length(:pinned_activities,
1950 max: max_pinned_statuses,
1951 message: "You have already pinned the maximum number of statuses"
1956 |> update_and_set_cache()
1959 def remove_pinnned_activity(user, %Pleroma.Activity{id: id}) do
1960 params = %{pinned_activities: List.delete(user.pinned_activities, id)}
1963 |> cast(params, [:pinned_activities])
1964 |> update_and_set_cache()
1967 def update_email_notifications(user, settings) do
1968 email_notifications =
1969 user.email_notifications
1970 |> Map.merge(settings)
1971 |> Map.take(["digest"])
1973 params = %{email_notifications: email_notifications}
1974 fields = [:email_notifications]
1977 |> cast(params, fields)
1978 |> validate_required(fields)
1979 |> update_and_set_cache()
1982 defp set_domain_blocks(user, domain_blocks) do
1983 params = %{domain_blocks: domain_blocks}
1986 |> cast(params, [:domain_blocks])
1987 |> validate_required([:domain_blocks])
1988 |> update_and_set_cache()
1991 def block_domain(user, domain_blocked) do
1992 set_domain_blocks(user, Enum.uniq([domain_blocked | user.domain_blocks]))
1995 def unblock_domain(user, domain_blocked) do
1996 set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked))
1999 @spec add_to_block(User.t(), User.t()) ::
2000 {:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()}
2001 defp add_to_block(%User{} = user, %User{} = blocked) do
2002 UserRelationship.create_block(user, blocked)
2005 @spec add_to_block(User.t(), User.t()) ::
2006 {:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()}
2007 defp remove_from_block(%User{} = user, %User{} = blocked) do
2008 UserRelationship.delete_block(user, blocked)
2011 defp add_to_mutes(%User{} = user, %User{} = muted_user, notifications?) do
2012 with {:ok, user_mute} <- UserRelationship.create_mute(user, muted_user),
2013 {:ok, user_notification_mute} <-
2014 (notifications? && UserRelationship.create_notification_mute(user, muted_user)) ||
2016 {:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
2020 defp remove_from_mutes(user, %User{} = muted_user) do
2021 with {:ok, user_mute} <- UserRelationship.delete_mute(user, muted_user),
2022 {:ok, user_notification_mute} <-
2023 UserRelationship.delete_notification_mute(user, muted_user) do
2024 {:ok, [user_mute, user_notification_mute]}
2028 def set_invisible(user, invisible) do
2029 params = %{invisible: invisible}
2032 |> cast(params, [:invisible])
2033 |> validate_required([:invisible])
2034 |> update_and_set_cache()
2037 def sanitize_html(%User{} = user) do
2038 sanitize_html(user, nil)
2041 # User data that mastodon isn't filtering (treated as plaintext):
2044 def sanitize_html(%User{} = user, filter) do
2048 |> Enum.map(fn %{"name" => name, "value" => value} ->
2051 "value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
2056 |> Map.put(:bio, HTML.filter_tags(user.bio, filter))
2057 |> Map.put(:fields, fields)