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)
222 Dumps Flake Id to SQL-compatible format (16-byte UUID).
223 E.g. "9pQtDGXuq4p3VlcJEm" -> <<0, 0, 1, 110, 179, 218, 42, 92, 213, 41, 44, 227, 95, 213, 0, 0>>
225 def binary_id(source_id) when is_binary(source_id) do
226 with {:ok, dumped_id} <- FlakeId.Ecto.CompatType.dump(source_id) do
233 def binary_id(source_ids) when is_list(source_ids) do
234 Enum.map(source_ids, &binary_id/1)
237 def binary_id(%User{} = user), do: binary_id(user.id)
239 @doc "Returns status account"
240 @spec account_status(User.t()) :: account_status()
241 def account_status(%User{deactivated: true}), do: :deactivated
242 def account_status(%User{password_reset_pending: true}), do: :password_reset_pending
244 def account_status(%User{confirmation_pending: true}) do
245 case Config.get([:instance, :account_activation_required]) do
246 true -> :confirmation_pending
251 def account_status(%User{}), do: :active
253 @spec visible_for?(User.t(), User.t() | nil) :: boolean()
254 def visible_for?(user, for_user \\ nil)
256 def visible_for?(%User{invisible: true}, _), do: false
258 def visible_for?(%User{id: user_id}, %User{id: user_id}), do: true
260 def visible_for?(%User{local: local} = user, nil) do
266 if Config.get([:restrict_unauthenticated, :profiles, cfg_key]),
268 else: account_status(user) == :active
271 def visible_for?(%User{} = user, for_user) do
272 account_status(user) == :active || superuser?(for_user)
275 def visible_for?(_, _), do: false
277 @spec superuser?(User.t()) :: boolean()
278 def superuser?(%User{local: true, is_admin: true}), do: true
279 def superuser?(%User{local: true, is_moderator: true}), do: true
280 def superuser?(_), do: false
282 @spec invisible?(User.t()) :: boolean()
283 def invisible?(%User{invisible: true}), do: true
284 def invisible?(_), do: false
286 def avatar_url(user, options \\ []) do
288 %{"url" => [%{"href" => href} | _]} -> href
289 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
293 def banner_url(user, options \\ []) do
295 %{"url" => [%{"href" => href} | _]} -> href
296 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
300 def profile_url(%User{source_data: %{"url" => url}}), do: url
301 def profile_url(%User{ap_id: ap_id}), do: ap_id
302 def profile_url(_), do: nil
304 def ap_id(%User{nickname: nickname}), do: "#{Web.base_url()}/users/#{nickname}"
306 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
307 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
309 @spec ap_following(User.t()) :: Sring.t()
310 def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
311 def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
313 @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
314 def restrict_deactivated(query) do
315 from(u in query, where: u.deactivated != ^true)
318 defdelegate following_count(user), to: FollowingRelationship
320 defp truncate_fields_param(params) do
321 if Map.has_key?(params, :fields) do
322 Map.put(params, :fields, Enum.map(params[:fields], &truncate_field/1))
328 defp truncate_if_exists(params, key, max_length) do
329 if Map.has_key?(params, key) and is_binary(params[key]) do
330 {value, _chopped} = String.split_at(params[key], max_length)
331 Map.put(params, key, value)
337 def remote_user_creation(params) do
338 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
339 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
343 |> truncate_if_exists(:name, name_limit)
344 |> truncate_if_exists(:bio, bio_limit)
345 |> truncate_fields_param()
365 :hide_followers_count,
376 |> validate_required([:name, :ap_id])
377 |> unique_constraint(:nickname)
378 |> validate_format(:nickname, @email_regex)
379 |> validate_length(:bio, max: bio_limit)
380 |> validate_length(:name, max: name_limit)
381 |> validate_fields(true)
383 case params[:source_data] do
384 %{"followers" => followers, "following" => following} ->
386 |> put_change(:follower_address, followers)
387 |> put_change(:following_address, following)
390 followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
391 put_change(changeset, :follower_address, followers)
395 def update_changeset(struct, params \\ %{}) do
396 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
397 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
412 :hide_followers_count,
415 :allow_following_move,
418 :skip_thread_containment,
421 :pleroma_settings_store,
427 |> unique_constraint(:nickname)
428 |> validate_format(:nickname, local_nickname_regex())
429 |> validate_length(:bio, max: bio_limit)
430 |> validate_length(:name, min: 1, max: name_limit)
432 |> put_change_if_present(:bio, &{:ok, parse_bio(&1, struct)})
433 |> put_change_if_present(:avatar, &put_upload(&1, :avatar))
434 |> put_change_if_present(:banner, &put_upload(&1, :banner))
435 |> put_change_if_present(:background, &put_upload(&1, :background))
436 |> put_change_if_present(
437 :pleroma_settings_store,
438 &{:ok, Map.merge(struct.pleroma_settings_store, &1)}
440 |> validate_fields(false)
443 defp put_fields(changeset) do
444 if raw_fields = get_change(changeset, :raw_fields) do
447 |> Enum.filter(fn %{"name" => n} -> n != "" end)
451 |> Enum.map(fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
454 |> put_change(:raw_fields, raw_fields)
455 |> put_change(:fields, fields)
461 defp put_change_if_present(changeset, map_field, value_function) do
462 if value = get_change(changeset, map_field) do
463 with {:ok, new_value} <- value_function.(value) do
464 put_change(changeset, map_field, new_value)
473 defp put_upload(value, type) do
474 with %Plug.Upload{} <- value,
475 {:ok, object} <- ActivityPub.upload(value, type: type) do
480 def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
481 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
482 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
484 params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
486 params = if remote?, do: truncate_fields_param(params), else: params
508 :allow_following_move,
510 :hide_followers_count,
516 |> unique_constraint(:nickname)
517 |> validate_format(:nickname, local_nickname_regex())
518 |> validate_length(:bio, max: bio_limit)
519 |> validate_length(:name, max: name_limit)
520 |> validate_fields(remote?)
523 def update_as_admin_changeset(struct, params) do
525 |> update_changeset(params)
526 |> cast(params, [:email])
527 |> delete_change(:also_known_as)
528 |> unique_constraint(:email)
529 |> validate_format(:email, @email_regex)
532 @spec update_as_admin(%User{}, map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
533 def update_as_admin(user, params) do
534 params = Map.put(params, "password_confirmation", params["password"])
535 changeset = update_as_admin_changeset(user, params)
537 if params["password"] do
538 reset_password(user, changeset, params)
540 User.update_and_set_cache(changeset)
544 def password_update_changeset(struct, params) do
546 |> cast(params, [:password, :password_confirmation])
547 |> validate_required([:password, :password_confirmation])
548 |> validate_confirmation(:password)
549 |> put_password_hash()
550 |> put_change(:password_reset_pending, false)
553 @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
554 def reset_password(%User{} = user, params) do
555 reset_password(user, user, params)
558 def reset_password(%User{id: user_id} = user, struct, params) do
561 |> Multi.update(:user, password_update_changeset(struct, params))
562 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
563 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
565 case Repo.transaction(multi) do
566 {:ok, %{user: user} = _} -> set_cache(user)
567 {:error, _, changeset, _} -> {:error, changeset}
571 def update_password_reset_pending(user, value) do
574 |> put_change(:password_reset_pending, value)
575 |> update_and_set_cache()
578 def force_password_reset_async(user) do
579 BackgroundWorker.enqueue("force_password_reset", %{"user_id" => user.id})
582 @spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
583 def force_password_reset(user), do: update_password_reset_pending(user, true)
585 def register_changeset(struct, params \\ %{}, opts \\ []) do
586 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
587 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
590 if is_nil(opts[:need_confirmation]) do
591 Pleroma.Config.get([:instance, :account_activation_required])
593 opts[:need_confirmation]
597 |> confirmation_changeset(need_confirmation: need_confirmation?)
598 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
599 |> validate_required([:name, :nickname, :password, :password_confirmation])
600 |> validate_confirmation(:password)
601 |> unique_constraint(:email)
602 |> unique_constraint(:nickname)
603 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
604 |> validate_format(:nickname, local_nickname_regex())
605 |> validate_format(:email, @email_regex)
606 |> validate_length(:bio, max: bio_limit)
607 |> validate_length(:name, min: 1, max: name_limit)
608 |> maybe_validate_required_email(opts[:external])
611 |> unique_constraint(:ap_id)
612 |> put_following_and_follower_address()
615 def maybe_validate_required_email(changeset, true), do: changeset
617 def maybe_validate_required_email(changeset, _) do
618 if Pleroma.Config.get([:instance, :account_activation_required]) do
619 validate_required(changeset, [:email])
625 defp put_ap_id(changeset) do
626 ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
627 put_change(changeset, :ap_id, ap_id)
630 defp put_following_and_follower_address(changeset) do
631 followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
634 |> put_change(:follower_address, followers)
637 defp autofollow_users(user) do
638 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
641 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
644 follow_all(user, autofollowed_users)
647 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
648 def register(%Ecto.Changeset{} = changeset) do
649 with {:ok, user} <- Repo.insert(changeset) do
650 post_register_action(user)
654 def post_register_action(%User{} = user) do
655 with {:ok, user} <- autofollow_users(user),
656 {:ok, user} <- set_cache(user),
657 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
658 {:ok, _} <- try_send_confirmation_email(user) do
663 def try_send_confirmation_email(%User{} = user) do
664 if user.confirmation_pending &&
665 Pleroma.Config.get([:instance, :account_activation_required]) do
667 |> Pleroma.Emails.UserEmail.account_confirmation_email()
668 |> Pleroma.Emails.Mailer.deliver_async()
676 def try_send_confirmation_email(users) do
677 Enum.each(users, &try_send_confirmation_email/1)
680 def needs_update?(%User{local: true}), do: false
682 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
684 def needs_update?(%User{local: false} = user) do
685 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
688 def needs_update?(_), do: true
690 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
691 def maybe_direct_follow(%User{} = follower, %User{local: true, locked: true} = followed) do
692 follow(follower, followed, "pending")
695 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
696 follow(follower, followed)
699 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
700 if not ap_enabled?(followed) do
701 follow(follower, followed)
707 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
708 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
709 def follow_all(follower, followeds) do
711 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
712 |> Enum.each(&follow(follower, &1, "accept"))
717 defdelegate following(user), to: FollowingRelationship
719 def follow(%User{} = follower, %User{} = followed, state \\ "accept") do
720 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
723 followed.deactivated ->
724 {:error, "Could not follow user: #{followed.nickname} is deactivated."}
726 deny_follow_blocked and blocks?(followed, follower) ->
727 {:error, "Could not follow user: #{followed.nickname} blocked you."}
730 FollowingRelationship.follow(follower, followed, state)
732 {:ok, _} = update_follower_count(followed)
735 |> update_following_count()
740 def unfollow(%User{ap_id: ap_id}, %User{ap_id: ap_id}) do
741 {:error, "Not subscribed!"}
744 def unfollow(%User{} = follower, %User{} = followed) do
745 case get_follow_state(follower, followed) do
746 state when state in ["accept", "pending"] ->
747 FollowingRelationship.unfollow(follower, followed)
748 {:ok, followed} = update_follower_count(followed)
752 |> update_following_count()
755 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
758 {:error, "Not subscribed!"}
762 defdelegate following?(follower, followed), to: FollowingRelationship
764 def get_follow_state(%User{} = follower, %User{} = following) do
765 following_relationship = FollowingRelationship.get(follower, following)
766 get_follow_state(follower, following, following_relationship)
769 def get_follow_state(
772 following_relationship
774 case {following_relationship, following.local} do
776 case Utils.fetch_latest_follow(follower, following) do
777 %{data: %{"state" => state}} when state in ["pending", "accept"] -> state
781 {%{state: state}, _} ->
789 def locked?(%User{} = user) do
794 Repo.get_by(User, id: id)
797 def get_by_ap_id(ap_id) do
798 Repo.get_by(User, ap_id: ap_id)
801 def get_all_by_ap_id(ap_ids) do
802 from(u in __MODULE__,
803 where: u.ap_id in ^ap_ids
808 def get_all_by_ids(ids) do
809 from(u in __MODULE__, where: u.id in ^ids)
813 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
814 # of the ap_id and the domain and tries to get that user
815 def get_by_guessed_nickname(ap_id) do
816 domain = URI.parse(ap_id).host
817 name = List.last(String.split(ap_id, "/"))
818 nickname = "#{name}@#{domain}"
820 get_cached_by_nickname(nickname)
823 def set_cache({:ok, user}), do: set_cache(user)
824 def set_cache({:error, err}), do: {:error, err}
826 def set_cache(%User{} = user) do
827 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
828 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
832 def update_and_set_cache(struct, params) do
834 |> update_changeset(params)
835 |> update_and_set_cache()
838 def update_and_set_cache(changeset) do
839 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
844 def invalidate_cache(user) do
845 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
846 Cachex.del(:user_cache, "nickname:#{user.nickname}")
849 @spec get_cached_by_ap_id(String.t()) :: User.t() | nil
850 def get_cached_by_ap_id(ap_id) do
851 key = "ap_id:#{ap_id}"
853 with {:ok, nil} <- Cachex.get(:user_cache, key),
854 user when not is_nil(user) <- get_by_ap_id(ap_id),
855 {:ok, true} <- Cachex.put(:user_cache, key, user) do
863 def get_cached_by_id(id) do
867 Cachex.fetch!(:user_cache, key, fn _ ->
871 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
872 {:commit, user.ap_id}
878 get_cached_by_ap_id(ap_id)
881 def get_cached_by_nickname(nickname) do
882 key = "nickname:#{nickname}"
884 Cachex.fetch!(:user_cache, key, fn ->
885 case get_or_fetch_by_nickname(nickname) do
886 {:ok, user} -> {:commit, user}
887 {:error, _error} -> {:ignore, nil}
892 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
893 restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
896 is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
897 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
899 restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
900 get_cached_by_nickname(nickname_or_id)
902 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
903 get_cached_by_nickname(nickname_or_id)
910 def get_by_nickname(nickname) do
911 Repo.get_by(User, nickname: nickname) ||
912 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
913 Repo.get_by(User, nickname: local_nickname(nickname))
917 def get_by_email(email), do: Repo.get_by(User, email: email)
919 def get_by_nickname_or_email(nickname_or_email) do
920 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
923 def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname)
925 def get_or_fetch_by_nickname(nickname) do
926 with %User{} = user <- get_by_nickname(nickname) do
930 with [_nick, _domain] <- String.split(nickname, "@"),
931 {:ok, user} <- fetch_by_nickname(nickname) do
934 _e -> {:error, "not found " <> nickname}
939 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
940 def get_followers_query(%User{} = user, nil) do
941 User.Query.build(%{followers: user, deactivated: false})
944 def get_followers_query(user, page) do
946 |> get_followers_query(nil)
947 |> User.Query.paginate(page, 20)
950 @spec get_followers_query(User.t()) :: Ecto.Query.t()
951 def get_followers_query(user), do: get_followers_query(user, nil)
953 @spec get_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
954 def get_followers(user, page \\ nil) do
956 |> get_followers_query(page)
960 @spec get_external_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
961 def get_external_followers(user, page \\ nil) do
963 |> get_followers_query(page)
964 |> User.Query.build(%{external: true})
968 def get_followers_ids(user, page \\ nil) do
970 |> get_followers_query(page)
975 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
976 def get_friends_query(%User{} = user, nil) do
977 User.Query.build(%{friends: user, deactivated: false})
980 def get_friends_query(user, page) do
982 |> get_friends_query(nil)
983 |> User.Query.paginate(page, 20)
986 @spec get_friends_query(User.t()) :: Ecto.Query.t()
987 def get_friends_query(user), do: get_friends_query(user, nil)
989 def get_friends(user, page \\ nil) do
991 |> get_friends_query(page)
995 def get_friends_ap_ids(user) do
997 |> get_friends_query(nil)
998 |> select([u], u.ap_id)
1002 def get_friends_ids(user, page \\ nil) do
1004 |> get_friends_query(page)
1005 |> select([u], u.id)
1009 defdelegate get_follow_requests(user), to: FollowingRelationship
1011 def increase_note_count(%User{} = user) do
1013 |> where(id: ^user.id)
1014 |> update([u], inc: [note_count: 1])
1016 |> Repo.update_all([])
1018 {1, [user]} -> set_cache(user)
1023 def decrease_note_count(%User{} = user) do
1025 |> where(id: ^user.id)
1028 note_count: fragment("greatest(0, note_count - 1)")
1032 |> Repo.update_all([])
1034 {1, [user]} -> set_cache(user)
1039 def update_note_count(%User{} = user, note_count \\ nil) do
1044 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
1050 |> cast(%{note_count: note_count}, [:note_count])
1051 |> update_and_set_cache()
1054 @spec maybe_fetch_follow_information(User.t()) :: User.t()
1055 def maybe_fetch_follow_information(user) do
1056 with {:ok, user} <- fetch_follow_information(user) do
1060 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
1066 def fetch_follow_information(user) do
1067 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
1069 |> follow_information_changeset(info)
1070 |> update_and_set_cache()
1074 defp follow_information_changeset(user, params) do
1081 :hide_followers_count,
1086 def update_follower_count(%User{} = user) do
1087 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
1088 follower_count_query =
1089 User.Query.build(%{followers: user, deactivated: false})
1090 |> select([u], %{count: count(u.id)})
1093 |> where(id: ^user.id)
1094 |> join(:inner, [u], s in subquery(follower_count_query))
1096 set: [follower_count: s.count]
1099 |> Repo.update_all([])
1101 {1, [user]} -> set_cache(user)
1105 {:ok, maybe_fetch_follow_information(user)}
1109 @spec update_following_count(User.t()) :: User.t()
1110 def update_following_count(%User{local: false} = user) do
1111 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
1112 maybe_fetch_follow_information(user)
1118 def update_following_count(%User{local: true} = user) do
1119 following_count = FollowingRelationship.following_count(user)
1122 |> follow_information_changeset(%{following_count: following_count})
1126 def set_unread_conversation_count(%User{local: true} = user) do
1127 unread_query = Participation.unread_conversation_count_for_user(user)
1130 |> join(:inner, [u], p in subquery(unread_query))
1132 set: [unread_conversation_count: p.count]
1134 |> where([u], u.id == ^user.id)
1136 |> Repo.update_all([])
1138 {1, [user]} -> set_cache(user)
1143 def set_unread_conversation_count(user), do: {:ok, user}
1145 def increment_unread_conversation_count(conversation, %User{local: true} = user) do
1147 Participation.unread_conversation_count_for_user(user)
1148 |> where([p], p.conversation_id == ^conversation.id)
1151 |> join(:inner, [u], p in subquery(unread_query))
1153 inc: [unread_conversation_count: 1]
1155 |> where([u], u.id == ^user.id)
1156 |> where([u, p], p.count == 0)
1158 |> Repo.update_all([])
1160 {1, [user]} -> set_cache(user)
1165 def increment_unread_conversation_count(_, user), do: {:ok, user}
1167 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
1168 def get_users_from_set(ap_ids, local_only \\ true) do
1169 criteria = %{ap_id: ap_ids, deactivated: false}
1170 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
1172 User.Query.build(criteria)
1176 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
1177 def get_recipients_from_activity(%Activity{recipients: to}) do
1178 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
1182 @spec mute(User.t(), User.t(), boolean()) ::
1183 {:ok, list(UserRelationship.t())} | {:error, String.t()}
1184 def mute(%User{} = muter, %User{} = mutee, notifications? \\ true) do
1185 add_to_mutes(muter, mutee, notifications?)
1188 def unmute(%User{} = muter, %User{} = mutee) do
1189 remove_from_mutes(muter, mutee)
1192 def subscribe(%User{} = subscriber, %User{} = target) do
1193 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
1195 if blocks?(target, subscriber) and deny_follow_blocked do
1196 {:error, "Could not subscribe: #{target.nickname} is blocking you"}
1198 # Note: the relationship is inverse: subscriber acts as relationship target
1199 UserRelationship.create_inverse_subscription(target, subscriber)
1203 def subscribe(%User{} = subscriber, %{ap_id: ap_id}) do
1204 with %User{} = subscribee <- get_cached_by_ap_id(ap_id) do
1205 subscribe(subscriber, subscribee)
1209 def unsubscribe(%User{} = unsubscriber, %User{} = target) do
1210 # Note: the relationship is inverse: subscriber acts as relationship target
1211 UserRelationship.delete_inverse_subscription(target, unsubscriber)
1214 def unsubscribe(%User{} = unsubscriber, %{ap_id: ap_id}) do
1215 with %User{} = user <- get_cached_by_ap_id(ap_id) do
1216 unsubscribe(unsubscriber, user)
1220 def block(%User{} = blocker, %User{} = blocked) do
1221 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
1223 if following?(blocker, blocked) do
1224 {:ok, blocker, _} = unfollow(blocker, blocked)
1230 # clear any requested follows as well
1232 case CommonAPI.reject_follow_request(blocked, blocker) do
1233 {:ok, %User{} = updated_blocked} -> updated_blocked
1237 unsubscribe(blocked, blocker)
1239 if following?(blocked, blocker), do: unfollow(blocked, blocker)
1241 {:ok, blocker} = update_follower_count(blocker)
1242 {:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
1243 add_to_block(blocker, blocked)
1246 # helper to handle the block given only an actor's AP id
1247 def block(%User{} = blocker, %{ap_id: ap_id}) do
1248 block(blocker, get_cached_by_ap_id(ap_id))
1251 def unblock(%User{} = blocker, %User{} = blocked) do
1252 remove_from_block(blocker, blocked)
1255 # helper to handle the block given only an actor's AP id
1256 def unblock(%User{} = blocker, %{ap_id: ap_id}) do
1257 unblock(blocker, get_cached_by_ap_id(ap_id))
1260 def mutes?(nil, _), do: false
1261 def mutes?(%User{} = user, %User{} = target), do: mutes_user?(user, target)
1263 def mutes_user?(%User{} = user, %User{} = target) do
1264 UserRelationship.mute_exists?(user, target)
1267 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
1268 def muted_notifications?(nil, _), do: false
1270 def muted_notifications?(%User{} = user, %User{} = target),
1271 do: UserRelationship.notification_mute_exists?(user, target)
1273 def blocks?(nil, _), do: false
1275 def blocks?(%User{} = user, %User{} = target) do
1276 blocks_user?(user, target) ||
1277 (!User.following?(user, target) && blocks_domain?(user, target))
1280 def blocks_user?(%User{} = user, %User{} = target) do
1281 UserRelationship.block_exists?(user, target)
1284 def blocks_user?(_, _), do: false
1286 def blocks_domain?(%User{} = user, %User{} = target) do
1287 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
1288 %{host: host} = URI.parse(target.ap_id)
1289 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1292 def blocks_domain?(_, _), do: false
1294 def subscribed_to?(%User{} = user, %User{} = target) do
1295 # Note: the relationship is inverse: subscriber acts as relationship target
1296 UserRelationship.inverse_subscription_exists?(target, user)
1299 def subscribed_to?(%User{} = user, %{ap_id: ap_id}) do
1300 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1301 subscribed_to?(user, target)
1306 Returns map of outgoing (blocked, muted etc.) relations' user AP IDs by relation type.
1307 E.g. `outgoing_relations_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}`
1309 @spec outgoing_relations_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())}
1310 def outgoing_relations_ap_ids(_, []), do: %{}
1312 def outgoing_relations_ap_ids(%User{} = user, relationship_types)
1313 when is_list(relationship_types) do
1316 |> assoc(:outgoing_relationships)
1317 |> join(:inner, [user_rel], u in assoc(user_rel, :target))
1318 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1319 |> select([user_rel, u], [user_rel.relationship_type, fragment("array_agg(?)", u.ap_id)])
1320 |> group_by([user_rel, u], user_rel.relationship_type)
1322 |> Enum.into(%{}, fn [k, v] -> {k, v} end)
1327 fn rel_type -> {rel_type, db_result[rel_type] || []} end
1331 def deactivate_async(user, status \\ true) do
1332 BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
1335 def deactivate(user, status \\ true)
1337 def deactivate(users, status) when is_list(users) do
1338 Repo.transaction(fn ->
1339 for user <- users, do: deactivate(user, status)
1343 def deactivate(%User{} = user, status) do
1344 with {:ok, user} <- set_activation_status(user, status) do
1347 |> Enum.filter(& &1.local)
1348 |> Enum.each(fn follower ->
1349 follower |> update_following_count() |> set_cache()
1352 # Only update local user counts, remote will be update during the next pull.
1355 |> Enum.filter(& &1.local)
1356 |> Enum.each(&update_follower_count/1)
1362 def update_notification_settings(%User{} = user, settings) do
1364 |> cast(%{notification_settings: settings}, [])
1365 |> cast_embed(:notification_settings)
1366 |> validate_required([:notification_settings])
1367 |> update_and_set_cache()
1370 def delete(users) when is_list(users) do
1371 for user <- users, do: delete(user)
1374 def delete(%User{} = user) do
1375 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1378 def perform(:force_password_reset, user), do: force_password_reset(user)
1380 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1381 def perform(:delete, %User{} = user) do
1382 {:ok, _user} = ActivityPub.delete(user)
1384 # Remove all relationships
1387 |> Enum.each(fn follower ->
1388 ActivityPub.unfollow(follower, user)
1389 unfollow(follower, user)
1394 |> Enum.each(fn followed ->
1395 ActivityPub.unfollow(user, followed)
1396 unfollow(user, followed)
1399 delete_user_activities(user)
1400 invalidate_cache(user)
1404 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1406 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1407 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1408 when is_list(blocked_identifiers) do
1410 blocked_identifiers,
1411 fn blocked_identifier ->
1412 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1413 {:ok, _user_block} <- block(blocker, blocked),
1414 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1418 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1425 def perform(:follow_import, %User{} = follower, followed_identifiers)
1426 when is_list(followed_identifiers) do
1428 followed_identifiers,
1429 fn followed_identifier ->
1430 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1431 {:ok, follower} <- maybe_direct_follow(follower, followed),
1432 {:ok, _} <- ActivityPub.follow(follower, followed) do
1436 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1443 @spec external_users_query() :: Ecto.Query.t()
1444 def external_users_query do
1452 @spec external_users(keyword()) :: [User.t()]
1453 def external_users(opts \\ []) do
1455 external_users_query()
1456 |> select([u], struct(u, [:id, :ap_id]))
1460 do: where(query, [u], u.id > ^opts[:max_id]),
1465 do: limit(query, ^opts[:limit]),
1471 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1472 BackgroundWorker.enqueue("blocks_import", %{
1473 "blocker_id" => blocker.id,
1474 "blocked_identifiers" => blocked_identifiers
1478 def follow_import(%User{} = follower, followed_identifiers)
1479 when is_list(followed_identifiers) do
1480 BackgroundWorker.enqueue("follow_import", %{
1481 "follower_id" => follower.id,
1482 "followed_identifiers" => followed_identifiers
1486 def delete_user_activities(%User{ap_id: ap_id}) do
1488 |> Activity.Queries.by_actor()
1489 |> RepoStreamer.chunk_stream(50)
1490 |> Stream.each(fn activities -> Enum.each(activities, &delete_activity/1) end)
1494 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1496 |> Object.normalize()
1497 |> ActivityPub.delete()
1500 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1501 object = Object.normalize(activity)
1504 |> get_cached_by_ap_id()
1505 |> ActivityPub.unlike(object)
1508 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1509 object = Object.normalize(activity)
1512 |> get_cached_by_ap_id()
1513 |> ActivityPub.unannounce(object)
1516 defp delete_activity(_activity), do: "Doing nothing"
1518 def html_filter_policy(%User{no_rich_text: true}) do
1519 Pleroma.HTML.Scrubber.TwitterText
1522 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1524 def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
1526 def get_or_fetch_by_ap_id(ap_id) do
1527 user = get_cached_by_ap_id(ap_id)
1529 if !is_nil(user) and !needs_update?(user) do
1532 fetch_by_ap_id(ap_id)
1537 Creates an internal service actor by URI if missing.
1538 Optionally takes nickname for addressing.
1540 @spec get_or_create_service_actor_by_ap_id(String.t(), String.t()) :: User.t() | nil
1541 def get_or_create_service_actor_by_ap_id(uri, nickname) do
1543 case get_cached_by_ap_id(uri) do
1545 with {:error, %{errors: errors}} <- create_service_actor(uri, nickname) do
1546 Logger.error("Cannot create service actor: #{uri}/.\n#{inspect(errors)}")
1550 %User{invisible: false} = user ->
1560 @spec set_invisible(User.t()) :: {:ok, User.t()}
1561 defp set_invisible(user) do
1563 |> change(%{invisible: true})
1564 |> update_and_set_cache()
1567 @spec create_service_actor(String.t(), String.t()) ::
1568 {:ok, User.t()} | {:error, Ecto.Changeset.t()}
1569 defp create_service_actor(uri, nickname) do
1575 follower_address: uri <> "/followers"
1578 |> unique_constraint(:nickname)
1584 def public_key(%{source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}}) do
1587 |> :public_key.pem_decode()
1589 |> :public_key.pem_entry_decode()
1594 def public_key(_), do: {:error, "not found key"}
1596 def get_public_key_for_ap_id(ap_id) do
1597 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1598 {:ok, public_key} <- public_key(user) do
1605 defp blank?(""), do: nil
1606 defp blank?(n), do: n
1608 def insert_or_update_user(data) do
1610 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1611 |> remote_user_creation()
1612 |> Repo.insert(on_conflict: {:replace_all_except, [:id]}, conflict_target: :nickname)
1616 def ap_enabled?(%User{local: true}), do: true
1617 def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled
1618 def ap_enabled?(_), do: false
1620 @doc "Gets or fetch a user by uri or nickname."
1621 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1622 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1623 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1625 # wait a period of time and return newest version of the User structs
1626 # this is because we have synchronous follow APIs and need to simulate them
1627 # with an async handshake
1628 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1629 with %User{} = a <- get_cached_by_id(a.id),
1630 %User{} = b <- get_cached_by_id(b.id) do
1637 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1638 with :ok <- :timer.sleep(timeout),
1639 %User{} = a <- get_cached_by_id(a.id),
1640 %User{} = b <- get_cached_by_id(b.id) do
1647 def parse_bio(bio) when is_binary(bio) and bio != "" do
1649 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1653 def parse_bio(_), do: ""
1655 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1656 # TODO: get profile URLs other than user.ap_id
1657 profile_urls = [user.ap_id]
1660 |> CommonUtils.format_input("text/plain",
1661 mentions_format: :full,
1662 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1667 def parse_bio(_, _), do: ""
1669 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1670 Repo.transaction(fn ->
1671 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1675 def tag(nickname, tags) when is_binary(nickname),
1676 do: tag(get_by_nickname(nickname), tags)
1678 def tag(%User{} = user, tags),
1679 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1681 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1682 Repo.transaction(fn ->
1683 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1687 def untag(nickname, tags) when is_binary(nickname),
1688 do: untag(get_by_nickname(nickname), tags)
1690 def untag(%User{} = user, tags),
1691 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1693 defp update_tags(%User{} = user, new_tags) do
1694 {:ok, updated_user} =
1696 |> change(%{tags: new_tags})
1697 |> update_and_set_cache()
1702 defp normalize_tags(tags) do
1705 |> Enum.map(&String.downcase/1)
1708 defp local_nickname_regex do
1709 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1710 @extended_local_nickname_regex
1712 @strict_local_nickname_regex
1716 def local_nickname(nickname_or_mention) do
1719 |> String.split("@")
1723 def full_nickname(nickname_or_mention),
1724 do: String.trim_leading(nickname_or_mention, "@")
1726 def error_user(ap_id) do
1730 nickname: "erroruser@example.com",
1731 inserted_at: NaiveDateTime.utc_now()
1735 @spec all_superusers() :: [User.t()]
1736 def all_superusers do
1737 User.Query.build(%{super_users: true, local: true, deactivated: false})
1741 def muting_reblogs?(%User{} = user, %User{} = target) do
1742 UserRelationship.reblog_mute_exists?(user, target)
1745 def showing_reblogs?(%User{} = user, %User{} = target) do
1746 not muting_reblogs?(user, target)
1750 The function returns a query to get users with no activity for given interval of days.
1751 Inactive users are those who didn't read any notification, or had any activity where
1752 the user is the activity's actor, during `inactivity_threshold` days.
1753 Deactivated users will not appear in this list.
1757 iex> Pleroma.User.list_inactive_users()
1760 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1761 def list_inactive_users_query(inactivity_threshold \\ 7) do
1762 negative_inactivity_threshold = -inactivity_threshold
1763 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1764 # Subqueries are not supported in `where` clauses, join gets too complicated.
1765 has_read_notifications =
1766 from(n in Pleroma.Notification,
1767 where: n.seen == true,
1769 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1772 |> Pleroma.Repo.all()
1774 from(u in Pleroma.User,
1775 left_join: a in Pleroma.Activity,
1776 on: u.ap_id == a.actor,
1777 where: not is_nil(u.nickname),
1778 where: u.deactivated != ^true,
1779 where: u.id not in ^has_read_notifications,
1782 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1783 is_nil(max(a.inserted_at))
1788 Enable or disable email notifications for user
1792 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => false}}, "digest", true)
1793 Pleroma.User{email_notifications: %{"digest" => true}}
1795 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => true}}, "digest", false)
1796 Pleroma.User{email_notifications: %{"digest" => false}}
1798 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1799 {:ok, t()} | {:error, Ecto.Changeset.t()}
1800 def switch_email_notifications(user, type, status) do
1801 User.update_email_notifications(user, %{type => status})
1805 Set `last_digest_emailed_at` value for the user to current time
1807 @spec touch_last_digest_emailed_at(t()) :: t()
1808 def touch_last_digest_emailed_at(user) do
1809 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1811 {:ok, updated_user} =
1813 |> change(%{last_digest_emailed_at: now})
1814 |> update_and_set_cache()
1819 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1820 def toggle_confirmation(%User{} = user) do
1822 |> confirmation_changeset(need_confirmation: !user.confirmation_pending)
1823 |> update_and_set_cache()
1826 @spec toggle_confirmation([User.t()]) :: [{:ok, User.t()} | {:error, Changeset.t()}]
1827 def toggle_confirmation(users) do
1828 Enum.map(users, &toggle_confirmation/1)
1831 def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
1835 def get_mascot(%{mascot: mascot}) when is_nil(mascot) do
1836 # use instance-default
1837 config = Pleroma.Config.get([:assets, :mascots])
1838 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1839 mascot = Keyword.get(config, default_mascot)
1842 "id" => "default-mascot",
1843 "url" => mascot[:url],
1844 "preview_url" => mascot[:url],
1846 "mime_type" => mascot[:mime_type]
1851 def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
1853 def ensure_keys_present(%User{} = user) do
1854 with {:ok, pem} <- Keys.generate_rsa_pem() do
1856 |> cast(%{keys: pem}, [:keys])
1857 |> validate_required([:keys])
1858 |> update_and_set_cache()
1862 def get_ap_ids_by_nicknames(nicknames) do
1864 where: u.nickname in ^nicknames,
1870 defdelegate search(query, opts \\ []), to: User.Search
1872 defp put_password_hash(
1873 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1875 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1878 defp put_password_hash(changeset), do: changeset
1880 def is_internal_user?(%User{nickname: nil}), do: true
1881 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1882 def is_internal_user?(_), do: false
1884 # A hack because user delete activities have a fake id for whatever reason
1885 # TODO: Get rid of this
1886 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
1888 def get_delivered_users_by_object_id(object_id) do
1890 inner_join: delivery in assoc(u, :deliveries),
1891 where: delivery.object_id == ^object_id
1896 def change_email(user, email) do
1898 |> cast(%{email: email}, [:email])
1899 |> validate_required([:email])
1900 |> unique_constraint(:email)
1901 |> validate_format(:email, @email_regex)
1902 |> update_and_set_cache()
1905 # Internal function; public one is `deactivate/2`
1906 defp set_activation_status(user, deactivated) do
1908 |> cast(%{deactivated: deactivated}, [:deactivated])
1909 |> update_and_set_cache()
1912 def update_banner(user, banner) do
1914 |> cast(%{banner: banner}, [:banner])
1915 |> update_and_set_cache()
1918 def update_background(user, background) do
1920 |> cast(%{background: background}, [:background])
1921 |> update_and_set_cache()
1924 def update_source_data(user, source_data) do
1926 |> cast(%{source_data: source_data}, [:source_data])
1927 |> update_and_set_cache()
1930 def roles(%{is_moderator: is_moderator, is_admin: is_admin}) do
1933 moderator: is_moderator
1937 # ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``.
1938 # For example: [{"name": "Pronoun", "value": "she/her"}, …]
1939 def fields(%{fields: nil, source_data: %{"attachment" => attachment}}) do
1940 limit = Pleroma.Config.get([:instance, :max_remote_account_fields], 0)
1943 |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
1944 |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
1948 def fields(%{fields: nil}), do: []
1950 def fields(%{fields: fields}), do: fields
1952 def sanitized_fields(%User{} = user) do
1955 |> Enum.map(fn %{"name" => name, "value" => value} ->
1958 "value" => Pleroma.HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
1963 def validate_fields(changeset, remote? \\ false) do
1964 limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
1965 limit = Pleroma.Config.get([:instance, limit_name], 0)
1968 |> validate_length(:fields, max: limit)
1969 |> validate_change(:fields, fn :fields, fields ->
1970 if Enum.all?(fields, &valid_field?/1) do
1978 defp valid_field?(%{"name" => name, "value" => value}) do
1979 name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255)
1980 value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255)
1982 is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
1983 String.length(value) <= value_limit
1986 defp valid_field?(_), do: false
1988 defp truncate_field(%{"name" => name, "value" => value}) do
1990 String.split_at(name, Pleroma.Config.get([:instance, :account_field_name_length], 255))
1993 String.split_at(value, Pleroma.Config.get([:instance, :account_field_value_length], 255))
1995 %{"name" => name, "value" => value}
1998 def admin_api_update(user, params) do
2005 |> update_and_set_cache()
2008 @doc "Signs user out of all applications"
2009 def global_sign_out(user) do
2010 OAuth.Authorization.delete_user_authorizations(user)
2011 OAuth.Token.delete_user_tokens(user)
2014 def mascot_update(user, url) do
2016 |> cast(%{mascot: url}, [:mascot])
2017 |> validate_required([:mascot])
2018 |> update_and_set_cache()
2021 def mastodon_settings_update(user, settings) do
2023 |> cast(%{settings: settings}, [:settings])
2024 |> validate_required([:settings])
2025 |> update_and_set_cache()
2028 @spec confirmation_changeset(User.t(), keyword()) :: Changeset.t()
2029 def confirmation_changeset(user, need_confirmation: need_confirmation?) do
2031 if need_confirmation? do
2033 confirmation_pending: true,
2034 confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
2038 confirmation_pending: false,
2039 confirmation_token: nil
2043 cast(user, params, [:confirmation_pending, :confirmation_token])
2046 def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do
2047 if id not in user.pinned_activities do
2048 max_pinned_statuses = Pleroma.Config.get([:instance, :max_pinned_statuses], 0)
2049 params = %{pinned_activities: user.pinned_activities ++ [id]}
2052 |> cast(params, [:pinned_activities])
2053 |> validate_length(:pinned_activities,
2054 max: max_pinned_statuses,
2055 message: "You have already pinned the maximum number of statuses"
2060 |> update_and_set_cache()
2063 def remove_pinnned_activity(user, %Pleroma.Activity{id: id}) do
2064 params = %{pinned_activities: List.delete(user.pinned_activities, id)}
2067 |> cast(params, [:pinned_activities])
2068 |> update_and_set_cache()
2071 def update_email_notifications(user, settings) do
2072 email_notifications =
2073 user.email_notifications
2074 |> Map.merge(settings)
2075 |> Map.take(["digest"])
2077 params = %{email_notifications: email_notifications}
2078 fields = [:email_notifications]
2081 |> cast(params, fields)
2082 |> validate_required(fields)
2083 |> update_and_set_cache()
2086 defp set_domain_blocks(user, domain_blocks) do
2087 params = %{domain_blocks: domain_blocks}
2090 |> cast(params, [:domain_blocks])
2091 |> validate_required([:domain_blocks])
2092 |> update_and_set_cache()
2095 def block_domain(user, domain_blocked) do
2096 set_domain_blocks(user, Enum.uniq([domain_blocked | user.domain_blocks]))
2099 def unblock_domain(user, domain_blocked) do
2100 set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked))
2103 @spec add_to_block(User.t(), User.t()) ::
2104 {:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()}
2105 defp add_to_block(%User{} = user, %User{} = blocked) do
2106 UserRelationship.create_block(user, blocked)
2109 @spec add_to_block(User.t(), User.t()) ::
2110 {:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()}
2111 defp remove_from_block(%User{} = user, %User{} = blocked) do
2112 UserRelationship.delete_block(user, blocked)
2115 defp add_to_mutes(%User{} = user, %User{} = muted_user, notifications?) do
2116 with {:ok, user_mute} <- UserRelationship.create_mute(user, muted_user),
2117 {:ok, user_notification_mute} <-
2118 (notifications? && UserRelationship.create_notification_mute(user, muted_user)) ||
2120 {:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
2124 defp remove_from_mutes(user, %User{} = muted_user) do
2125 with {:ok, user_mute} <- UserRelationship.delete_mute(user, muted_user),
2126 {:ok, user_notification_mute} <-
2127 UserRelationship.delete_notification_mute(user, muted_user) do
2128 {:ok, [user_mute, user_notification_mute]}
2132 def set_invisible(user, invisible) do
2133 params = %{invisible: invisible}
2136 |> cast(params, [:invisible])
2137 |> validate_required([:invisible])
2138 |> update_and_set_cache()
2141 def sanitize_html(%User{} = user) do
2142 sanitize_html(user, nil)
2145 # User data that mastodon isn't filtering (treated as plaintext):
2148 def sanitize_html(%User{} = user, filter) do
2152 |> Enum.map(fn %{"name" => name, "value" => value} ->
2155 "value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
2160 |> Map.put(:bio, HTML.filter_tags(user.bio, filter))
2161 |> Map.put(:fields, fields)