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
10 import Ecto, only: [assoc: 2]
14 alias Pleroma.Activity
16 alias Pleroma.Conversation.Participation
17 alias Pleroma.Delivery
18 alias Pleroma.FollowingRelationship
20 alias Pleroma.Notification
22 alias Pleroma.Registration
24 alias Pleroma.RepoStreamer
26 alias Pleroma.UserRelationship
28 alias Pleroma.Web.ActivityPub.ActivityPub
29 alias Pleroma.Web.ActivityPub.Utils
30 alias Pleroma.Web.CommonAPI
31 alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
32 alias Pleroma.Web.OAuth
33 alias Pleroma.Web.RelMe
34 alias Pleroma.Workers.BackgroundWorker
38 @type t :: %__MODULE__{}
39 @type account_status :: :active | :deactivated | :password_reset_pending | :confirmation_pending
40 @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
42 # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
43 @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])?)*$/
45 @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
46 @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
48 # AP ID user relationships (blocks, mutes etc.)
49 # Format: [rel_type: [outgoing_rel: :outgoing_rel_target, incoming_rel: :incoming_rel_source]]
50 @user_relationships_config [
52 blocker_blocks: :blocked_users,
53 blockee_blocks: :blocker_users
56 muter_mutes: :muted_users,
57 mutee_mutes: :muter_users
60 reblog_muter_mutes: :reblog_muted_users,
61 reblog_mutee_mutes: :reblog_muter_users
64 notification_muter_mutes: :notification_muted_users,
65 notification_mutee_mutes: :notification_muter_users
67 # Note: `inverse_subscription` relationship is inverse: subscriber acts as relationship target
68 inverse_subscription: [
69 subscribee_subscriptions: :subscriber_users,
70 subscriber_subscriptions: :subscribee_users
76 field(:email, :string)
78 field(:nickname, :string)
79 field(:password_hash, :string)
80 field(:password, :string, virtual: true)
81 field(:password_confirmation, :string, virtual: true)
83 field(:ap_id, :string)
85 field(:local, :boolean, default: true)
86 field(:follower_address, :string)
87 field(:following_address, :string)
88 field(:search_rank, :float, virtual: true)
89 field(:search_type, :integer, virtual: true)
90 field(:tags, {:array, :string}, default: [])
91 field(:last_refreshed_at, :naive_datetime_usec)
92 field(:last_digest_emailed_at, :naive_datetime)
93 field(:banner, :map, default: %{})
94 field(:background, :map, default: %{})
95 field(:source_data, :map, default: %{})
96 field(:note_count, :integer, default: 0)
97 field(:follower_count, :integer, default: 0)
98 field(:following_count, :integer, default: 0)
99 field(:locked, :boolean, default: false)
100 field(:confirmation_pending, :boolean, default: false)
101 field(:password_reset_pending, :boolean, default: false)
102 field(:confirmation_token, :string, default: nil)
103 field(:default_scope, :string, default: "public")
104 field(:domain_blocks, {:array, :string}, default: [])
105 field(:deactivated, :boolean, default: false)
106 field(:no_rich_text, :boolean, default: false)
107 field(:ap_enabled, :boolean, default: false)
108 field(:is_moderator, :boolean, default: false)
109 field(:is_admin, :boolean, default: false)
110 field(:show_role, :boolean, default: true)
111 field(:settings, :map, default: nil)
112 field(:magic_key, :string, default: nil)
113 field(:uri, :string, default: nil)
114 field(:hide_followers_count, :boolean, default: false)
115 field(:hide_follows_count, :boolean, default: false)
116 field(:hide_followers, :boolean, default: false)
117 field(:hide_follows, :boolean, default: false)
118 field(:hide_favorites, :boolean, default: true)
119 field(:unread_conversation_count, :integer, default: 0)
120 field(:pinned_activities, {:array, :string}, default: [])
121 field(:email_notifications, :map, default: %{"digest" => false})
122 field(:mascot, :map, default: nil)
123 field(:emoji, {:array, :map}, default: [])
124 field(:pleroma_settings_store, :map, default: %{})
125 field(:fields, {:array, :map}, default: [])
126 field(:raw_fields, {:array, :map}, default: [])
127 field(:discoverable, :boolean, default: false)
128 field(:invisible, :boolean, default: false)
129 field(:allow_following_move, :boolean, default: true)
130 field(:skip_thread_containment, :boolean, default: false)
131 field(:actor_type, :string, default: "Person")
132 field(:also_known_as, {:array, :string}, default: [])
135 :notification_settings,
136 Pleroma.User.NotificationSetting,
140 has_many(:notifications, Notification)
141 has_many(:registrations, Registration)
142 has_many(:deliveries, Delivery)
144 has_many(:outgoing_relationships, UserRelationship, foreign_key: :source_id)
145 has_many(:incoming_relationships, UserRelationship, foreign_key: :target_id)
147 for {relationship_type,
149 {outgoing_relation, outgoing_relation_target},
150 {incoming_relation, incoming_relation_source}
151 ]} <- @user_relationships_config do
152 # Definitions of `has_many :blocker_blocks`, `has_many :muter_mutes` etc.
153 has_many(outgoing_relation, UserRelationship,
154 foreign_key: :source_id,
155 where: [relationship_type: relationship_type]
158 # Definitions of `has_many :blockee_blocks`, `has_many :mutee_mutes` etc.
159 has_many(incoming_relation, UserRelationship,
160 foreign_key: :target_id,
161 where: [relationship_type: relationship_type]
164 # Definitions of `has_many :blocked_users`, `has_many :muted_users` etc.
165 has_many(outgoing_relation_target, through: [outgoing_relation, :target])
167 # Definitions of `has_many :blocker_users`, `has_many :muter_users` etc.
168 has_many(incoming_relation_source, through: [incoming_relation, :source])
171 # `:blocks` is deprecated (replaced with `blocked_users` relation)
172 field(:blocks, {:array, :string}, default: [])
173 # `:mutes` is deprecated (replaced with `muted_users` relation)
174 field(:mutes, {:array, :string}, default: [])
175 # `:muted_reblogs` is deprecated (replaced with `reblog_muted_users` relation)
176 field(:muted_reblogs, {:array, :string}, default: [])
177 # `:muted_notifications` is deprecated (replaced with `notification_muted_users` relation)
178 field(:muted_notifications, {:array, :string}, default: [])
179 # `:subscribers` is deprecated (replaced with `subscriber_users` relation)
180 field(:subscribers, {:array, :string}, default: [])
185 for {_relationship_type, [{_outgoing_relation, outgoing_relation_target}, _]} <-
186 @user_relationships_config do
187 # Definitions of `blocked_users_relation/1`, `muted_users_relation/1`, etc.
188 def unquote(:"#{outgoing_relation_target}_relation")(user, restrict_deactivated? \\ false) do
189 target_users_query = assoc(user, unquote(outgoing_relation_target))
191 if restrict_deactivated? do
192 restrict_deactivated(target_users_query)
198 # Definitions of `blocked_users/1`, `muted_users/1`, etc.
199 def unquote(outgoing_relation_target)(user, restrict_deactivated? \\ false) do
201 |> apply(unquote(:"#{outgoing_relation_target}_relation"), [
203 restrict_deactivated?
208 # Definitions of `blocked_users_ap_ids/1`, `muted_users_ap_ids/1`, etc.
209 def unquote(:"#{outgoing_relation_target}_ap_ids")(user, restrict_deactivated? \\ false) do
211 |> apply(unquote(:"#{outgoing_relation_target}_relation"), [
213 restrict_deactivated?
215 |> select([u], u.ap_id)
220 @doc "Returns status account"
221 @spec account_status(User.t()) :: account_status()
222 def account_status(%User{deactivated: true}), do: :deactivated
223 def account_status(%User{password_reset_pending: true}), do: :password_reset_pending
225 def account_status(%User{confirmation_pending: true}) do
226 case Config.get([:instance, :account_activation_required]) do
227 true -> :confirmation_pending
232 def account_status(%User{}), do: :active
234 @spec visible_for?(User.t(), User.t() | nil) :: boolean()
235 def visible_for?(user, for_user \\ nil)
237 def visible_for?(%User{invisible: true}, _), do: false
239 def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
241 def visible_for?(%User{} = user, for_user) do
242 account_status(user) == :active || superuser?(for_user)
245 def visible_for?(_, _), do: false
247 @spec superuser?(User.t()) :: boolean()
248 def superuser?(%User{local: true, is_admin: true}), do: true
249 def superuser?(%User{local: true, is_moderator: true}), do: true
250 def superuser?(_), do: false
252 @spec invisible?(User.t()) :: boolean()
253 def invisible?(%User{invisible: true}), do: true
254 def invisible?(_), do: false
256 def avatar_url(user, options \\ []) do
258 %{"url" => [%{"href" => href} | _]} -> href
259 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
263 def banner_url(user, options \\ []) do
265 %{"url" => [%{"href" => href} | _]} -> href
266 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
270 def profile_url(%User{source_data: %{"url" => url}}), do: url
271 def profile_url(%User{ap_id: ap_id}), do: ap_id
272 def profile_url(_), do: nil
274 def ap_id(%User{nickname: nickname}), do: "#{Web.base_url()}/users/#{nickname}"
276 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
277 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
279 @spec ap_following(User.t()) :: Sring.t()
280 def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
281 def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
283 def follow_state(%User{} = user, %User{} = target) do
284 case Utils.fetch_latest_follow(user, target) do
285 %{data: %{"state" => state}} -> state
286 # Ideally this would be nil, but then Cachex does not commit the value
291 def get_cached_follow_state(user, target) do
292 key = "follow_state:#{user.ap_id}|#{target.ap_id}"
293 Cachex.fetch!(:user_cache, key, fn _ -> {:commit, follow_state(user, target)} end)
296 @spec set_follow_state_cache(String.t(), String.t(), String.t()) :: {:ok | :error, boolean()}
297 def set_follow_state_cache(user_ap_id, target_ap_id, state) do
298 Cachex.put(:user_cache, "follow_state:#{user_ap_id}|#{target_ap_id}", state)
301 @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
302 def restrict_deactivated(query) do
303 from(u in query, where: u.deactivated != ^true)
306 defdelegate following_count(user), to: FollowingRelationship
308 defp truncate_fields_param(params) do
309 if Map.has_key?(params, :fields) do
310 Map.put(params, :fields, Enum.map(params[:fields], &truncate_field/1))
316 defp truncate_if_exists(params, key, max_length) do
317 if Map.has_key?(params, key) and is_binary(params[key]) do
318 {value, _chopped} = String.split_at(params[key], max_length)
319 Map.put(params, key, value)
325 def remote_user_creation(params) do
326 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
327 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
331 |> truncate_if_exists(:name, name_limit)
332 |> truncate_if_exists(:bio, bio_limit)
333 |> truncate_fields_param()
353 :hide_followers_count,
364 |> validate_required([:name, :ap_id])
365 |> unique_constraint(:nickname)
366 |> validate_format(:nickname, @email_regex)
367 |> validate_length(:bio, max: bio_limit)
368 |> validate_length(:name, max: name_limit)
369 |> validate_fields(true)
371 case params[:source_data] do
372 %{"followers" => followers, "following" => following} ->
374 |> put_change(:follower_address, followers)
375 |> put_change(:following_address, following)
378 followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
379 put_change(changeset, :follower_address, followers)
383 def update_changeset(struct, params \\ %{}) do
384 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
385 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
400 :hide_followers_count,
403 :allow_following_move,
406 :skip_thread_containment,
409 :pleroma_settings_store,
415 |> unique_constraint(:nickname)
416 |> validate_format(:nickname, local_nickname_regex())
417 |> validate_length(:bio, max: bio_limit)
418 |> validate_length(:name, min: 1, max: name_limit)
419 |> validate_fields(false)
422 def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
423 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
424 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
426 params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
428 params = if remote?, do: truncate_fields_param(params), else: params
450 :allow_following_move,
452 :hide_followers_count,
458 |> unique_constraint(:nickname)
459 |> validate_format(:nickname, local_nickname_regex())
460 |> validate_length(:bio, max: bio_limit)
461 |> validate_length(:name, max: name_limit)
462 |> validate_fields(remote?)
465 def password_update_changeset(struct, params) do
467 |> cast(params, [:password, :password_confirmation])
468 |> validate_required([:password, :password_confirmation])
469 |> validate_confirmation(:password)
470 |> put_password_hash()
471 |> put_change(:password_reset_pending, false)
474 @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
475 def reset_password(%User{id: user_id} = user, data) do
478 |> Multi.update(:user, password_update_changeset(user, data))
479 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
480 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
482 case Repo.transaction(multi) do
483 {:ok, %{user: user} = _} -> set_cache(user)
484 {:error, _, changeset, _} -> {:error, changeset}
488 def update_password_reset_pending(user, value) do
491 |> put_change(:password_reset_pending, value)
492 |> update_and_set_cache()
495 def force_password_reset_async(user) do
496 BackgroundWorker.enqueue("force_password_reset", %{"user_id" => user.id})
499 @spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
500 def force_password_reset(user), do: update_password_reset_pending(user, true)
502 def register_changeset(struct, params \\ %{}, opts \\ []) do
503 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
504 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
507 if is_nil(opts[:need_confirmation]) do
508 Pleroma.Config.get([:instance, :account_activation_required])
510 opts[:need_confirmation]
514 |> confirmation_changeset(need_confirmation: need_confirmation?)
515 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
516 |> validate_required([:name, :nickname, :password, :password_confirmation])
517 |> validate_confirmation(:password)
518 |> unique_constraint(:email)
519 |> unique_constraint(:nickname)
520 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
521 |> validate_format(:nickname, local_nickname_regex())
522 |> validate_format(:email, @email_regex)
523 |> validate_length(:bio, max: bio_limit)
524 |> validate_length(:name, min: 1, max: name_limit)
525 |> maybe_validate_required_email(opts[:external])
528 |> unique_constraint(:ap_id)
529 |> put_following_and_follower_address()
532 def maybe_validate_required_email(changeset, true), do: changeset
533 def maybe_validate_required_email(changeset, _), do: validate_required(changeset, [:email])
535 defp put_ap_id(changeset) do
536 ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
537 put_change(changeset, :ap_id, ap_id)
540 defp put_following_and_follower_address(changeset) do
541 followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
544 |> put_change(:follower_address, followers)
547 defp autofollow_users(user) do
548 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
551 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
554 follow_all(user, autofollowed_users)
557 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
558 def register(%Ecto.Changeset{} = changeset) do
559 with {:ok, user} <- Repo.insert(changeset) do
560 post_register_action(user)
564 def post_register_action(%User{} = user) do
565 with {:ok, user} <- autofollow_users(user),
566 {:ok, user} <- set_cache(user),
567 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
568 {:ok, _} <- try_send_confirmation_email(user) do
573 def try_send_confirmation_email(%User{} = user) do
574 if user.confirmation_pending &&
575 Pleroma.Config.get([:instance, :account_activation_required]) do
577 |> Pleroma.Emails.UserEmail.account_confirmation_email()
578 |> Pleroma.Emails.Mailer.deliver_async()
586 def try_send_confirmation_email(users) do
587 Enum.each(users, &try_send_confirmation_email/1)
590 def needs_update?(%User{local: true}), do: false
592 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
594 def needs_update?(%User{local: false} = user) do
595 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
598 def needs_update?(_), do: true
600 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
601 def maybe_direct_follow(%User{} = follower, %User{local: true, locked: true} = followed) do
602 follow(follower, followed, "pending")
605 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
606 follow(follower, followed)
609 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
610 if not ap_enabled?(followed) do
611 follow(follower, followed)
617 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
618 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
619 def follow_all(follower, followeds) do
621 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
622 |> Enum.each(&follow(follower, &1, "accept"))
627 defdelegate following(user), to: FollowingRelationship
629 def follow(%User{} = follower, %User{} = followed, state \\ "accept") do
630 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
633 followed.deactivated ->
634 {:error, "Could not follow user: #{followed.nickname} is deactivated."}
636 deny_follow_blocked and blocks?(followed, follower) ->
637 {:error, "Could not follow user: #{followed.nickname} blocked you."}
640 FollowingRelationship.follow(follower, followed, state)
642 {:ok, _} = update_follower_count(followed)
645 |> update_following_count()
650 def unfollow(%User{} = follower, %User{} = followed) do
651 if following?(follower, followed) and follower.ap_id != followed.ap_id do
652 FollowingRelationship.unfollow(follower, followed)
654 {:ok, followed} = update_follower_count(followed)
658 |> update_following_count()
661 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
663 {:error, "Not subscribed!"}
667 defdelegate following?(follower, followed), to: FollowingRelationship
669 def locked?(%User{} = user) do
674 Repo.get_by(User, id: id)
677 def get_by_ap_id(ap_id) do
678 Repo.get_by(User, ap_id: ap_id)
681 def get_all_by_ap_id(ap_ids) do
682 from(u in __MODULE__,
683 where: u.ap_id in ^ap_ids
688 def get_all_by_ids(ids) do
689 from(u in __MODULE__, where: u.id in ^ids)
693 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
694 # of the ap_id and the domain and tries to get that user
695 def get_by_guessed_nickname(ap_id) do
696 domain = URI.parse(ap_id).host
697 name = List.last(String.split(ap_id, "/"))
698 nickname = "#{name}@#{domain}"
700 get_cached_by_nickname(nickname)
703 def set_cache({:ok, user}), do: set_cache(user)
704 def set_cache({:error, err}), do: {:error, err}
706 def set_cache(%User{} = user) do
707 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
708 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
712 def update_and_set_cache(struct, params) do
714 |> update_changeset(params)
715 |> update_and_set_cache()
718 def update_and_set_cache(changeset) do
719 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
724 def invalidate_cache(user) do
725 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
726 Cachex.del(:user_cache, "nickname:#{user.nickname}")
729 def get_cached_by_ap_id(ap_id) do
730 key = "ap_id:#{ap_id}"
731 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
734 def get_cached_by_id(id) do
738 Cachex.fetch!(:user_cache, key, fn _ ->
742 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
743 {:commit, user.ap_id}
749 get_cached_by_ap_id(ap_id)
752 def get_cached_by_nickname(nickname) do
753 key = "nickname:#{nickname}"
755 Cachex.fetch!(:user_cache, key, fn ->
756 case get_or_fetch_by_nickname(nickname) do
757 {:ok, user} -> {:commit, user}
758 {:error, _error} -> {:ignore, nil}
763 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
764 restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
767 is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
768 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
770 restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
771 get_cached_by_nickname(nickname_or_id)
773 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
774 get_cached_by_nickname(nickname_or_id)
781 def get_by_nickname(nickname) do
782 Repo.get_by(User, nickname: nickname) ||
783 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
784 Repo.get_by(User, nickname: local_nickname(nickname))
788 def get_by_email(email), do: Repo.get_by(User, email: email)
790 def get_by_nickname_or_email(nickname_or_email) do
791 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
794 def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname)
796 def get_or_fetch_by_nickname(nickname) do
797 with %User{} = user <- get_by_nickname(nickname) do
801 with [_nick, _domain] <- String.split(nickname, "@"),
802 {:ok, user} <- fetch_by_nickname(nickname) do
803 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
804 fetch_initial_posts(user)
809 _e -> {:error, "not found " <> nickname}
814 @doc "Fetch some posts when the user has just been federated with"
815 def fetch_initial_posts(user) do
816 BackgroundWorker.enqueue("fetch_initial_posts", %{"user_id" => user.id})
819 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
820 def get_followers_query(%User{} = user, nil) do
821 User.Query.build(%{followers: user, deactivated: false})
824 def get_followers_query(user, page) do
826 |> get_followers_query(nil)
827 |> User.Query.paginate(page, 20)
830 @spec get_followers_query(User.t()) :: Ecto.Query.t()
831 def get_followers_query(user), do: get_followers_query(user, nil)
833 @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
834 def get_followers(user, page \\ nil) do
836 |> get_followers_query(page)
840 @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
841 def get_external_followers(user, page \\ nil) do
843 |> get_followers_query(page)
844 |> User.Query.build(%{external: true})
848 def get_followers_ids(user, page \\ nil) do
850 |> get_followers_query(page)
855 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
856 def get_friends_query(%User{} = user, nil) do
857 User.Query.build(%{friends: user, deactivated: false})
860 def get_friends_query(user, page) do
862 |> get_friends_query(nil)
863 |> User.Query.paginate(page, 20)
866 @spec get_friends_query(User.t()) :: Ecto.Query.t()
867 def get_friends_query(user), do: get_friends_query(user, nil)
869 def get_friends(user, page \\ nil) do
871 |> get_friends_query(page)
875 def get_friends_ap_ids(user) do
877 |> get_friends_query(nil)
878 |> select([u], u.ap_id)
882 def get_friends_ids(user, page \\ nil) do
884 |> get_friends_query(page)
889 defdelegate get_follow_requests(user), to: FollowingRelationship
891 def increase_note_count(%User{} = user) do
893 |> where(id: ^user.id)
894 |> update([u], inc: [note_count: 1])
896 |> Repo.update_all([])
898 {1, [user]} -> set_cache(user)
903 def decrease_note_count(%User{} = user) do
905 |> where(id: ^user.id)
908 note_count: fragment("greatest(0, note_count - 1)")
912 |> Repo.update_all([])
914 {1, [user]} -> set_cache(user)
919 def update_note_count(%User{} = user, note_count \\ nil) do
924 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
930 |> cast(%{note_count: note_count}, [:note_count])
931 |> update_and_set_cache()
934 @spec maybe_fetch_follow_information(User.t()) :: User.t()
935 def maybe_fetch_follow_information(user) do
936 with {:ok, user} <- fetch_follow_information(user) do
940 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
946 def fetch_follow_information(user) do
947 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
949 |> follow_information_changeset(info)
950 |> update_and_set_cache()
954 defp follow_information_changeset(user, params) do
961 :hide_followers_count,
966 def update_follower_count(%User{} = user) do
967 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
968 follower_count_query =
969 User.Query.build(%{followers: user, deactivated: false})
970 |> select([u], %{count: count(u.id)})
973 |> where(id: ^user.id)
974 |> join(:inner, [u], s in subquery(follower_count_query))
976 set: [follower_count: s.count]
979 |> Repo.update_all([])
981 {1, [user]} -> set_cache(user)
985 {:ok, maybe_fetch_follow_information(user)}
989 @spec update_following_count(User.t()) :: User.t()
990 def update_following_count(%User{local: false} = user) do
991 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
992 maybe_fetch_follow_information(user)
998 def update_following_count(%User{local: true} = user) do
999 following_count = FollowingRelationship.following_count(user)
1002 |> follow_information_changeset(%{following_count: following_count})
1006 def set_unread_conversation_count(%User{local: true} = user) do
1007 unread_query = Participation.unread_conversation_count_for_user(user)
1010 |> join(:inner, [u], p in subquery(unread_query))
1012 set: [unread_conversation_count: p.count]
1014 |> where([u], u.id == ^user.id)
1016 |> Repo.update_all([])
1018 {1, [user]} -> set_cache(user)
1023 def set_unread_conversation_count(user), do: {:ok, user}
1025 def increment_unread_conversation_count(conversation, %User{local: true} = user) do
1027 Participation.unread_conversation_count_for_user(user)
1028 |> where([p], p.conversation_id == ^conversation.id)
1031 |> join(:inner, [u], p in subquery(unread_query))
1033 inc: [unread_conversation_count: 1]
1035 |> where([u], u.id == ^user.id)
1036 |> where([u, p], p.count == 0)
1038 |> Repo.update_all([])
1040 {1, [user]} -> set_cache(user)
1045 def increment_unread_conversation_count(_, user), do: {:ok, user}
1047 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
1048 def get_users_from_set(ap_ids, local_only \\ true) do
1049 criteria = %{ap_id: ap_ids, deactivated: false}
1050 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
1052 User.Query.build(criteria)
1056 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
1057 def get_recipients_from_activity(%Activity{recipients: to}) do
1058 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
1062 @spec mute(User.t(), User.t(), boolean()) ::
1063 {:ok, list(UserRelationship.t())} | {:error, String.t()}
1064 def mute(%User{} = muter, %User{} = mutee, notifications? \\ true) do
1065 add_to_mutes(muter, mutee, notifications?)
1068 def unmute(%User{} = muter, %User{} = mutee) do
1069 remove_from_mutes(muter, mutee)
1072 def subscribe(%User{} = subscriber, %User{} = target) do
1073 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
1075 if blocks?(target, subscriber) and deny_follow_blocked do
1076 {:error, "Could not subscribe: #{target.nickname} is blocking you"}
1078 # Note: the relationship is inverse: subscriber acts as relationship target
1079 UserRelationship.create_inverse_subscription(target, subscriber)
1083 def subscribe(%User{} = subscriber, %{ap_id: ap_id}) do
1084 with %User{} = subscribee <- get_cached_by_ap_id(ap_id) do
1085 subscribe(subscriber, subscribee)
1089 def unsubscribe(%User{} = unsubscriber, %User{} = target) do
1090 # Note: the relationship is inverse: subscriber acts as relationship target
1091 UserRelationship.delete_inverse_subscription(target, unsubscriber)
1094 def unsubscribe(%User{} = unsubscriber, %{ap_id: ap_id}) do
1095 with %User{} = user <- get_cached_by_ap_id(ap_id) do
1096 unsubscribe(unsubscriber, user)
1100 def block(%User{} = blocker, %User{} = blocked) do
1101 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
1103 if following?(blocker, blocked) do
1104 {:ok, blocker, _} = unfollow(blocker, blocked)
1110 # clear any requested follows as well
1112 case CommonAPI.reject_follow_request(blocked, blocker) do
1113 {:ok, %User{} = updated_blocked} -> updated_blocked
1117 unsubscribe(blocked, blocker)
1119 if following?(blocked, blocker), do: unfollow(blocked, blocker)
1121 {:ok, blocker} = update_follower_count(blocker)
1122 {:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
1123 add_to_block(blocker, blocked)
1126 # helper to handle the block given only an actor's AP id
1127 def block(%User{} = blocker, %{ap_id: ap_id}) do
1128 block(blocker, get_cached_by_ap_id(ap_id))
1131 def unblock(%User{} = blocker, %User{} = blocked) do
1132 remove_from_block(blocker, blocked)
1135 # helper to handle the block given only an actor's AP id
1136 def unblock(%User{} = blocker, %{ap_id: ap_id}) do
1137 unblock(blocker, get_cached_by_ap_id(ap_id))
1140 def mutes?(nil, _), do: false
1141 def mutes?(%User{} = user, %User{} = target), do: mutes_user?(user, target)
1143 def mutes_user?(%User{} = user, %User{} = target) do
1144 UserRelationship.mute_exists?(user, target)
1147 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
1148 def muted_notifications?(nil, _), do: false
1150 def muted_notifications?(%User{} = user, %User{} = target),
1151 do: UserRelationship.notification_mute_exists?(user, target)
1153 def blocks?(nil, _), do: false
1155 def blocks?(%User{} = user, %User{} = target) do
1156 blocks_user?(user, target) ||
1157 (!User.following?(user, target) && blocks_domain?(user, target))
1160 def blocks_user?(%User{} = user, %User{} = target) do
1161 UserRelationship.block_exists?(user, target)
1164 def blocks_user?(_, _), do: false
1166 def blocks_domain?(%User{} = user, %User{} = target) do
1167 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
1168 %{host: host} = URI.parse(target.ap_id)
1169 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1172 def blocks_domain?(_, _), do: false
1174 def subscribed_to?(%User{} = user, %User{} = target) do
1175 # Note: the relationship is inverse: subscriber acts as relationship target
1176 UserRelationship.inverse_subscription_exists?(target, user)
1179 def subscribed_to?(%User{} = user, %{ap_id: ap_id}) do
1180 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1181 subscribed_to?(user, target)
1186 Returns map of outgoing (blocked, muted etc.) relations' user AP IDs by relation type.
1187 E.g. `outgoing_relations_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}`
1189 @spec outgoing_relations_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())}
1190 def outgoing_relations_ap_ids(_, []), do: %{}
1192 def outgoing_relations_ap_ids(%User{} = user, relationship_types)
1193 when is_list(relationship_types) do
1196 |> assoc(:outgoing_relationships)
1197 |> join(:inner, [user_rel], u in assoc(user_rel, :target))
1198 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1199 |> select([user_rel, u], [user_rel.relationship_type, fragment("array_agg(?)", u.ap_id)])
1200 |> group_by([user_rel, u], user_rel.relationship_type)
1202 |> Enum.into(%{}, fn [k, v] -> {k, v} end)
1207 fn rel_type -> {rel_type, db_result[rel_type] || []} end
1211 def deactivate_async(user, status \\ true) do
1212 BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
1215 def deactivate(user, status \\ true)
1217 def deactivate(users, status) when is_list(users) do
1218 Repo.transaction(fn ->
1219 for user <- users, do: deactivate(user, status)
1223 def deactivate(%User{} = user, status) do
1224 with {:ok, user} <- set_activation_status(user, status) do
1227 |> Enum.filter(& &1.local)
1228 |> Enum.each(fn follower ->
1229 follower |> update_following_count() |> set_cache()
1232 # Only update local user counts, remote will be update during the next pull.
1235 |> Enum.filter(& &1.local)
1236 |> Enum.each(&update_follower_count/1)
1242 def update_notification_settings(%User{} = user, settings) do
1244 |> cast(%{notification_settings: settings}, [])
1245 |> cast_embed(:notification_settings)
1246 |> validate_required([:notification_settings])
1247 |> update_and_set_cache()
1250 def delete(users) when is_list(users) do
1251 for user <- users, do: delete(user)
1254 def delete(%User{} = user) do
1255 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1258 def perform(:force_password_reset, user), do: force_password_reset(user)
1260 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1261 def perform(:delete, %User{} = user) do
1262 {:ok, _user} = ActivityPub.delete(user)
1264 # Remove all relationships
1267 |> Enum.each(fn follower ->
1268 ActivityPub.unfollow(follower, user)
1269 unfollow(follower, user)
1274 |> Enum.each(fn followed ->
1275 ActivityPub.unfollow(user, followed)
1276 unfollow(user, followed)
1279 delete_user_activities(user)
1280 invalidate_cache(user)
1284 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1285 def perform(:fetch_initial_posts, %User{} = user) do
1286 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1288 # Insert all the posts in reverse order, so they're in the right order on the timeline
1289 user.source_data["outbox"]
1290 |> Utils.fetch_ordered_collection(pages)
1292 |> Enum.each(&Pleroma.Web.Federator.incoming_ap_doc/1)
1295 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1297 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1298 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1299 when is_list(blocked_identifiers) do
1301 blocked_identifiers,
1302 fn blocked_identifier ->
1303 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1304 {:ok, _user_block} <- block(blocker, blocked),
1305 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1309 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1316 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1317 def perform(:follow_import, %User{} = follower, followed_identifiers)
1318 when is_list(followed_identifiers) do
1320 followed_identifiers,
1321 fn followed_identifier ->
1322 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1323 {:ok, follower} <- maybe_direct_follow(follower, followed),
1324 {:ok, _} <- ActivityPub.follow(follower, followed) do
1328 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1335 @spec external_users_query() :: Ecto.Query.t()
1336 def external_users_query do
1344 @spec external_users(keyword()) :: [User.t()]
1345 def external_users(opts \\ []) do
1347 external_users_query()
1348 |> select([u], struct(u, [:id, :ap_id]))
1352 do: where(query, [u], u.id > ^opts[:max_id]),
1357 do: limit(query, ^opts[:limit]),
1363 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1364 BackgroundWorker.enqueue("blocks_import", %{
1365 "blocker_id" => blocker.id,
1366 "blocked_identifiers" => blocked_identifiers
1370 def follow_import(%User{} = follower, followed_identifiers)
1371 when is_list(followed_identifiers) do
1372 BackgroundWorker.enqueue("follow_import", %{
1373 "follower_id" => follower.id,
1374 "followed_identifiers" => followed_identifiers
1378 def delete_user_activities(%User{ap_id: ap_id}) do
1380 |> Activity.Queries.by_actor()
1381 |> RepoStreamer.chunk_stream(50)
1382 |> Stream.each(fn activities -> Enum.each(activities, &delete_activity/1) end)
1386 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1388 |> Object.normalize()
1389 |> ActivityPub.delete()
1392 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1393 object = Object.normalize(activity)
1396 |> get_cached_by_ap_id()
1397 |> ActivityPub.unlike(object)
1400 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1401 object = Object.normalize(activity)
1404 |> get_cached_by_ap_id()
1405 |> ActivityPub.unannounce(object)
1408 defp delete_activity(_activity), do: "Doing nothing"
1410 def html_filter_policy(%User{no_rich_text: true}) do
1411 Pleroma.HTML.Scrubber.TwitterText
1414 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1416 def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
1418 def get_or_fetch_by_ap_id(ap_id) do
1419 user = get_cached_by_ap_id(ap_id)
1421 if !is_nil(user) and !needs_update?(user) do
1424 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1425 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1427 resp = fetch_by_ap_id(ap_id)
1429 if should_fetch_initial do
1430 with {:ok, %User{} = user} <- resp do
1431 fetch_initial_posts(user)
1440 Creates an internal service actor by URI if missing.
1441 Optionally takes nickname for addressing.
1443 @spec get_or_create_service_actor_by_ap_id(String.t(), String.t()) :: User.t() | nil
1444 def get_or_create_service_actor_by_ap_id(uri, nickname) do
1446 case get_cached_by_ap_id(uri) do
1448 with {:error, %{errors: errors}} <- create_service_actor(uri, nickname) do
1449 Logger.error("Cannot create service actor: #{uri}/.\n#{inspect(errors)}")
1453 %User{invisible: false} = user ->
1463 @spec set_invisible(User.t()) :: {:ok, User.t()}
1464 defp set_invisible(user) do
1466 |> change(%{invisible: true})
1467 |> update_and_set_cache()
1470 @spec create_service_actor(String.t(), String.t()) ::
1471 {:ok, User.t()} | {:error, Ecto.Changeset.t()}
1472 defp create_service_actor(uri, nickname) do
1478 follower_address: uri <> "/followers"
1481 |> unique_constraint(:nickname)
1487 def public_key(%{source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}}) do
1490 |> :public_key.pem_decode()
1492 |> :public_key.pem_entry_decode()
1497 def public_key(_), do: {:error, "not found key"}
1499 def get_public_key_for_ap_id(ap_id) do
1500 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1501 {:ok, public_key} <- public_key(user) do
1508 defp blank?(""), do: nil
1509 defp blank?(n), do: n
1511 def insert_or_update_user(data) do
1513 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1514 |> remote_user_creation()
1515 |> Repo.insert(on_conflict: {:replace_all_except, [:id]}, conflict_target: :nickname)
1519 def ap_enabled?(%User{local: true}), do: true
1520 def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled
1521 def ap_enabled?(_), do: false
1523 @doc "Gets or fetch a user by uri or nickname."
1524 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1525 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1526 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1528 # wait a period of time and return newest version of the User structs
1529 # this is because we have synchronous follow APIs and need to simulate them
1530 # with an async handshake
1531 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1532 with %User{} = a <- get_cached_by_id(a.id),
1533 %User{} = b <- get_cached_by_id(b.id) do
1540 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1541 with :ok <- :timer.sleep(timeout),
1542 %User{} = a <- get_cached_by_id(a.id),
1543 %User{} = b <- get_cached_by_id(b.id) do
1550 def parse_bio(bio) when is_binary(bio) and bio != "" do
1552 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1556 def parse_bio(_), do: ""
1558 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1559 # TODO: get profile URLs other than user.ap_id
1560 profile_urls = [user.ap_id]
1563 |> CommonUtils.format_input("text/plain",
1564 mentions_format: :full,
1565 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1570 def parse_bio(_, _), do: ""
1572 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1573 Repo.transaction(fn ->
1574 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1578 def tag(nickname, tags) when is_binary(nickname),
1579 do: tag(get_by_nickname(nickname), tags)
1581 def tag(%User{} = user, tags),
1582 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1584 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1585 Repo.transaction(fn ->
1586 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1590 def untag(nickname, tags) when is_binary(nickname),
1591 do: untag(get_by_nickname(nickname), tags)
1593 def untag(%User{} = user, tags),
1594 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1596 defp update_tags(%User{} = user, new_tags) do
1597 {:ok, updated_user} =
1599 |> change(%{tags: new_tags})
1600 |> update_and_set_cache()
1605 defp normalize_tags(tags) do
1608 |> Enum.map(&String.downcase/1)
1611 defp local_nickname_regex do
1612 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1613 @extended_local_nickname_regex
1615 @strict_local_nickname_regex
1619 def local_nickname(nickname_or_mention) do
1622 |> String.split("@")
1626 def full_nickname(nickname_or_mention),
1627 do: String.trim_leading(nickname_or_mention, "@")
1629 def error_user(ap_id) do
1633 nickname: "erroruser@example.com",
1634 inserted_at: NaiveDateTime.utc_now()
1638 @spec all_superusers() :: [User.t()]
1639 def all_superusers do
1640 User.Query.build(%{super_users: true, local: true, deactivated: false})
1644 def showing_reblogs?(%User{} = user, %User{} = target) do
1645 not UserRelationship.reblog_mute_exists?(user, target)
1649 The function returns a query to get users with no activity for given interval of days.
1650 Inactive users are those who didn't read any notification, or had any activity where
1651 the user is the activity's actor, during `inactivity_threshold` days.
1652 Deactivated users will not appear in this list.
1656 iex> Pleroma.User.list_inactive_users()
1659 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1660 def list_inactive_users_query(inactivity_threshold \\ 7) do
1661 negative_inactivity_threshold = -inactivity_threshold
1662 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1663 # Subqueries are not supported in `where` clauses, join gets too complicated.
1664 has_read_notifications =
1665 from(n in Pleroma.Notification,
1666 where: n.seen == true,
1668 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1671 |> Pleroma.Repo.all()
1673 from(u in Pleroma.User,
1674 left_join: a in Pleroma.Activity,
1675 on: u.ap_id == a.actor,
1676 where: not is_nil(u.nickname),
1677 where: u.deactivated != ^true,
1678 where: u.id not in ^has_read_notifications,
1681 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1682 is_nil(max(a.inserted_at))
1687 Enable or disable email notifications for user
1691 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => false}}, "digest", true)
1692 Pleroma.User{email_notifications: %{"digest" => true}}
1694 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => true}}, "digest", false)
1695 Pleroma.User{email_notifications: %{"digest" => false}}
1697 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1698 {:ok, t()} | {:error, Ecto.Changeset.t()}
1699 def switch_email_notifications(user, type, status) do
1700 User.update_email_notifications(user, %{type => status})
1704 Set `last_digest_emailed_at` value for the user to current time
1706 @spec touch_last_digest_emailed_at(t()) :: t()
1707 def touch_last_digest_emailed_at(user) do
1708 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1710 {:ok, updated_user} =
1712 |> change(%{last_digest_emailed_at: now})
1713 |> update_and_set_cache()
1718 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1719 def toggle_confirmation(%User{} = user) do
1721 |> confirmation_changeset(need_confirmation: !user.confirmation_pending)
1722 |> update_and_set_cache()
1725 @spec toggle_confirmation([User.t()]) :: [{:ok, User.t()} | {:error, Changeset.t()}]
1726 def toggle_confirmation(users) do
1727 Enum.map(users, &toggle_confirmation/1)
1730 def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
1734 def get_mascot(%{mascot: mascot}) when is_nil(mascot) do
1735 # use instance-default
1736 config = Pleroma.Config.get([:assets, :mascots])
1737 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1738 mascot = Keyword.get(config, default_mascot)
1741 "id" => "default-mascot",
1742 "url" => mascot[:url],
1743 "preview_url" => mascot[:url],
1745 "mime_type" => mascot[:mime_type]
1750 def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
1752 def ensure_keys_present(%User{} = user) do
1753 with {:ok, pem} <- Keys.generate_rsa_pem() do
1755 |> cast(%{keys: pem}, [:keys])
1756 |> validate_required([:keys])
1757 |> update_and_set_cache()
1761 def get_ap_ids_by_nicknames(nicknames) do
1763 where: u.nickname in ^nicknames,
1769 defdelegate search(query, opts \\ []), to: User.Search
1771 defp put_password_hash(
1772 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1774 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1777 defp put_password_hash(changeset), do: changeset
1779 def is_internal_user?(%User{nickname: nil}), do: true
1780 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1781 def is_internal_user?(_), do: false
1783 # A hack because user delete activities have a fake id for whatever reason
1784 # TODO: Get rid of this
1785 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
1787 def get_delivered_users_by_object_id(object_id) do
1789 inner_join: delivery in assoc(u, :deliveries),
1790 where: delivery.object_id == ^object_id
1795 def change_email(user, email) do
1797 |> cast(%{email: email}, [:email])
1798 |> validate_required([:email])
1799 |> unique_constraint(:email)
1800 |> validate_format(:email, @email_regex)
1801 |> update_and_set_cache()
1804 # Internal function; public one is `deactivate/2`
1805 defp set_activation_status(user, deactivated) do
1807 |> cast(%{deactivated: deactivated}, [:deactivated])
1808 |> update_and_set_cache()
1811 def update_banner(user, banner) do
1813 |> cast(%{banner: banner}, [:banner])
1814 |> update_and_set_cache()
1817 def update_background(user, background) do
1819 |> cast(%{background: background}, [:background])
1820 |> update_and_set_cache()
1823 def update_source_data(user, source_data) do
1825 |> cast(%{source_data: source_data}, [:source_data])
1826 |> update_and_set_cache()
1829 def roles(%{is_moderator: is_moderator, is_admin: is_admin}) do
1832 moderator: is_moderator
1836 # ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``.
1837 # For example: [{"name": "Pronoun", "value": "she/her"}, …]
1838 def fields(%{fields: nil, source_data: %{"attachment" => attachment}}) do
1839 limit = Pleroma.Config.get([:instance, :max_remote_account_fields], 0)
1842 |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
1843 |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
1847 def fields(%{fields: nil}), do: []
1849 def fields(%{fields: fields}), do: fields
1851 def validate_fields(changeset, remote? \\ false) do
1852 limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
1853 limit = Pleroma.Config.get([:instance, limit_name], 0)
1856 |> validate_length(:fields, max: limit)
1857 |> validate_change(:fields, fn :fields, fields ->
1858 if Enum.all?(fields, &valid_field?/1) do
1866 defp valid_field?(%{"name" => name, "value" => value}) do
1867 name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255)
1868 value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255)
1870 is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
1871 String.length(value) <= value_limit
1874 defp valid_field?(_), do: false
1876 defp truncate_field(%{"name" => name, "value" => value}) do
1878 String.split_at(name, Pleroma.Config.get([:instance, :account_field_name_length], 255))
1881 String.split_at(value, Pleroma.Config.get([:instance, :account_field_value_length], 255))
1883 %{"name" => name, "value" => value}
1886 def admin_api_update(user, params) do
1893 |> update_and_set_cache()
1896 @doc "Signs user out of all applications"
1897 def global_sign_out(user) do
1898 OAuth.Authorization.delete_user_authorizations(user)
1899 OAuth.Token.delete_user_tokens(user)
1902 def mascot_update(user, url) do
1904 |> cast(%{mascot: url}, [:mascot])
1905 |> validate_required([:mascot])
1906 |> update_and_set_cache()
1909 def mastodon_settings_update(user, settings) do
1911 |> cast(%{settings: settings}, [:settings])
1912 |> validate_required([:settings])
1913 |> update_and_set_cache()
1916 @spec confirmation_changeset(User.t(), keyword()) :: Changeset.t()
1917 def confirmation_changeset(user, need_confirmation: need_confirmation?) do
1919 if need_confirmation? do
1921 confirmation_pending: true,
1922 confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
1926 confirmation_pending: false,
1927 confirmation_token: nil
1931 cast(user, params, [:confirmation_pending, :confirmation_token])
1934 def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do
1935 if id not in user.pinned_activities do
1936 max_pinned_statuses = Pleroma.Config.get([:instance, :max_pinned_statuses], 0)
1937 params = %{pinned_activities: user.pinned_activities ++ [id]}
1940 |> cast(params, [:pinned_activities])
1941 |> validate_length(:pinned_activities,
1942 max: max_pinned_statuses,
1943 message: "You have already pinned the maximum number of statuses"
1948 |> update_and_set_cache()
1951 def remove_pinnned_activity(user, %Pleroma.Activity{id: id}) do
1952 params = %{pinned_activities: List.delete(user.pinned_activities, id)}
1955 |> cast(params, [:pinned_activities])
1956 |> update_and_set_cache()
1959 def update_email_notifications(user, settings) do
1960 email_notifications =
1961 user.email_notifications
1962 |> Map.merge(settings)
1963 |> Map.take(["digest"])
1965 params = %{email_notifications: email_notifications}
1966 fields = [:email_notifications]
1969 |> cast(params, fields)
1970 |> validate_required(fields)
1971 |> update_and_set_cache()
1974 defp set_domain_blocks(user, domain_blocks) do
1975 params = %{domain_blocks: domain_blocks}
1978 |> cast(params, [:domain_blocks])
1979 |> validate_required([:domain_blocks])
1980 |> update_and_set_cache()
1983 def block_domain(user, domain_blocked) do
1984 set_domain_blocks(user, Enum.uniq([domain_blocked | user.domain_blocks]))
1987 def unblock_domain(user, domain_blocked) do
1988 set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked))
1991 @spec add_to_block(User.t(), User.t()) ::
1992 {:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()}
1993 defp add_to_block(%User{} = user, %User{} = blocked) do
1994 UserRelationship.create_block(user, blocked)
1997 @spec add_to_block(User.t(), User.t()) ::
1998 {:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()}
1999 defp remove_from_block(%User{} = user, %User{} = blocked) do
2000 UserRelationship.delete_block(user, blocked)
2003 defp add_to_mutes(%User{} = user, %User{} = muted_user, notifications?) do
2004 with {:ok, user_mute} <- UserRelationship.create_mute(user, muted_user),
2005 {:ok, user_notification_mute} <-
2006 (notifications? && UserRelationship.create_notification_mute(user, muted_user)) ||
2008 {:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
2012 defp remove_from_mutes(user, %User{} = muted_user) do
2013 with {:ok, user_mute} <- UserRelationship.delete_mute(user, muted_user),
2014 {:ok, user_notification_mute} <-
2015 UserRelationship.delete_notification_mute(user, muted_user) do
2016 {:ok, [user_mute, user_notification_mute]}
2020 def set_invisible(user, invisible) do
2021 params = %{invisible: invisible}
2024 |> cast(params, [:invisible])
2025 |> validate_required([:invisible])
2026 |> update_and_set_cache()