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: user_id}), do: true
242 def visible_for?(%User{local: local} = user, nil) do
248 if Config.get([:restrict_unauthenticated, :profiles, cfg_key]),
250 else: account_status(user) == :active
253 def visible_for?(%User{} = user, for_user) do
254 account_status(user) == :active || superuser?(for_user)
257 def visible_for?(_, _), do: false
259 @spec superuser?(User.t()) :: boolean()
260 def superuser?(%User{local: true, is_admin: true}), do: true
261 def superuser?(%User{local: true, is_moderator: true}), do: true
262 def superuser?(_), do: false
264 @spec invisible?(User.t()) :: boolean()
265 def invisible?(%User{invisible: true}), do: true
266 def invisible?(_), do: false
268 def avatar_url(user, options \\ []) do
270 %{"url" => [%{"href" => href} | _]} -> href
271 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
275 def banner_url(user, options \\ []) do
277 %{"url" => [%{"href" => href} | _]} -> href
278 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
282 def profile_url(%User{source_data: %{"url" => url}}), do: url
283 def profile_url(%User{ap_id: ap_id}), do: ap_id
284 def profile_url(_), do: nil
286 def ap_id(%User{nickname: nickname}), do: "#{Web.base_url()}/users/#{nickname}"
288 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
289 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
291 @spec ap_following(User.t()) :: Sring.t()
292 def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
293 def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
295 @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
296 def restrict_deactivated(query) do
297 from(u in query, where: u.deactivated != ^true)
300 defdelegate following_count(user), to: FollowingRelationship
302 defp truncate_fields_param(params) do
303 if Map.has_key?(params, :fields) do
304 Map.put(params, :fields, Enum.map(params[:fields], &truncate_field/1))
310 defp truncate_if_exists(params, key, max_length) do
311 if Map.has_key?(params, key) and is_binary(params[key]) do
312 {value, _chopped} = String.split_at(params[key], max_length)
313 Map.put(params, key, value)
319 def remote_user_creation(params) do
320 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
321 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
325 |> truncate_if_exists(:name, name_limit)
326 |> truncate_if_exists(:bio, bio_limit)
327 |> truncate_fields_param()
347 :hide_followers_count,
358 |> validate_required([:name, :ap_id])
359 |> unique_constraint(:nickname)
360 |> validate_format(:nickname, @email_regex)
361 |> validate_length(:bio, max: bio_limit)
362 |> validate_length(:name, max: name_limit)
363 |> validate_fields(true)
365 case params[:source_data] do
366 %{"followers" => followers, "following" => following} ->
368 |> put_change(:follower_address, followers)
369 |> put_change(:following_address, following)
372 followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
373 put_change(changeset, :follower_address, followers)
377 def update_changeset(struct, params \\ %{}) do
378 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
379 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
394 :hide_followers_count,
397 :allow_following_move,
400 :skip_thread_containment,
403 :pleroma_settings_store,
409 |> unique_constraint(:nickname)
410 |> validate_format(:nickname, local_nickname_regex())
411 |> validate_length(:bio, max: bio_limit)
412 |> validate_length(:name, min: 1, max: name_limit)
414 |> put_change_if_present(:bio, &{:ok, parse_bio(&1, struct)})
415 |> put_change_if_present(:avatar, &put_upload(&1, :avatar))
416 |> put_change_if_present(:banner, &put_upload(&1, :banner))
417 |> put_change_if_present(:background, &put_upload(&1, :background))
418 |> put_change_if_present(
419 :pleroma_settings_store,
420 &{:ok, Map.merge(struct.pleroma_settings_store, &1)}
422 |> validate_fields(false)
425 defp put_fields(changeset) do
426 if raw_fields = get_change(changeset, :raw_fields) do
429 |> Enum.filter(fn %{"name" => n} -> n != "" end)
433 |> Enum.map(fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
436 |> put_change(:raw_fields, raw_fields)
437 |> put_change(:fields, fields)
443 defp put_change_if_present(changeset, map_field, value_function) do
444 if value = get_change(changeset, map_field) do
445 with {:ok, new_value} <- value_function.(value) do
446 put_change(changeset, map_field, new_value)
455 defp put_upload(value, type) do
456 with %Plug.Upload{} <- value,
457 {:ok, object} <- ActivityPub.upload(value, type: type) do
462 def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
463 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
464 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
466 params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
468 params = if remote?, do: truncate_fields_param(params), else: params
490 :allow_following_move,
492 :hide_followers_count,
498 |> unique_constraint(:nickname)
499 |> validate_format(:nickname, local_nickname_regex())
500 |> validate_length(:bio, max: bio_limit)
501 |> validate_length(:name, max: name_limit)
502 |> validate_fields(remote?)
505 def update_as_admin_changeset(struct, params) do
507 |> update_changeset(params)
508 |> cast(params, [:email])
509 |> delete_change(:also_known_as)
510 |> unique_constraint(:email)
511 |> validate_format(:email, @email_regex)
514 @spec update_as_admin(%User{}, map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
515 def update_as_admin(user, params) do
516 params = Map.put(params, "password_confirmation", params["password"])
517 changeset = update_as_admin_changeset(user, params)
519 if params["password"] do
520 reset_password(user, changeset, params)
522 User.update_and_set_cache(changeset)
526 def password_update_changeset(struct, params) do
528 |> cast(params, [:password, :password_confirmation])
529 |> validate_required([:password, :password_confirmation])
530 |> validate_confirmation(:password)
531 |> put_password_hash()
532 |> put_change(:password_reset_pending, false)
535 @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
536 def reset_password(%User{} = user, params) do
537 reset_password(user, user, params)
540 def reset_password(%User{id: user_id} = user, struct, params) do
543 |> Multi.update(:user, password_update_changeset(struct, params))
544 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
545 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
547 case Repo.transaction(multi) do
548 {:ok, %{user: user} = _} -> set_cache(user)
549 {:error, _, changeset, _} -> {:error, changeset}
553 def update_password_reset_pending(user, value) do
556 |> put_change(:password_reset_pending, value)
557 |> update_and_set_cache()
560 def force_password_reset_async(user) do
561 BackgroundWorker.enqueue("force_password_reset", %{"user_id" => user.id})
564 @spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
565 def force_password_reset(user), do: update_password_reset_pending(user, true)
567 def register_changeset(struct, params \\ %{}, opts \\ []) do
568 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
569 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
572 if is_nil(opts[:need_confirmation]) do
573 Pleroma.Config.get([:instance, :account_activation_required])
575 opts[:need_confirmation]
579 |> confirmation_changeset(need_confirmation: need_confirmation?)
580 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
581 |> validate_required([:name, :nickname, :password, :password_confirmation])
582 |> validate_confirmation(:password)
583 |> unique_constraint(:email)
584 |> unique_constraint(:nickname)
585 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
586 |> validate_format(:nickname, local_nickname_regex())
587 |> validate_format(:email, @email_regex)
588 |> validate_length(:bio, max: bio_limit)
589 |> validate_length(:name, min: 1, max: name_limit)
590 |> maybe_validate_required_email(opts[:external])
593 |> unique_constraint(:ap_id)
594 |> put_following_and_follower_address()
597 def maybe_validate_required_email(changeset, true), do: changeset
599 def maybe_validate_required_email(changeset, _) do
600 if Pleroma.Config.get([:instance, :account_activation_required]) do
601 validate_required(changeset, [:email])
607 defp put_ap_id(changeset) do
608 ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
609 put_change(changeset, :ap_id, ap_id)
612 defp put_following_and_follower_address(changeset) do
613 followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
616 |> put_change(:follower_address, followers)
619 defp autofollow_users(user) do
620 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
623 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
626 follow_all(user, autofollowed_users)
629 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
630 def register(%Ecto.Changeset{} = changeset) do
631 with {:ok, user} <- Repo.insert(changeset) do
632 post_register_action(user)
636 def post_register_action(%User{} = user) do
637 with {:ok, user} <- autofollow_users(user),
638 {:ok, user} <- set_cache(user),
639 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
640 {:ok, _} <- try_send_confirmation_email(user) do
645 def try_send_confirmation_email(%User{} = user) do
646 if user.confirmation_pending &&
647 Pleroma.Config.get([:instance, :account_activation_required]) do
649 |> Pleroma.Emails.UserEmail.account_confirmation_email()
650 |> Pleroma.Emails.Mailer.deliver_async()
658 def try_send_confirmation_email(users) do
659 Enum.each(users, &try_send_confirmation_email/1)
662 def needs_update?(%User{local: true}), do: false
664 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
666 def needs_update?(%User{local: false} = user) do
667 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
670 def needs_update?(_), do: true
672 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
673 def maybe_direct_follow(%User{} = follower, %User{local: true, locked: true} = followed) do
674 follow(follower, followed, "pending")
677 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
678 follow(follower, followed)
681 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
682 if not ap_enabled?(followed) do
683 follow(follower, followed)
689 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
690 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
691 def follow_all(follower, followeds) do
693 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
694 |> Enum.each(&follow(follower, &1, "accept"))
699 defdelegate following(user), to: FollowingRelationship
701 def follow(%User{} = follower, %User{} = followed, state \\ "accept") do
702 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
705 followed.deactivated ->
706 {:error, "Could not follow user: #{followed.nickname} is deactivated."}
708 deny_follow_blocked and blocks?(followed, follower) ->
709 {:error, "Could not follow user: #{followed.nickname} blocked you."}
712 FollowingRelationship.follow(follower, followed, state)
714 {:ok, _} = update_follower_count(followed)
717 |> update_following_count()
722 def unfollow(%User{ap_id: ap_id}, %User{ap_id: ap_id}) do
723 {:error, "Not subscribed!"}
726 def unfollow(%User{} = follower, %User{} = followed) do
727 case get_follow_state(follower, followed) do
728 state when state in ["accept", "pending"] ->
729 FollowingRelationship.unfollow(follower, followed)
730 {:ok, followed} = update_follower_count(followed)
734 |> update_following_count()
737 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
740 {:error, "Not subscribed!"}
744 defdelegate following?(follower, followed), to: FollowingRelationship
746 def get_follow_state(%User{} = follower, %User{} = following) do
747 following_relationship = FollowingRelationship.get(follower, following)
749 case {following_relationship, following.local} do
751 case Utils.fetch_latest_follow(follower, following) do
752 %{data: %{"state" => state}} when state in ["pending", "accept"] -> state
756 {%{state: state}, _} ->
764 def locked?(%User{} = user) do
769 Repo.get_by(User, id: id)
772 def get_by_ap_id(ap_id) do
773 Repo.get_by(User, ap_id: ap_id)
776 def get_all_by_ap_id(ap_ids) do
777 from(u in __MODULE__,
778 where: u.ap_id in ^ap_ids
783 def get_all_by_ids(ids) do
784 from(u in __MODULE__, where: u.id in ^ids)
788 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
789 # of the ap_id and the domain and tries to get that user
790 def get_by_guessed_nickname(ap_id) do
791 domain = URI.parse(ap_id).host
792 name = List.last(String.split(ap_id, "/"))
793 nickname = "#{name}@#{domain}"
795 get_cached_by_nickname(nickname)
798 def set_cache({:ok, user}), do: set_cache(user)
799 def set_cache({:error, err}), do: {:error, err}
801 def set_cache(%User{} = user) do
802 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
803 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
807 def update_and_set_cache(struct, params) do
809 |> update_changeset(params)
810 |> update_and_set_cache()
813 def update_and_set_cache(changeset) do
814 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
819 def invalidate_cache(user) do
820 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
821 Cachex.del(:user_cache, "nickname:#{user.nickname}")
824 @spec get_cached_by_ap_id(String.t()) :: User.t() | nil
825 def get_cached_by_ap_id(ap_id) do
826 key = "ap_id:#{ap_id}"
828 with {:ok, nil} <- Cachex.get(:user_cache, key),
829 user when not is_nil(user) <- get_by_ap_id(ap_id),
830 {:ok, true} <- Cachex.put(:user_cache, key, user) do
838 def get_cached_by_id(id) do
842 Cachex.fetch!(:user_cache, key, fn _ ->
846 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
847 {:commit, user.ap_id}
853 get_cached_by_ap_id(ap_id)
856 def get_cached_by_nickname(nickname) do
857 key = "nickname:#{nickname}"
859 Cachex.fetch!(:user_cache, key, fn ->
860 case get_or_fetch_by_nickname(nickname) do
861 {:ok, user} -> {:commit, user}
862 {:error, _error} -> {:ignore, nil}
867 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
868 restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
871 is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
872 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
874 restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
875 get_cached_by_nickname(nickname_or_id)
877 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
878 get_cached_by_nickname(nickname_or_id)
885 def get_by_nickname(nickname) do
886 Repo.get_by(User, nickname: nickname) ||
887 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
888 Repo.get_by(User, nickname: local_nickname(nickname))
892 def get_by_email(email), do: Repo.get_by(User, email: email)
894 def get_by_nickname_or_email(nickname_or_email) do
895 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
898 def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname)
900 def get_or_fetch_by_nickname(nickname) do
901 with %User{} = user <- get_by_nickname(nickname) do
905 with [_nick, _domain] <- String.split(nickname, "@"),
906 {:ok, user} <- fetch_by_nickname(nickname) do
909 _e -> {:error, "not found " <> nickname}
914 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
915 def get_followers_query(%User{} = user, nil) do
916 User.Query.build(%{followers: user, deactivated: false})
919 def get_followers_query(user, page) do
921 |> get_followers_query(nil)
922 |> User.Query.paginate(page, 20)
925 @spec get_followers_query(User.t()) :: Ecto.Query.t()
926 def get_followers_query(user), do: get_followers_query(user, nil)
928 @spec get_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
929 def get_followers(user, page \\ nil) do
931 |> get_followers_query(page)
935 @spec get_external_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
936 def get_external_followers(user, page \\ nil) do
938 |> get_followers_query(page)
939 |> User.Query.build(%{external: true})
943 def get_followers_ids(user, page \\ nil) do
945 |> get_followers_query(page)
950 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
951 def get_friends_query(%User{} = user, nil) do
952 User.Query.build(%{friends: user, deactivated: false})
955 def get_friends_query(user, page) do
957 |> get_friends_query(nil)
958 |> User.Query.paginate(page, 20)
961 @spec get_friends_query(User.t()) :: Ecto.Query.t()
962 def get_friends_query(user), do: get_friends_query(user, nil)
964 def get_friends(user, page \\ nil) do
966 |> get_friends_query(page)
970 def get_friends_ap_ids(user) do
972 |> get_friends_query(nil)
973 |> select([u], u.ap_id)
977 def get_friends_ids(user, page \\ nil) do
979 |> get_friends_query(page)
984 defdelegate get_follow_requests(user), to: FollowingRelationship
986 def increase_note_count(%User{} = user) do
988 |> where(id: ^user.id)
989 |> update([u], inc: [note_count: 1])
991 |> Repo.update_all([])
993 {1, [user]} -> set_cache(user)
998 def decrease_note_count(%User{} = user) do
1000 |> where(id: ^user.id)
1003 note_count: fragment("greatest(0, note_count - 1)")
1007 |> Repo.update_all([])
1009 {1, [user]} -> set_cache(user)
1014 def update_note_count(%User{} = user, note_count \\ nil) do
1019 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
1025 |> cast(%{note_count: note_count}, [:note_count])
1026 |> update_and_set_cache()
1029 @spec maybe_fetch_follow_information(User.t()) :: User.t()
1030 def maybe_fetch_follow_information(user) do
1031 with {:ok, user} <- fetch_follow_information(user) do
1035 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
1041 def fetch_follow_information(user) do
1042 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
1044 |> follow_information_changeset(info)
1045 |> update_and_set_cache()
1049 defp follow_information_changeset(user, params) do
1056 :hide_followers_count,
1061 def update_follower_count(%User{} = user) do
1062 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
1063 follower_count_query =
1064 User.Query.build(%{followers: user, deactivated: false})
1065 |> select([u], %{count: count(u.id)})
1068 |> where(id: ^user.id)
1069 |> join(:inner, [u], s in subquery(follower_count_query))
1071 set: [follower_count: s.count]
1074 |> Repo.update_all([])
1076 {1, [user]} -> set_cache(user)
1080 {:ok, maybe_fetch_follow_information(user)}
1084 @spec update_following_count(User.t()) :: User.t()
1085 def update_following_count(%User{local: false} = user) do
1086 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
1087 maybe_fetch_follow_information(user)
1093 def update_following_count(%User{local: true} = user) do
1094 following_count = FollowingRelationship.following_count(user)
1097 |> follow_information_changeset(%{following_count: following_count})
1101 def set_unread_conversation_count(%User{local: true} = user) do
1102 unread_query = Participation.unread_conversation_count_for_user(user)
1105 |> join(:inner, [u], p in subquery(unread_query))
1107 set: [unread_conversation_count: p.count]
1109 |> where([u], u.id == ^user.id)
1111 |> Repo.update_all([])
1113 {1, [user]} -> set_cache(user)
1118 def set_unread_conversation_count(user), do: {:ok, user}
1120 def increment_unread_conversation_count(conversation, %User{local: true} = user) do
1122 Participation.unread_conversation_count_for_user(user)
1123 |> where([p], p.conversation_id == ^conversation.id)
1126 |> join(:inner, [u], p in subquery(unread_query))
1128 inc: [unread_conversation_count: 1]
1130 |> where([u], u.id == ^user.id)
1131 |> where([u, p], p.count == 0)
1133 |> Repo.update_all([])
1135 {1, [user]} -> set_cache(user)
1140 def increment_unread_conversation_count(_, user), do: {:ok, user}
1142 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
1143 def get_users_from_set(ap_ids, local_only \\ true) do
1144 criteria = %{ap_id: ap_ids, deactivated: false}
1145 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
1147 User.Query.build(criteria)
1151 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
1152 def get_recipients_from_activity(%Activity{recipients: to}) do
1153 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
1157 @spec mute(User.t(), User.t(), boolean()) ::
1158 {:ok, list(UserRelationship.t())} | {:error, String.t()}
1159 def mute(%User{} = muter, %User{} = mutee, notifications? \\ true) do
1160 add_to_mutes(muter, mutee, notifications?)
1163 def unmute(%User{} = muter, %User{} = mutee) do
1164 remove_from_mutes(muter, mutee)
1167 def subscribe(%User{} = subscriber, %User{} = target) do
1168 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
1170 if blocks?(target, subscriber) and deny_follow_blocked do
1171 {:error, "Could not subscribe: #{target.nickname} is blocking you"}
1173 # Note: the relationship is inverse: subscriber acts as relationship target
1174 UserRelationship.create_inverse_subscription(target, subscriber)
1178 def subscribe(%User{} = subscriber, %{ap_id: ap_id}) do
1179 with %User{} = subscribee <- get_cached_by_ap_id(ap_id) do
1180 subscribe(subscriber, subscribee)
1184 def unsubscribe(%User{} = unsubscriber, %User{} = target) do
1185 # Note: the relationship is inverse: subscriber acts as relationship target
1186 UserRelationship.delete_inverse_subscription(target, unsubscriber)
1189 def unsubscribe(%User{} = unsubscriber, %{ap_id: ap_id}) do
1190 with %User{} = user <- get_cached_by_ap_id(ap_id) do
1191 unsubscribe(unsubscriber, user)
1195 def block(%User{} = blocker, %User{} = blocked) do
1196 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
1198 if following?(blocker, blocked) do
1199 {:ok, blocker, _} = unfollow(blocker, blocked)
1205 # clear any requested follows as well
1207 case CommonAPI.reject_follow_request(blocked, blocker) do
1208 {:ok, %User{} = updated_blocked} -> updated_blocked
1212 unsubscribe(blocked, blocker)
1214 if following?(blocked, blocker), do: unfollow(blocked, blocker)
1216 {:ok, blocker} = update_follower_count(blocker)
1217 {:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
1218 add_to_block(blocker, blocked)
1221 # helper to handle the block given only an actor's AP id
1222 def block(%User{} = blocker, %{ap_id: ap_id}) do
1223 block(blocker, get_cached_by_ap_id(ap_id))
1226 def unblock(%User{} = blocker, %User{} = blocked) do
1227 remove_from_block(blocker, blocked)
1230 # helper to handle the block given only an actor's AP id
1231 def unblock(%User{} = blocker, %{ap_id: ap_id}) do
1232 unblock(blocker, get_cached_by_ap_id(ap_id))
1235 def mutes?(nil, _), do: false
1236 def mutes?(%User{} = user, %User{} = target), do: mutes_user?(user, target)
1238 def mutes_user?(%User{} = user, %User{} = target) do
1239 UserRelationship.mute_exists?(user, target)
1242 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
1243 def muted_notifications?(nil, _), do: false
1245 def muted_notifications?(%User{} = user, %User{} = target),
1246 do: UserRelationship.notification_mute_exists?(user, target)
1248 def blocks?(nil, _), do: false
1250 def blocks?(%User{} = user, %User{} = target) do
1251 blocks_user?(user, target) ||
1252 (!User.following?(user, target) && blocks_domain?(user, target))
1255 def blocks_user?(%User{} = user, %User{} = target) do
1256 UserRelationship.block_exists?(user, target)
1259 def blocks_user?(_, _), do: false
1261 def blocks_domain?(%User{} = user, %User{} = target) do
1262 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
1263 %{host: host} = URI.parse(target.ap_id)
1264 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1267 def blocks_domain?(_, _), do: false
1269 def subscribed_to?(%User{} = user, %User{} = target) do
1270 # Note: the relationship is inverse: subscriber acts as relationship target
1271 UserRelationship.inverse_subscription_exists?(target, user)
1274 def subscribed_to?(%User{} = user, %{ap_id: ap_id}) do
1275 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1276 subscribed_to?(user, target)
1281 Returns map of outgoing (blocked, muted etc.) relations' user AP IDs by relation type.
1282 E.g. `outgoing_relations_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}`
1284 @spec outgoing_relations_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())}
1285 def outgoing_relations_ap_ids(_, []), do: %{}
1287 def outgoing_relations_ap_ids(%User{} = user, relationship_types)
1288 when is_list(relationship_types) do
1291 |> assoc(:outgoing_relationships)
1292 |> join(:inner, [user_rel], u in assoc(user_rel, :target))
1293 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1294 |> select([user_rel, u], [user_rel.relationship_type, fragment("array_agg(?)", u.ap_id)])
1295 |> group_by([user_rel, u], user_rel.relationship_type)
1297 |> Enum.into(%{}, fn [k, v] -> {k, v} end)
1302 fn rel_type -> {rel_type, db_result[rel_type] || []} end
1306 def deactivate_async(user, status \\ true) do
1307 BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
1310 def deactivate(user, status \\ true)
1312 def deactivate(users, status) when is_list(users) do
1313 Repo.transaction(fn ->
1314 for user <- users, do: deactivate(user, status)
1318 def deactivate(%User{} = user, status) do
1319 with {:ok, user} <- set_activation_status(user, status) do
1322 |> Enum.filter(& &1.local)
1323 |> Enum.each(fn follower ->
1324 follower |> update_following_count() |> set_cache()
1327 # Only update local user counts, remote will be update during the next pull.
1330 |> Enum.filter(& &1.local)
1331 |> Enum.each(&update_follower_count/1)
1337 def update_notification_settings(%User{} = user, settings) do
1339 |> cast(%{notification_settings: settings}, [])
1340 |> cast_embed(:notification_settings)
1341 |> validate_required([:notification_settings])
1342 |> update_and_set_cache()
1345 def delete(users) when is_list(users) do
1346 for user <- users, do: delete(user)
1349 def delete(%User{} = user) do
1350 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1353 def perform(:force_password_reset, user), do: force_password_reset(user)
1355 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1356 def perform(:delete, %User{} = user) do
1357 {:ok, _user} = ActivityPub.delete(user)
1359 # Remove all relationships
1362 |> Enum.each(fn follower ->
1363 ActivityPub.unfollow(follower, user)
1364 unfollow(follower, user)
1369 |> Enum.each(fn followed ->
1370 ActivityPub.unfollow(user, followed)
1371 unfollow(user, followed)
1374 delete_user_activities(user)
1375 invalidate_cache(user)
1379 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1381 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1382 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1383 when is_list(blocked_identifiers) do
1385 blocked_identifiers,
1386 fn blocked_identifier ->
1387 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1388 {:ok, _user_block} <- block(blocker, blocked),
1389 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1393 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1400 def perform(:follow_import, %User{} = follower, followed_identifiers)
1401 when is_list(followed_identifiers) do
1403 followed_identifiers,
1404 fn followed_identifier ->
1405 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1406 {:ok, follower} <- maybe_direct_follow(follower, followed),
1407 {:ok, _} <- ActivityPub.follow(follower, followed) do
1411 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1418 @spec external_users_query() :: Ecto.Query.t()
1419 def external_users_query do
1427 @spec external_users(keyword()) :: [User.t()]
1428 def external_users(opts \\ []) do
1430 external_users_query()
1431 |> select([u], struct(u, [:id, :ap_id]))
1435 do: where(query, [u], u.id > ^opts[:max_id]),
1440 do: limit(query, ^opts[:limit]),
1446 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1447 BackgroundWorker.enqueue("blocks_import", %{
1448 "blocker_id" => blocker.id,
1449 "blocked_identifiers" => blocked_identifiers
1453 def follow_import(%User{} = follower, followed_identifiers)
1454 when is_list(followed_identifiers) do
1455 BackgroundWorker.enqueue("follow_import", %{
1456 "follower_id" => follower.id,
1457 "followed_identifiers" => followed_identifiers
1461 def delete_user_activities(%User{ap_id: ap_id}) do
1463 |> Activity.Queries.by_actor()
1464 |> RepoStreamer.chunk_stream(50)
1465 |> Stream.each(fn activities -> Enum.each(activities, &delete_activity/1) end)
1469 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1471 |> Object.normalize()
1472 |> ActivityPub.delete()
1475 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1476 object = Object.normalize(activity)
1479 |> get_cached_by_ap_id()
1480 |> ActivityPub.unlike(object)
1483 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1484 object = Object.normalize(activity)
1487 |> get_cached_by_ap_id()
1488 |> ActivityPub.unannounce(object)
1491 defp delete_activity(_activity), do: "Doing nothing"
1493 def html_filter_policy(%User{no_rich_text: true}) do
1494 Pleroma.HTML.Scrubber.TwitterText
1497 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1499 def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
1501 def get_or_fetch_by_ap_id(ap_id) do
1502 user = get_cached_by_ap_id(ap_id)
1504 if !is_nil(user) and !needs_update?(user) do
1507 fetch_by_ap_id(ap_id)
1512 Creates an internal service actor by URI if missing.
1513 Optionally takes nickname for addressing.
1515 @spec get_or_create_service_actor_by_ap_id(String.t(), String.t()) :: User.t() | nil
1516 def get_or_create_service_actor_by_ap_id(uri, nickname) do
1518 case get_cached_by_ap_id(uri) do
1520 with {:error, %{errors: errors}} <- create_service_actor(uri, nickname) do
1521 Logger.error("Cannot create service actor: #{uri}/.\n#{inspect(errors)}")
1525 %User{invisible: false} = user ->
1535 @spec set_invisible(User.t()) :: {:ok, User.t()}
1536 defp set_invisible(user) do
1538 |> change(%{invisible: true})
1539 |> update_and_set_cache()
1542 @spec create_service_actor(String.t(), String.t()) ::
1543 {:ok, User.t()} | {:error, Ecto.Changeset.t()}
1544 defp create_service_actor(uri, nickname) do
1550 follower_address: uri <> "/followers"
1553 |> unique_constraint(:nickname)
1559 def public_key(%{source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}}) do
1562 |> :public_key.pem_decode()
1564 |> :public_key.pem_entry_decode()
1569 def public_key(_), do: {:error, "not found key"}
1571 def get_public_key_for_ap_id(ap_id) do
1572 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1573 {:ok, public_key} <- public_key(user) do
1580 defp blank?(""), do: nil
1581 defp blank?(n), do: n
1583 def insert_or_update_user(data) do
1585 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1586 |> remote_user_creation()
1587 |> Repo.insert(on_conflict: {:replace_all_except, [:id]}, conflict_target: :nickname)
1591 def ap_enabled?(%User{local: true}), do: true
1592 def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled
1593 def ap_enabled?(_), do: false
1595 @doc "Gets or fetch a user by uri or nickname."
1596 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1597 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1598 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1600 # wait a period of time and return newest version of the User structs
1601 # this is because we have synchronous follow APIs and need to simulate them
1602 # with an async handshake
1603 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1604 with %User{} = a <- get_cached_by_id(a.id),
1605 %User{} = b <- get_cached_by_id(b.id) do
1612 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1613 with :ok <- :timer.sleep(timeout),
1614 %User{} = a <- get_cached_by_id(a.id),
1615 %User{} = b <- get_cached_by_id(b.id) do
1622 def parse_bio(bio) when is_binary(bio) and bio != "" do
1624 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1628 def parse_bio(_), do: ""
1630 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1631 # TODO: get profile URLs other than user.ap_id
1632 profile_urls = [user.ap_id]
1635 |> CommonUtils.format_input("text/plain",
1636 mentions_format: :full,
1637 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1642 def parse_bio(_, _), do: ""
1644 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1645 Repo.transaction(fn ->
1646 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1650 def tag(nickname, tags) when is_binary(nickname),
1651 do: tag(get_by_nickname(nickname), tags)
1653 def tag(%User{} = user, tags),
1654 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1656 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1657 Repo.transaction(fn ->
1658 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1662 def untag(nickname, tags) when is_binary(nickname),
1663 do: untag(get_by_nickname(nickname), tags)
1665 def untag(%User{} = user, tags),
1666 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1668 defp update_tags(%User{} = user, new_tags) do
1669 {:ok, updated_user} =
1671 |> change(%{tags: new_tags})
1672 |> update_and_set_cache()
1677 defp normalize_tags(tags) do
1680 |> Enum.map(&String.downcase/1)
1683 defp local_nickname_regex do
1684 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1685 @extended_local_nickname_regex
1687 @strict_local_nickname_regex
1691 def local_nickname(nickname_or_mention) do
1694 |> String.split("@")
1698 def full_nickname(nickname_or_mention),
1699 do: String.trim_leading(nickname_or_mention, "@")
1701 def error_user(ap_id) do
1705 nickname: "erroruser@example.com",
1706 inserted_at: NaiveDateTime.utc_now()
1710 @spec all_superusers() :: [User.t()]
1711 def all_superusers do
1712 User.Query.build(%{super_users: true, local: true, deactivated: false})
1716 def showing_reblogs?(%User{} = user, %User{} = target) do
1717 not UserRelationship.reblog_mute_exists?(user, target)
1721 The function returns a query to get users with no activity for given interval of days.
1722 Inactive users are those who didn't read any notification, or had any activity where
1723 the user is the activity's actor, during `inactivity_threshold` days.
1724 Deactivated users will not appear in this list.
1728 iex> Pleroma.User.list_inactive_users()
1731 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1732 def list_inactive_users_query(inactivity_threshold \\ 7) do
1733 negative_inactivity_threshold = -inactivity_threshold
1734 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1735 # Subqueries are not supported in `where` clauses, join gets too complicated.
1736 has_read_notifications =
1737 from(n in Pleroma.Notification,
1738 where: n.seen == true,
1740 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1743 |> Pleroma.Repo.all()
1745 from(u in Pleroma.User,
1746 left_join: a in Pleroma.Activity,
1747 on: u.ap_id == a.actor,
1748 where: not is_nil(u.nickname),
1749 where: u.deactivated != ^true,
1750 where: u.id not in ^has_read_notifications,
1753 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1754 is_nil(max(a.inserted_at))
1759 Enable or disable email notifications for user
1763 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => false}}, "digest", true)
1764 Pleroma.User{email_notifications: %{"digest" => true}}
1766 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => true}}, "digest", false)
1767 Pleroma.User{email_notifications: %{"digest" => false}}
1769 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1770 {:ok, t()} | {:error, Ecto.Changeset.t()}
1771 def switch_email_notifications(user, type, status) do
1772 User.update_email_notifications(user, %{type => status})
1776 Set `last_digest_emailed_at` value for the user to current time
1778 @spec touch_last_digest_emailed_at(t()) :: t()
1779 def touch_last_digest_emailed_at(user) do
1780 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1782 {:ok, updated_user} =
1784 |> change(%{last_digest_emailed_at: now})
1785 |> update_and_set_cache()
1790 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1791 def toggle_confirmation(%User{} = user) do
1793 |> confirmation_changeset(need_confirmation: !user.confirmation_pending)
1794 |> update_and_set_cache()
1797 @spec toggle_confirmation([User.t()]) :: [{:ok, User.t()} | {:error, Changeset.t()}]
1798 def toggle_confirmation(users) do
1799 Enum.map(users, &toggle_confirmation/1)
1802 def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
1806 def get_mascot(%{mascot: mascot}) when is_nil(mascot) do
1807 # use instance-default
1808 config = Pleroma.Config.get([:assets, :mascots])
1809 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1810 mascot = Keyword.get(config, default_mascot)
1813 "id" => "default-mascot",
1814 "url" => mascot[:url],
1815 "preview_url" => mascot[:url],
1817 "mime_type" => mascot[:mime_type]
1822 def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
1824 def ensure_keys_present(%User{} = user) do
1825 with {:ok, pem} <- Keys.generate_rsa_pem() do
1827 |> cast(%{keys: pem}, [:keys])
1828 |> validate_required([:keys])
1829 |> update_and_set_cache()
1833 def get_ap_ids_by_nicknames(nicknames) do
1835 where: u.nickname in ^nicknames,
1841 defdelegate search(query, opts \\ []), to: User.Search
1843 defp put_password_hash(
1844 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1846 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1849 defp put_password_hash(changeset), do: changeset
1851 def is_internal_user?(%User{nickname: nil}), do: true
1852 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1853 def is_internal_user?(_), do: false
1855 # A hack because user delete activities have a fake id for whatever reason
1856 # TODO: Get rid of this
1857 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
1859 def get_delivered_users_by_object_id(object_id) do
1861 inner_join: delivery in assoc(u, :deliveries),
1862 where: delivery.object_id == ^object_id
1867 def change_email(user, email) do
1869 |> cast(%{email: email}, [:email])
1870 |> validate_required([:email])
1871 |> unique_constraint(:email)
1872 |> validate_format(:email, @email_regex)
1873 |> update_and_set_cache()
1876 # Internal function; public one is `deactivate/2`
1877 defp set_activation_status(user, deactivated) do
1879 |> cast(%{deactivated: deactivated}, [:deactivated])
1880 |> update_and_set_cache()
1883 def update_banner(user, banner) do
1885 |> cast(%{banner: banner}, [:banner])
1886 |> update_and_set_cache()
1889 def update_background(user, background) do
1891 |> cast(%{background: background}, [:background])
1892 |> update_and_set_cache()
1895 def update_source_data(user, source_data) do
1897 |> cast(%{source_data: source_data}, [:source_data])
1898 |> update_and_set_cache()
1901 def roles(%{is_moderator: is_moderator, is_admin: is_admin}) do
1904 moderator: is_moderator
1908 # ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``.
1909 # For example: [{"name": "Pronoun", "value": "she/her"}, …]
1910 def fields(%{fields: nil, source_data: %{"attachment" => attachment}}) do
1911 limit = Pleroma.Config.get([:instance, :max_remote_account_fields], 0)
1914 |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
1915 |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
1919 def fields(%{fields: nil}), do: []
1921 def fields(%{fields: fields}), do: fields
1923 def sanitized_fields(%User{} = user) do
1926 |> Enum.map(fn %{"name" => name, "value" => value} ->
1929 "value" => Pleroma.HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
1934 def validate_fields(changeset, remote? \\ false) do
1935 limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
1936 limit = Pleroma.Config.get([:instance, limit_name], 0)
1939 |> validate_length(:fields, max: limit)
1940 |> validate_change(:fields, fn :fields, fields ->
1941 if Enum.all?(fields, &valid_field?/1) do
1949 defp valid_field?(%{"name" => name, "value" => value}) do
1950 name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255)
1951 value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255)
1953 is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
1954 String.length(value) <= value_limit
1957 defp valid_field?(_), do: false
1959 defp truncate_field(%{"name" => name, "value" => value}) do
1961 String.split_at(name, Pleroma.Config.get([:instance, :account_field_name_length], 255))
1964 String.split_at(value, Pleroma.Config.get([:instance, :account_field_value_length], 255))
1966 %{"name" => name, "value" => value}
1969 def admin_api_update(user, params) do
1976 |> update_and_set_cache()
1979 @doc "Signs user out of all applications"
1980 def global_sign_out(user) do
1981 OAuth.Authorization.delete_user_authorizations(user)
1982 OAuth.Token.delete_user_tokens(user)
1985 def mascot_update(user, url) do
1987 |> cast(%{mascot: url}, [:mascot])
1988 |> validate_required([:mascot])
1989 |> update_and_set_cache()
1992 def mastodon_settings_update(user, settings) do
1994 |> cast(%{settings: settings}, [:settings])
1995 |> validate_required([:settings])
1996 |> update_and_set_cache()
1999 @spec confirmation_changeset(User.t(), keyword()) :: Changeset.t()
2000 def confirmation_changeset(user, need_confirmation: need_confirmation?) do
2002 if need_confirmation? do
2004 confirmation_pending: true,
2005 confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
2009 confirmation_pending: false,
2010 confirmation_token: nil
2014 cast(user, params, [:confirmation_pending, :confirmation_token])
2017 def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do
2018 if id not in user.pinned_activities do
2019 max_pinned_statuses = Pleroma.Config.get([:instance, :max_pinned_statuses], 0)
2020 params = %{pinned_activities: user.pinned_activities ++ [id]}
2023 |> cast(params, [:pinned_activities])
2024 |> validate_length(:pinned_activities,
2025 max: max_pinned_statuses,
2026 message: "You have already pinned the maximum number of statuses"
2031 |> update_and_set_cache()
2034 def remove_pinnned_activity(user, %Pleroma.Activity{id: id}) do
2035 params = %{pinned_activities: List.delete(user.pinned_activities, id)}
2038 |> cast(params, [:pinned_activities])
2039 |> update_and_set_cache()
2042 def update_email_notifications(user, settings) do
2043 email_notifications =
2044 user.email_notifications
2045 |> Map.merge(settings)
2046 |> Map.take(["digest"])
2048 params = %{email_notifications: email_notifications}
2049 fields = [:email_notifications]
2052 |> cast(params, fields)
2053 |> validate_required(fields)
2054 |> update_and_set_cache()
2057 defp set_domain_blocks(user, domain_blocks) do
2058 params = %{domain_blocks: domain_blocks}
2061 |> cast(params, [:domain_blocks])
2062 |> validate_required([:domain_blocks])
2063 |> update_and_set_cache()
2066 def block_domain(user, domain_blocked) do
2067 set_domain_blocks(user, Enum.uniq([domain_blocked | user.domain_blocks]))
2070 def unblock_domain(user, domain_blocked) do
2071 set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked))
2074 @spec add_to_block(User.t(), User.t()) ::
2075 {:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()}
2076 defp add_to_block(%User{} = user, %User{} = blocked) do
2077 UserRelationship.create_block(user, blocked)
2080 @spec add_to_block(User.t(), User.t()) ::
2081 {:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()}
2082 defp remove_from_block(%User{} = user, %User{} = blocked) do
2083 UserRelationship.delete_block(user, blocked)
2086 defp add_to_mutes(%User{} = user, %User{} = muted_user, notifications?) do
2087 with {:ok, user_mute} <- UserRelationship.create_mute(user, muted_user),
2088 {:ok, user_notification_mute} <-
2089 (notifications? && UserRelationship.create_notification_mute(user, muted_user)) ||
2091 {:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
2095 defp remove_from_mutes(user, %User{} = muted_user) do
2096 with {:ok, user_mute} <- UserRelationship.delete_mute(user, muted_user),
2097 {:ok, user_notification_mute} <-
2098 UserRelationship.delete_notification_mute(user, muted_user) do
2099 {:ok, [user_mute, user_notification_mute]}
2103 def set_invisible(user, invisible) do
2104 params = %{invisible: invisible}
2107 |> cast(params, [:invisible])
2108 |> validate_required([:invisible])
2109 |> update_and_set_cache()
2112 def sanitize_html(%User{} = user) do
2113 sanitize_html(user, nil)
2116 # User data that mastodon isn't filtering (treated as plaintext):
2119 def sanitize_html(%User{} = user, filter) do
2123 |> Enum.map(fn %{"name" => name, "value" => value} ->
2126 "value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
2131 |> Map.put(:bio, HTML.filter_tags(user.bio, filter))
2132 |> Map.put(:fields, fields)