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
19 alias Pleroma.Formatter
22 alias Pleroma.Notification
24 alias Pleroma.Registration
26 alias Pleroma.RepoStreamer
28 alias Pleroma.UserRelationship
30 alias Pleroma.Web.ActivityPub.ActivityPub
31 alias Pleroma.Web.ActivityPub.Utils
32 alias Pleroma.Web.CommonAPI
33 alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
34 alias Pleroma.Web.OAuth
35 alias Pleroma.Web.RelMe
36 alias Pleroma.Workers.BackgroundWorker
40 @type t :: %__MODULE__{}
41 @type account_status :: :active | :deactivated | :password_reset_pending | :confirmation_pending
42 @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
44 # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
45 @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])?)*$/
47 @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
48 @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
50 # AP ID user relationships (blocks, mutes etc.)
51 # Format: [rel_type: [outgoing_rel: :outgoing_rel_target, incoming_rel: :incoming_rel_source]]
52 @user_relationships_config [
54 blocker_blocks: :blocked_users,
55 blockee_blocks: :blocker_users
58 muter_mutes: :muted_users,
59 mutee_mutes: :muter_users
62 reblog_muter_mutes: :reblog_muted_users,
63 reblog_mutee_mutes: :reblog_muter_users
66 notification_muter_mutes: :notification_muted_users,
67 notification_mutee_mutes: :notification_muter_users
69 # Note: `inverse_subscription` relationship is inverse: subscriber acts as relationship target
70 inverse_subscription: [
71 subscribee_subscriptions: :subscriber_users,
72 subscriber_subscriptions: :subscribee_users
78 field(:email, :string)
80 field(:nickname, :string)
81 field(:password_hash, :string)
82 field(:password, :string, virtual: true)
83 field(:password_confirmation, :string, virtual: true)
85 field(:ap_id, :string)
87 field(:local, :boolean, default: true)
88 field(:follower_address, :string)
89 field(:following_address, :string)
90 field(:search_rank, :float, virtual: true)
91 field(:search_type, :integer, virtual: true)
92 field(:tags, {:array, :string}, default: [])
93 field(:last_refreshed_at, :naive_datetime_usec)
94 field(:last_digest_emailed_at, :naive_datetime)
95 field(:banner, :map, default: %{})
96 field(:background, :map, default: %{})
97 field(:source_data, :map, default: %{})
98 field(:note_count, :integer, default: 0)
99 field(:follower_count, :integer, default: 0)
100 field(:following_count, :integer, default: 0)
101 field(:locked, :boolean, default: false)
102 field(:confirmation_pending, :boolean, default: false)
103 field(:password_reset_pending, :boolean, default: false)
104 field(:confirmation_token, :string, default: nil)
105 field(:default_scope, :string, default: "public")
106 field(:domain_blocks, {:array, :string}, default: [])
107 field(:deactivated, :boolean, default: false)
108 field(:no_rich_text, :boolean, default: false)
109 field(:ap_enabled, :boolean, default: false)
110 field(:is_moderator, :boolean, default: false)
111 field(:is_admin, :boolean, default: false)
112 field(:show_role, :boolean, default: true)
113 field(:settings, :map, default: nil)
114 field(:magic_key, :string, default: nil)
115 field(:uri, :string, default: nil)
116 field(:hide_followers_count, :boolean, default: false)
117 field(:hide_follows_count, :boolean, default: false)
118 field(:hide_followers, :boolean, default: false)
119 field(:hide_follows, :boolean, default: false)
120 field(:hide_favorites, :boolean, default: true)
121 field(:unread_conversation_count, :integer, default: 0)
122 field(:pinned_activities, {:array, :string}, default: [])
123 field(:email_notifications, :map, default: %{"digest" => false})
124 field(:mascot, :map, default: nil)
125 field(:emoji, {:array, :map}, default: [])
126 field(:pleroma_settings_store, :map, default: %{})
127 field(:fields, {:array, :map}, default: [])
128 field(:raw_fields, {:array, :map}, default: [])
129 field(:discoverable, :boolean, default: false)
130 field(:invisible, :boolean, default: false)
131 field(:allow_following_move, :boolean, default: true)
132 field(:skip_thread_containment, :boolean, default: false)
133 field(:actor_type, :string, default: "Person")
134 field(:also_known_as, {:array, :string}, default: [])
137 :notification_settings,
138 Pleroma.User.NotificationSetting,
142 has_many(:notifications, Notification)
143 has_many(:registrations, Registration)
144 has_many(:deliveries, Delivery)
146 has_many(:outgoing_relationships, UserRelationship, foreign_key: :source_id)
147 has_many(:incoming_relationships, UserRelationship, foreign_key: :target_id)
149 for {relationship_type,
151 {outgoing_relation, outgoing_relation_target},
152 {incoming_relation, incoming_relation_source}
153 ]} <- @user_relationships_config do
154 # Definitions of `has_many` relations: :blocker_blocks, :muter_mutes, :reblog_muter_mutes,
155 # :notification_muter_mutes, :subscribee_subscriptions
156 has_many(outgoing_relation, UserRelationship,
157 foreign_key: :source_id,
158 where: [relationship_type: relationship_type]
161 # Definitions of `has_many` relations: :blockee_blocks, :mutee_mutes, :reblog_mutee_mutes,
162 # :notification_mutee_mutes, :subscriber_subscriptions
163 has_many(incoming_relation, UserRelationship,
164 foreign_key: :target_id,
165 where: [relationship_type: relationship_type]
168 # Definitions of `has_many` relations: :blocked_users, :muted_users, :reblog_muted_users,
169 # :notification_muted_users, :subscriber_users
170 has_many(outgoing_relation_target, through: [outgoing_relation, :target])
172 # Definitions of `has_many` relations: :blocker_users, :muter_users, :reblog_muter_users,
173 # :notification_muter_users, :subscribee_users
174 has_many(incoming_relation_source, through: [incoming_relation, :source])
177 # `:blocks` is deprecated (replaced with `blocked_users` relation)
178 field(:blocks, {:array, :string}, default: [])
179 # `:mutes` is deprecated (replaced with `muted_users` relation)
180 field(:mutes, {:array, :string}, default: [])
181 # `:muted_reblogs` is deprecated (replaced with `reblog_muted_users` relation)
182 field(:muted_reblogs, {:array, :string}, default: [])
183 # `:muted_notifications` is deprecated (replaced with `notification_muted_users` relation)
184 field(:muted_notifications, {:array, :string}, default: [])
185 # `:subscribers` is deprecated (replaced with `subscriber_users` relation)
186 field(:subscribers, {:array, :string}, default: [])
191 for {_relationship_type, [{_outgoing_relation, outgoing_relation_target}, _]} <-
192 @user_relationships_config do
193 # `def blocked_users_relation/2`, `def muted_users_relation/2`,
194 # `def reblog_muted_users_relation/2`, `def notification_muted_users/2`,
195 # `def subscriber_users/2`
196 def unquote(:"#{outgoing_relation_target}_relation")(user, restrict_deactivated? \\ false) do
197 target_users_query = assoc(user, unquote(outgoing_relation_target))
199 if restrict_deactivated? do
200 restrict_deactivated(target_users_query)
206 # `def blocked_users/2`, `def muted_users/2`, `def reblog_muted_users/2`,
207 # `def notification_muted_users/2`, `def subscriber_users/2`
208 def unquote(outgoing_relation_target)(user, restrict_deactivated? \\ false) do
210 |> apply(unquote(:"#{outgoing_relation_target}_relation"), [
212 restrict_deactivated?
217 # `def blocked_users_ap_ids/2`, `def muted_users_ap_ids/2`, `def reblog_muted_users_ap_ids/2`,
218 # `def notification_muted_users_ap_ids/2`, `def subscriber_users_ap_ids/2`
219 def unquote(:"#{outgoing_relation_target}_ap_ids")(user, restrict_deactivated? \\ false) do
221 |> apply(unquote(:"#{outgoing_relation_target}_relation"), [
223 restrict_deactivated?
225 |> select([u], u.ap_id)
231 Dumps Flake Id to SQL-compatible format (16-byte UUID).
232 E.g. "9pQtDGXuq4p3VlcJEm" -> <<0, 0, 1, 110, 179, 218, 42, 92, 213, 41, 44, 227, 95, 213, 0, 0>>
234 def binary_id(source_id) when is_binary(source_id) do
235 with {:ok, dumped_id} <- FlakeId.Ecto.CompatType.dump(source_id) do
242 def binary_id(source_ids) when is_list(source_ids) do
243 Enum.map(source_ids, &binary_id/1)
246 def binary_id(%User{} = user), do: binary_id(user.id)
248 @doc "Returns status account"
249 @spec account_status(User.t()) :: account_status()
250 def account_status(%User{deactivated: true}), do: :deactivated
251 def account_status(%User{password_reset_pending: true}), do: :password_reset_pending
253 def account_status(%User{confirmation_pending: true}) do
254 case Config.get([:instance, :account_activation_required]) do
255 true -> :confirmation_pending
260 def account_status(%User{}), do: :active
262 @spec visible_for?(User.t(), User.t() | nil) :: boolean()
263 def visible_for?(user, for_user \\ nil)
265 def visible_for?(%User{invisible: true}, _), do: false
267 def visible_for?(%User{id: user_id}, %User{id: user_id}), do: true
269 def visible_for?(%User{local: local} = user, nil) do
275 if Config.get([:restrict_unauthenticated, :profiles, cfg_key]),
277 else: account_status(user) == :active
280 def visible_for?(%User{} = user, for_user) do
281 account_status(user) == :active || superuser?(for_user)
284 def visible_for?(_, _), do: false
286 @spec superuser?(User.t()) :: boolean()
287 def superuser?(%User{local: true, is_admin: true}), do: true
288 def superuser?(%User{local: true, is_moderator: true}), do: true
289 def superuser?(_), do: false
291 @spec invisible?(User.t()) :: boolean()
292 def invisible?(%User{invisible: true}), do: true
293 def invisible?(_), do: false
295 def avatar_url(user, options \\ []) do
297 %{"url" => [%{"href" => href} | _]} -> href
298 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
302 def banner_url(user, options \\ []) do
304 %{"url" => [%{"href" => href} | _]} -> href
305 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
309 def ap_id(%User{nickname: nickname}), do: "#{Web.base_url()}/users/#{nickname}"
311 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
312 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
314 @spec ap_following(User.t()) :: String.t()
315 def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
316 def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
318 @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
319 def restrict_deactivated(query) do
320 from(u in query, where: u.deactivated != ^true)
323 defdelegate following_count(user), to: FollowingRelationship
325 defp truncate_fields_param(params) do
326 if Map.has_key?(params, :fields) do
327 Map.put(params, :fields, Enum.map(params[:fields], &truncate_field/1))
333 defp truncate_if_exists(params, key, max_length) do
334 if Map.has_key?(params, key) and is_binary(params[key]) do
335 {value, _chopped} = String.split_at(params[key], max_length)
336 Map.put(params, key, value)
342 def remote_user_creation(params) do
343 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
344 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
348 |> truncate_if_exists(:name, name_limit)
349 |> truncate_if_exists(:bio, bio_limit)
350 |> truncate_fields_param()
370 :hide_followers_count,
381 |> validate_required([:name, :ap_id])
382 |> unique_constraint(:nickname)
383 |> validate_format(:nickname, @email_regex)
384 |> validate_length(:bio, max: bio_limit)
385 |> validate_length(:name, max: name_limit)
386 |> validate_fields(true)
388 case params[:source_data] do
389 %{"followers" => followers, "following" => following} ->
391 |> put_change(:follower_address, followers)
392 |> put_change(:following_address, following)
395 followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
396 put_change(changeset, :follower_address, followers)
400 def update_changeset(struct, params \\ %{}) do
401 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
402 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
417 :hide_followers_count,
420 :allow_following_move,
423 :skip_thread_containment,
426 :pleroma_settings_store,
432 |> unique_constraint(:nickname)
433 |> validate_format(:nickname, local_nickname_regex())
434 |> validate_length(:bio, max: bio_limit)
435 |> validate_length(:name, min: 1, max: name_limit)
437 |> put_change_if_present(:bio, &{:ok, parse_bio(&1, struct)})
438 |> put_change_if_present(:avatar, &put_upload(&1, :avatar))
439 |> put_change_if_present(:banner, &put_upload(&1, :banner))
440 |> put_change_if_present(:background, &put_upload(&1, :background))
441 |> put_change_if_present(
442 :pleroma_settings_store,
443 &{:ok, Map.merge(struct.pleroma_settings_store, &1)}
445 |> validate_fields(false)
448 defp put_fields(changeset) do
449 if raw_fields = get_change(changeset, :raw_fields) do
452 |> Enum.filter(fn %{"name" => n} -> n != "" end)
456 |> Enum.map(fn f -> Map.update!(f, "value", &parse_fields(&1)) end)
459 |> put_change(:raw_fields, raw_fields)
460 |> put_change(:fields, fields)
466 defp parse_fields(value) do
468 |> Formatter.linkify(mentions_format: :full)
472 defp put_change_if_present(changeset, map_field, value_function) do
473 if value = get_change(changeset, map_field) do
474 with {:ok, new_value} <- value_function.(value) do
475 put_change(changeset, map_field, new_value)
484 defp put_upload(value, type) do
485 with %Plug.Upload{} <- value,
486 {:ok, object} <- ActivityPub.upload(value, type: type) do
491 def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
492 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
493 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
495 params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
497 params = if remote?, do: truncate_fields_param(params), else: params
519 :allow_following_move,
521 :hide_followers_count,
527 |> unique_constraint(:nickname)
528 |> validate_format(:nickname, local_nickname_regex())
529 |> validate_length(:bio, max: bio_limit)
530 |> validate_length(:name, max: name_limit)
531 |> validate_fields(remote?)
534 def update_as_admin_changeset(struct, params) do
536 |> update_changeset(params)
537 |> cast(params, [:email])
538 |> delete_change(:also_known_as)
539 |> unique_constraint(:email)
540 |> validate_format(:email, @email_regex)
543 @spec update_as_admin(%User{}, map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
544 def update_as_admin(user, params) do
545 params = Map.put(params, "password_confirmation", params["password"])
546 changeset = update_as_admin_changeset(user, params)
548 if params["password"] do
549 reset_password(user, changeset, params)
551 User.update_and_set_cache(changeset)
555 def password_update_changeset(struct, params) do
557 |> cast(params, [:password, :password_confirmation])
558 |> validate_required([:password, :password_confirmation])
559 |> validate_confirmation(:password)
560 |> put_password_hash()
561 |> put_change(:password_reset_pending, false)
564 @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
565 def reset_password(%User{} = user, params) do
566 reset_password(user, user, params)
569 def reset_password(%User{id: user_id} = user, struct, params) do
572 |> Multi.update(:user, password_update_changeset(struct, params))
573 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
574 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
576 case Repo.transaction(multi) do
577 {:ok, %{user: user} = _} -> set_cache(user)
578 {:error, _, changeset, _} -> {:error, changeset}
582 def update_password_reset_pending(user, value) do
585 |> put_change(:password_reset_pending, value)
586 |> update_and_set_cache()
589 def force_password_reset_async(user) do
590 BackgroundWorker.enqueue("force_password_reset", %{"user_id" => user.id})
593 @spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
594 def force_password_reset(user), do: update_password_reset_pending(user, true)
596 def register_changeset(struct, params \\ %{}, opts \\ []) do
597 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
598 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
601 if is_nil(opts[:need_confirmation]) do
602 Pleroma.Config.get([:instance, :account_activation_required])
604 opts[:need_confirmation]
608 |> confirmation_changeset(need_confirmation: need_confirmation?)
609 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
610 |> validate_required([:name, :nickname, :password, :password_confirmation])
611 |> validate_confirmation(:password)
612 |> unique_constraint(:email)
613 |> unique_constraint(:nickname)
614 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
615 |> validate_format(:nickname, local_nickname_regex())
616 |> validate_format(:email, @email_regex)
617 |> validate_length(:bio, max: bio_limit)
618 |> validate_length(:name, min: 1, max: name_limit)
619 |> maybe_validate_required_email(opts[:external])
622 |> unique_constraint(:ap_id)
623 |> put_following_and_follower_address()
626 def maybe_validate_required_email(changeset, true), do: changeset
628 def maybe_validate_required_email(changeset, _) do
629 if Pleroma.Config.get([:instance, :account_activation_required]) do
630 validate_required(changeset, [:email])
636 defp put_ap_id(changeset) do
637 ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
638 put_change(changeset, :ap_id, ap_id)
641 defp put_following_and_follower_address(changeset) do
642 followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
645 |> put_change(:follower_address, followers)
648 defp autofollow_users(user) do
649 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
652 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
655 follow_all(user, autofollowed_users)
658 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
659 def register(%Ecto.Changeset{} = changeset) do
660 with {:ok, user} <- Repo.insert(changeset) do
661 post_register_action(user)
665 def post_register_action(%User{} = user) do
666 with {:ok, user} <- autofollow_users(user),
667 {:ok, user} <- set_cache(user),
668 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
669 {:ok, _} <- try_send_confirmation_email(user) do
674 def try_send_confirmation_email(%User{} = user) do
675 if user.confirmation_pending &&
676 Pleroma.Config.get([:instance, :account_activation_required]) do
678 |> Pleroma.Emails.UserEmail.account_confirmation_email()
679 |> Pleroma.Emails.Mailer.deliver_async()
687 def try_send_confirmation_email(users) do
688 Enum.each(users, &try_send_confirmation_email/1)
691 def needs_update?(%User{local: true}), do: false
693 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
695 def needs_update?(%User{local: false} = user) do
696 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
699 def needs_update?(_), do: true
701 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
702 def maybe_direct_follow(%User{} = follower, %User{local: true, locked: true} = followed) do
703 follow(follower, followed, "pending")
706 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
707 follow(follower, followed)
710 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
711 if not ap_enabled?(followed) do
712 follow(follower, followed)
718 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
719 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
720 def follow_all(follower, followeds) do
722 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
723 |> Enum.each(&follow(follower, &1, "accept"))
728 defdelegate following(user), to: FollowingRelationship
730 def follow(%User{} = follower, %User{} = followed, state \\ "accept") do
731 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
734 followed.deactivated ->
735 {:error, "Could not follow user: #{followed.nickname} is deactivated."}
737 deny_follow_blocked and blocks?(followed, follower) ->
738 {:error, "Could not follow user: #{followed.nickname} blocked you."}
741 FollowingRelationship.follow(follower, followed, state)
743 {:ok, _} = update_follower_count(followed)
746 |> update_following_count()
751 def unfollow(%User{ap_id: ap_id}, %User{ap_id: ap_id}) do
752 {:error, "Not subscribed!"}
755 def unfollow(%User{} = follower, %User{} = followed) do
756 case get_follow_state(follower, followed) do
757 state when state in ["accept", "pending"] ->
758 FollowingRelationship.unfollow(follower, followed)
759 {:ok, followed} = update_follower_count(followed)
763 |> update_following_count()
766 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
769 {:error, "Not subscribed!"}
773 defdelegate following?(follower, followed), to: FollowingRelationship
775 def get_follow_state(%User{} = follower, %User{} = following) do
776 following_relationship = FollowingRelationship.get(follower, following)
777 get_follow_state(follower, following, following_relationship)
780 def get_follow_state(
783 following_relationship
785 case {following_relationship, following.local} do
787 case Utils.fetch_latest_follow(follower, following) do
788 %{data: %{"state" => state}} when state in ["pending", "accept"] -> state
792 {%{state: state}, _} ->
800 def locked?(%User{} = user) do
805 Repo.get_by(User, id: id)
808 def get_by_ap_id(ap_id) do
809 Repo.get_by(User, ap_id: ap_id)
812 def get_all_by_ap_id(ap_ids) do
813 from(u in __MODULE__,
814 where: u.ap_id in ^ap_ids
819 def get_all_by_ids(ids) do
820 from(u in __MODULE__, where: u.id in ^ids)
824 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
825 # of the ap_id and the domain and tries to get that user
826 def get_by_guessed_nickname(ap_id) do
827 domain = URI.parse(ap_id).host
828 name = List.last(String.split(ap_id, "/"))
829 nickname = "#{name}@#{domain}"
831 get_cached_by_nickname(nickname)
834 def set_cache({:ok, user}), do: set_cache(user)
835 def set_cache({:error, err}), do: {:error, err}
837 def set_cache(%User{} = user) do
838 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
839 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
843 def update_and_set_cache(struct, params) do
845 |> update_changeset(params)
846 |> update_and_set_cache()
849 def update_and_set_cache(changeset) do
850 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
855 def invalidate_cache(user) do
856 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
857 Cachex.del(:user_cache, "nickname:#{user.nickname}")
860 @spec get_cached_by_ap_id(String.t()) :: User.t() | nil
861 def get_cached_by_ap_id(ap_id) do
862 key = "ap_id:#{ap_id}"
864 with {:ok, nil} <- Cachex.get(:user_cache, key),
865 user when not is_nil(user) <- get_by_ap_id(ap_id),
866 {:ok, true} <- Cachex.put(:user_cache, key, user) do
874 def get_cached_by_id(id) do
878 Cachex.fetch!(:user_cache, key, fn _ ->
882 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
883 {:commit, user.ap_id}
889 get_cached_by_ap_id(ap_id)
892 def get_cached_by_nickname(nickname) do
893 key = "nickname:#{nickname}"
895 Cachex.fetch!(:user_cache, key, fn ->
896 case get_or_fetch_by_nickname(nickname) do
897 {:ok, user} -> {:commit, user}
898 {:error, _error} -> {:ignore, nil}
903 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
904 restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
907 is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
908 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
910 restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
911 get_cached_by_nickname(nickname_or_id)
913 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
914 get_cached_by_nickname(nickname_or_id)
921 def get_by_nickname(nickname) do
922 Repo.get_by(User, nickname: nickname) ||
923 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
924 Repo.get_by(User, nickname: local_nickname(nickname))
928 def get_by_email(email), do: Repo.get_by(User, email: email)
930 def get_by_nickname_or_email(nickname_or_email) do
931 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
934 def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname)
936 def get_or_fetch_by_nickname(nickname) do
937 with %User{} = user <- get_by_nickname(nickname) do
941 with [_nick, _domain] <- String.split(nickname, "@"),
942 {:ok, user} <- fetch_by_nickname(nickname) do
945 _e -> {:error, "not found " <> nickname}
950 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
951 def get_followers_query(%User{} = user, nil) do
952 User.Query.build(%{followers: user, deactivated: false})
955 def get_followers_query(user, page) do
957 |> get_followers_query(nil)
958 |> User.Query.paginate(page, 20)
961 @spec get_followers_query(User.t()) :: Ecto.Query.t()
962 def get_followers_query(user), do: get_followers_query(user, nil)
964 @spec get_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
965 def get_followers(user, page \\ nil) do
967 |> get_followers_query(page)
971 @spec get_external_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
972 def get_external_followers(user, page \\ nil) do
974 |> get_followers_query(page)
975 |> User.Query.build(%{external: true})
979 def get_followers_ids(user, page \\ nil) do
981 |> get_followers_query(page)
986 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
987 def get_friends_query(%User{} = user, nil) do
988 User.Query.build(%{friends: user, deactivated: false})
991 def get_friends_query(user, page) do
993 |> get_friends_query(nil)
994 |> User.Query.paginate(page, 20)
997 @spec get_friends_query(User.t()) :: Ecto.Query.t()
998 def get_friends_query(user), do: get_friends_query(user, nil)
1000 def get_friends(user, page \\ nil) do
1002 |> get_friends_query(page)
1006 def get_friends_ap_ids(user) do
1008 |> get_friends_query(nil)
1009 |> select([u], u.ap_id)
1013 def get_friends_ids(user, page \\ nil) do
1015 |> get_friends_query(page)
1016 |> select([u], u.id)
1020 defdelegate get_follow_requests(user), to: FollowingRelationship
1022 def increase_note_count(%User{} = user) do
1024 |> where(id: ^user.id)
1025 |> update([u], inc: [note_count: 1])
1027 |> Repo.update_all([])
1029 {1, [user]} -> set_cache(user)
1034 def decrease_note_count(%User{} = user) do
1036 |> where(id: ^user.id)
1039 note_count: fragment("greatest(0, note_count - 1)")
1043 |> Repo.update_all([])
1045 {1, [user]} -> set_cache(user)
1050 def update_note_count(%User{} = user, note_count \\ nil) do
1055 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
1061 |> cast(%{note_count: note_count}, [:note_count])
1062 |> update_and_set_cache()
1065 @spec maybe_fetch_follow_information(User.t()) :: User.t()
1066 def maybe_fetch_follow_information(user) do
1067 with {:ok, user} <- fetch_follow_information(user) do
1071 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
1077 def fetch_follow_information(user) do
1078 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
1080 |> follow_information_changeset(info)
1081 |> update_and_set_cache()
1085 defp follow_information_changeset(user, params) do
1092 :hide_followers_count,
1097 def update_follower_count(%User{} = user) do
1098 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
1099 follower_count_query =
1100 User.Query.build(%{followers: user, deactivated: false})
1101 |> select([u], %{count: count(u.id)})
1104 |> where(id: ^user.id)
1105 |> join(:inner, [u], s in subquery(follower_count_query))
1107 set: [follower_count: s.count]
1110 |> Repo.update_all([])
1112 {1, [user]} -> set_cache(user)
1116 {:ok, maybe_fetch_follow_information(user)}
1120 @spec update_following_count(User.t()) :: User.t()
1121 def update_following_count(%User{local: false} = user) do
1122 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
1123 maybe_fetch_follow_information(user)
1129 def update_following_count(%User{local: true} = user) do
1130 following_count = FollowingRelationship.following_count(user)
1133 |> follow_information_changeset(%{following_count: following_count})
1137 def set_unread_conversation_count(%User{local: true} = user) do
1138 unread_query = Participation.unread_conversation_count_for_user(user)
1141 |> join(:inner, [u], p in subquery(unread_query))
1143 set: [unread_conversation_count: p.count]
1145 |> where([u], u.id == ^user.id)
1147 |> Repo.update_all([])
1149 {1, [user]} -> set_cache(user)
1154 def set_unread_conversation_count(user), do: {:ok, user}
1156 def increment_unread_conversation_count(conversation, %User{local: true} = user) do
1158 Participation.unread_conversation_count_for_user(user)
1159 |> where([p], p.conversation_id == ^conversation.id)
1162 |> join(:inner, [u], p in subquery(unread_query))
1164 inc: [unread_conversation_count: 1]
1166 |> where([u], u.id == ^user.id)
1167 |> where([u, p], p.count == 0)
1169 |> Repo.update_all([])
1171 {1, [user]} -> set_cache(user)
1176 def increment_unread_conversation_count(_, user), do: {:ok, user}
1178 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
1179 def get_users_from_set(ap_ids, local_only \\ true) do
1180 criteria = %{ap_id: ap_ids, deactivated: false}
1181 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
1183 User.Query.build(criteria)
1187 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
1188 def get_recipients_from_activity(%Activity{recipients: to}) do
1189 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
1193 @spec mute(User.t(), User.t(), boolean()) ::
1194 {:ok, list(UserRelationship.t())} | {:error, String.t()}
1195 def mute(%User{} = muter, %User{} = mutee, notifications? \\ true) do
1196 add_to_mutes(muter, mutee, notifications?)
1199 def unmute(%User{} = muter, %User{} = mutee) do
1200 remove_from_mutes(muter, mutee)
1203 def subscribe(%User{} = subscriber, %User{} = target) do
1204 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
1206 if blocks?(target, subscriber) and deny_follow_blocked do
1207 {:error, "Could not subscribe: #{target.nickname} is blocking you"}
1209 # Note: the relationship is inverse: subscriber acts as relationship target
1210 UserRelationship.create_inverse_subscription(target, subscriber)
1214 def subscribe(%User{} = subscriber, %{ap_id: ap_id}) do
1215 with %User{} = subscribee <- get_cached_by_ap_id(ap_id) do
1216 subscribe(subscriber, subscribee)
1220 def unsubscribe(%User{} = unsubscriber, %User{} = target) do
1221 # Note: the relationship is inverse: subscriber acts as relationship target
1222 UserRelationship.delete_inverse_subscription(target, unsubscriber)
1225 def unsubscribe(%User{} = unsubscriber, %{ap_id: ap_id}) do
1226 with %User{} = user <- get_cached_by_ap_id(ap_id) do
1227 unsubscribe(unsubscriber, user)
1231 def block(%User{} = blocker, %User{} = blocked) do
1232 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
1234 if following?(blocker, blocked) do
1235 {:ok, blocker, _} = unfollow(blocker, blocked)
1241 # clear any requested follows as well
1243 case CommonAPI.reject_follow_request(blocked, blocker) do
1244 {:ok, %User{} = updated_blocked} -> updated_blocked
1248 unsubscribe(blocked, blocker)
1250 if following?(blocked, blocker), do: unfollow(blocked, blocker)
1252 {:ok, blocker} = update_follower_count(blocker)
1253 {:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
1254 add_to_block(blocker, blocked)
1257 # helper to handle the block given only an actor's AP id
1258 def block(%User{} = blocker, %{ap_id: ap_id}) do
1259 block(blocker, get_cached_by_ap_id(ap_id))
1262 def unblock(%User{} = blocker, %User{} = blocked) do
1263 remove_from_block(blocker, blocked)
1266 # helper to handle the block given only an actor's AP id
1267 def unblock(%User{} = blocker, %{ap_id: ap_id}) do
1268 unblock(blocker, get_cached_by_ap_id(ap_id))
1271 def mutes?(nil, _), do: false
1272 def mutes?(%User{} = user, %User{} = target), do: mutes_user?(user, target)
1274 def mutes_user?(%User{} = user, %User{} = target) do
1275 UserRelationship.mute_exists?(user, target)
1278 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
1279 def muted_notifications?(nil, _), do: false
1281 def muted_notifications?(%User{} = user, %User{} = target),
1282 do: UserRelationship.notification_mute_exists?(user, target)
1284 def blocks?(nil, _), do: false
1286 def blocks?(%User{} = user, %User{} = target) do
1287 blocks_user?(user, target) ||
1288 (!User.following?(user, target) && blocks_domain?(user, target))
1291 def blocks_user?(%User{} = user, %User{} = target) do
1292 UserRelationship.block_exists?(user, target)
1295 def blocks_user?(_, _), do: false
1297 def blocks_domain?(%User{} = user, %User{} = target) do
1298 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
1299 %{host: host} = URI.parse(target.ap_id)
1300 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1303 def blocks_domain?(_, _), do: false
1305 def subscribed_to?(%User{} = user, %User{} = target) do
1306 # Note: the relationship is inverse: subscriber acts as relationship target
1307 UserRelationship.inverse_subscription_exists?(target, user)
1310 def subscribed_to?(%User{} = user, %{ap_id: ap_id}) do
1311 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1312 subscribed_to?(user, target)
1317 Returns map of outgoing (blocked, muted etc.) relationships' user AP IDs by relation type.
1318 E.g. `outgoing_relationships_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}`
1320 @spec outgoing_relationships_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())}
1321 def outgoing_relationships_ap_ids(_user, []), do: %{}
1323 def outgoing_relationships_ap_ids(nil, _relationship_types), do: %{}
1325 def outgoing_relationships_ap_ids(%User{} = user, relationship_types)
1326 when is_list(relationship_types) do
1329 |> assoc(:outgoing_relationships)
1330 |> join(:inner, [user_rel], u in assoc(user_rel, :target))
1331 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1332 |> select([user_rel, u], [user_rel.relationship_type, fragment("array_agg(?)", u.ap_id)])
1333 |> group_by([user_rel, u], user_rel.relationship_type)
1335 |> Enum.into(%{}, fn [k, v] -> {k, v} end)
1340 fn rel_type -> {rel_type, db_result[rel_type] || []} end
1344 def incoming_relationships_ungrouped_ap_ids(user, relationship_types, ap_ids \\ nil)
1346 def incoming_relationships_ungrouped_ap_ids(_user, [], _ap_ids), do: []
1348 def incoming_relationships_ungrouped_ap_ids(nil, _relationship_types, _ap_ids), do: []
1350 def incoming_relationships_ungrouped_ap_ids(%User{} = user, relationship_types, ap_ids)
1351 when is_list(relationship_types) do
1353 |> assoc(:incoming_relationships)
1354 |> join(:inner, [user_rel], u in assoc(user_rel, :source))
1355 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1356 |> maybe_filter_on_ap_id(ap_ids)
1357 |> select([user_rel, u], u.ap_id)
1362 defp maybe_filter_on_ap_id(query, ap_ids) when is_list(ap_ids) do
1363 where(query, [user_rel, u], u.ap_id in ^ap_ids)
1366 defp maybe_filter_on_ap_id(query, _ap_ids), do: query
1368 def deactivate_async(user, status \\ true) do
1369 BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
1372 def deactivate(user, status \\ true)
1374 def deactivate(users, status) when is_list(users) do
1375 Repo.transaction(fn ->
1376 for user <- users, do: deactivate(user, status)
1380 def deactivate(%User{} = user, status) do
1381 with {:ok, user} <- set_activation_status(user, status) do
1384 |> Enum.filter(& &1.local)
1385 |> Enum.each(fn follower ->
1386 follower |> update_following_count() |> set_cache()
1389 # Only update local user counts, remote will be update during the next pull.
1392 |> Enum.filter(& &1.local)
1393 |> Enum.each(&update_follower_count/1)
1399 def update_notification_settings(%User{} = user, settings) do
1401 |> cast(%{notification_settings: settings}, [])
1402 |> cast_embed(:notification_settings)
1403 |> validate_required([:notification_settings])
1404 |> update_and_set_cache()
1407 def delete(users) when is_list(users) do
1408 for user <- users, do: delete(user)
1411 def delete(%User{} = user) do
1412 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1415 def perform(:force_password_reset, user), do: force_password_reset(user)
1417 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1418 def perform(:delete, %User{} = user) do
1419 {:ok, _user} = ActivityPub.delete(user)
1421 # Remove all relationships
1424 |> Enum.each(fn follower ->
1425 ActivityPub.unfollow(follower, user)
1426 unfollow(follower, user)
1431 |> Enum.each(fn followed ->
1432 ActivityPub.unfollow(user, followed)
1433 unfollow(user, followed)
1436 delete_user_activities(user)
1437 invalidate_cache(user)
1441 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1443 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1444 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1445 when is_list(blocked_identifiers) do
1447 blocked_identifiers,
1448 fn blocked_identifier ->
1449 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1450 {:ok, _user_block} <- block(blocker, blocked),
1451 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1455 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1462 def perform(:follow_import, %User{} = follower, followed_identifiers)
1463 when is_list(followed_identifiers) do
1465 followed_identifiers,
1466 fn followed_identifier ->
1467 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1468 {:ok, follower} <- maybe_direct_follow(follower, followed),
1469 {:ok, _} <- ActivityPub.follow(follower, followed) do
1473 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1480 @spec external_users_query() :: Ecto.Query.t()
1481 def external_users_query do
1489 @spec external_users(keyword()) :: [User.t()]
1490 def external_users(opts \\ []) do
1492 external_users_query()
1493 |> select([u], struct(u, [:id, :ap_id]))
1497 do: where(query, [u], u.id > ^opts[:max_id]),
1502 do: limit(query, ^opts[:limit]),
1508 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1509 BackgroundWorker.enqueue("blocks_import", %{
1510 "blocker_id" => blocker.id,
1511 "blocked_identifiers" => blocked_identifiers
1515 def follow_import(%User{} = follower, followed_identifiers)
1516 when is_list(followed_identifiers) do
1517 BackgroundWorker.enqueue("follow_import", %{
1518 "follower_id" => follower.id,
1519 "followed_identifiers" => followed_identifiers
1523 def delete_user_activities(%User{ap_id: ap_id}) do
1525 |> Activity.Queries.by_actor()
1526 |> RepoStreamer.chunk_stream(50)
1527 |> Stream.each(fn activities -> Enum.each(activities, &delete_activity/1) end)
1531 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1533 |> Object.normalize()
1534 |> ActivityPub.delete()
1537 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1538 object = Object.normalize(activity)
1541 |> get_cached_by_ap_id()
1542 |> ActivityPub.unlike(object)
1545 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1546 object = Object.normalize(activity)
1549 |> get_cached_by_ap_id()
1550 |> ActivityPub.unannounce(object)
1553 defp delete_activity(_activity), do: "Doing nothing"
1555 def html_filter_policy(%User{no_rich_text: true}) do
1556 Pleroma.HTML.Scrubber.TwitterText
1559 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1561 def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
1563 def get_or_fetch_by_ap_id(ap_id) do
1564 user = get_cached_by_ap_id(ap_id)
1566 if !is_nil(user) and !needs_update?(user) do
1569 fetch_by_ap_id(ap_id)
1574 Creates an internal service actor by URI if missing.
1575 Optionally takes nickname for addressing.
1577 @spec get_or_create_service_actor_by_ap_id(String.t(), String.t()) :: User.t() | nil
1578 def get_or_create_service_actor_by_ap_id(uri, nickname) do
1580 case get_cached_by_ap_id(uri) do
1582 with {:error, %{errors: errors}} <- create_service_actor(uri, nickname) do
1583 Logger.error("Cannot create service actor: #{uri}/.\n#{inspect(errors)}")
1587 %User{invisible: false} = user ->
1597 @spec set_invisible(User.t()) :: {:ok, User.t()}
1598 defp set_invisible(user) do
1600 |> change(%{invisible: true})
1601 |> update_and_set_cache()
1604 @spec create_service_actor(String.t(), String.t()) ::
1605 {:ok, User.t()} | {:error, Ecto.Changeset.t()}
1606 defp create_service_actor(uri, nickname) do
1612 follower_address: uri <> "/followers"
1615 |> unique_constraint(:nickname)
1621 def public_key(%{source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}}) do
1624 |> :public_key.pem_decode()
1626 |> :public_key.pem_entry_decode()
1631 def public_key(_), do: {:error, "not found key"}
1633 def get_public_key_for_ap_id(ap_id) do
1634 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1635 {:ok, public_key} <- public_key(user) do
1642 defp blank?(""), do: nil
1643 defp blank?(n), do: n
1645 def insert_or_update_user(data) do
1647 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1648 |> remote_user_creation()
1649 |> Repo.insert(on_conflict: {:replace_all_except, [:id]}, conflict_target: :nickname)
1653 def ap_enabled?(%User{local: true}), do: true
1654 def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled
1655 def ap_enabled?(_), do: false
1657 @doc "Gets or fetch a user by uri or nickname."
1658 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1659 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1660 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1662 # wait a period of time and return newest version of the User structs
1663 # this is because we have synchronous follow APIs and need to simulate them
1664 # with an async handshake
1665 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1666 with %User{} = a <- get_cached_by_id(a.id),
1667 %User{} = b <- get_cached_by_id(b.id) do
1674 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1675 with :ok <- :timer.sleep(timeout),
1676 %User{} = a <- get_cached_by_id(a.id),
1677 %User{} = b <- get_cached_by_id(b.id) do
1684 def parse_bio(bio) when is_binary(bio) and bio != "" do
1686 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1690 def parse_bio(_), do: ""
1692 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1693 # TODO: get profile URLs other than user.ap_id
1694 profile_urls = [user.ap_id]
1697 |> CommonUtils.format_input("text/plain",
1698 mentions_format: :full,
1699 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1704 def parse_bio(_, _), do: ""
1706 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1707 Repo.transaction(fn ->
1708 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1712 def tag(nickname, tags) when is_binary(nickname),
1713 do: tag(get_by_nickname(nickname), tags)
1715 def tag(%User{} = user, tags),
1716 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1718 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1719 Repo.transaction(fn ->
1720 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1724 def untag(nickname, tags) when is_binary(nickname),
1725 do: untag(get_by_nickname(nickname), tags)
1727 def untag(%User{} = user, tags),
1728 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1730 defp update_tags(%User{} = user, new_tags) do
1731 {:ok, updated_user} =
1733 |> change(%{tags: new_tags})
1734 |> update_and_set_cache()
1739 defp normalize_tags(tags) do
1742 |> Enum.map(&String.downcase/1)
1745 defp local_nickname_regex do
1746 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1747 @extended_local_nickname_regex
1749 @strict_local_nickname_regex
1753 def local_nickname(nickname_or_mention) do
1756 |> String.split("@")
1760 def full_nickname(nickname_or_mention),
1761 do: String.trim_leading(nickname_or_mention, "@")
1763 def error_user(ap_id) do
1767 nickname: "erroruser@example.com",
1768 inserted_at: NaiveDateTime.utc_now()
1772 @spec all_superusers() :: [User.t()]
1773 def all_superusers do
1774 User.Query.build(%{super_users: true, local: true, deactivated: false})
1778 def muting_reblogs?(%User{} = user, %User{} = target) do
1779 UserRelationship.reblog_mute_exists?(user, target)
1782 def showing_reblogs?(%User{} = user, %User{} = target) do
1783 not muting_reblogs?(user, target)
1787 The function returns a query to get users with no activity for given interval of days.
1788 Inactive users are those who didn't read any notification, or had any activity where
1789 the user is the activity's actor, during `inactivity_threshold` days.
1790 Deactivated users will not appear in this list.
1794 iex> Pleroma.User.list_inactive_users()
1797 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1798 def list_inactive_users_query(inactivity_threshold \\ 7) do
1799 negative_inactivity_threshold = -inactivity_threshold
1800 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1801 # Subqueries are not supported in `where` clauses, join gets too complicated.
1802 has_read_notifications =
1803 from(n in Pleroma.Notification,
1804 where: n.seen == true,
1806 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1809 |> Pleroma.Repo.all()
1811 from(u in Pleroma.User,
1812 left_join: a in Pleroma.Activity,
1813 on: u.ap_id == a.actor,
1814 where: not is_nil(u.nickname),
1815 where: u.deactivated != ^true,
1816 where: u.id not in ^has_read_notifications,
1819 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1820 is_nil(max(a.inserted_at))
1825 Enable or disable email notifications for user
1829 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => false}}, "digest", true)
1830 Pleroma.User{email_notifications: %{"digest" => true}}
1832 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => true}}, "digest", false)
1833 Pleroma.User{email_notifications: %{"digest" => false}}
1835 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1836 {:ok, t()} | {:error, Ecto.Changeset.t()}
1837 def switch_email_notifications(user, type, status) do
1838 User.update_email_notifications(user, %{type => status})
1842 Set `last_digest_emailed_at` value for the user to current time
1844 @spec touch_last_digest_emailed_at(t()) :: t()
1845 def touch_last_digest_emailed_at(user) do
1846 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1848 {:ok, updated_user} =
1850 |> change(%{last_digest_emailed_at: now})
1851 |> update_and_set_cache()
1856 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1857 def toggle_confirmation(%User{} = user) do
1859 |> confirmation_changeset(need_confirmation: !user.confirmation_pending)
1860 |> update_and_set_cache()
1863 @spec toggle_confirmation([User.t()]) :: [{:ok, User.t()} | {:error, Changeset.t()}]
1864 def toggle_confirmation(users) do
1865 Enum.map(users, &toggle_confirmation/1)
1868 def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
1872 def get_mascot(%{mascot: mascot}) when is_nil(mascot) do
1873 # use instance-default
1874 config = Pleroma.Config.get([:assets, :mascots])
1875 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1876 mascot = Keyword.get(config, default_mascot)
1879 "id" => "default-mascot",
1880 "url" => mascot[:url],
1881 "preview_url" => mascot[:url],
1883 "mime_type" => mascot[:mime_type]
1888 def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
1890 def ensure_keys_present(%User{} = user) do
1891 with {:ok, pem} <- Keys.generate_rsa_pem() do
1893 |> cast(%{keys: pem}, [:keys])
1894 |> validate_required([:keys])
1895 |> update_and_set_cache()
1899 def get_ap_ids_by_nicknames(nicknames) do
1901 where: u.nickname in ^nicknames,
1907 defdelegate search(query, opts \\ []), to: User.Search
1909 defp put_password_hash(
1910 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1912 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1915 defp put_password_hash(changeset), do: changeset
1917 def is_internal_user?(%User{nickname: nil}), do: true
1918 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1919 def is_internal_user?(_), do: false
1921 # A hack because user delete activities have a fake id for whatever reason
1922 # TODO: Get rid of this
1923 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
1925 def get_delivered_users_by_object_id(object_id) do
1927 inner_join: delivery in assoc(u, :deliveries),
1928 where: delivery.object_id == ^object_id
1933 def change_email(user, email) do
1935 |> cast(%{email: email}, [:email])
1936 |> validate_required([:email])
1937 |> unique_constraint(:email)
1938 |> validate_format(:email, @email_regex)
1939 |> update_and_set_cache()
1942 # Internal function; public one is `deactivate/2`
1943 defp set_activation_status(user, deactivated) do
1945 |> cast(%{deactivated: deactivated}, [:deactivated])
1946 |> update_and_set_cache()
1949 def update_banner(user, banner) do
1951 |> cast(%{banner: banner}, [:banner])
1952 |> update_and_set_cache()
1955 def update_background(user, background) do
1957 |> cast(%{background: background}, [:background])
1958 |> update_and_set_cache()
1961 def update_source_data(user, source_data) do
1963 |> cast(%{source_data: source_data}, [:source_data])
1964 |> update_and_set_cache()
1967 def roles(%{is_moderator: is_moderator, is_admin: is_admin}) do
1970 moderator: is_moderator
1974 # ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``.
1975 # For example: [{"name": "Pronoun", "value": "she/her"}, …]
1976 def fields(%{fields: nil, source_data: %{"attachment" => attachment}}) do
1977 limit = Pleroma.Config.get([:instance, :max_remote_account_fields], 0)
1980 |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
1981 |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
1985 def fields(%{fields: nil}), do: []
1987 def fields(%{fields: fields}), do: fields
1989 def validate_fields(changeset, remote? \\ false) do
1990 limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
1991 limit = Pleroma.Config.get([:instance, limit_name], 0)
1994 |> validate_length(:fields, max: limit)
1995 |> validate_change(:fields, fn :fields, fields ->
1996 if Enum.all?(fields, &valid_field?/1) do
2004 defp valid_field?(%{"name" => name, "value" => value}) do
2005 name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255)
2006 value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255)
2008 is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
2009 String.length(value) <= value_limit
2012 defp valid_field?(_), do: false
2014 defp truncate_field(%{"name" => name, "value" => value}) do
2016 String.split_at(name, Pleroma.Config.get([:instance, :account_field_name_length], 255))
2019 String.split_at(value, Pleroma.Config.get([:instance, :account_field_value_length], 255))
2021 %{"name" => name, "value" => value}
2024 def admin_api_update(user, params) do
2031 |> update_and_set_cache()
2034 @doc "Signs user out of all applications"
2035 def global_sign_out(user) do
2036 OAuth.Authorization.delete_user_authorizations(user)
2037 OAuth.Token.delete_user_tokens(user)
2040 def mascot_update(user, url) do
2042 |> cast(%{mascot: url}, [:mascot])
2043 |> validate_required([:mascot])
2044 |> update_and_set_cache()
2047 def mastodon_settings_update(user, settings) do
2049 |> cast(%{settings: settings}, [:settings])
2050 |> validate_required([:settings])
2051 |> update_and_set_cache()
2054 @spec confirmation_changeset(User.t(), keyword()) :: Changeset.t()
2055 def confirmation_changeset(user, need_confirmation: need_confirmation?) do
2057 if need_confirmation? do
2059 confirmation_pending: true,
2060 confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
2064 confirmation_pending: false,
2065 confirmation_token: nil
2069 cast(user, params, [:confirmation_pending, :confirmation_token])
2072 def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do
2073 if id not in user.pinned_activities do
2074 max_pinned_statuses = Pleroma.Config.get([:instance, :max_pinned_statuses], 0)
2075 params = %{pinned_activities: user.pinned_activities ++ [id]}
2078 |> cast(params, [:pinned_activities])
2079 |> validate_length(:pinned_activities,
2080 max: max_pinned_statuses,
2081 message: "You have already pinned the maximum number of statuses"
2086 |> update_and_set_cache()
2089 def remove_pinnned_activity(user, %Pleroma.Activity{id: id}) do
2090 params = %{pinned_activities: List.delete(user.pinned_activities, id)}
2093 |> cast(params, [:pinned_activities])
2094 |> update_and_set_cache()
2097 def update_email_notifications(user, settings) do
2098 email_notifications =
2099 user.email_notifications
2100 |> Map.merge(settings)
2101 |> Map.take(["digest"])
2103 params = %{email_notifications: email_notifications}
2104 fields = [:email_notifications]
2107 |> cast(params, fields)
2108 |> validate_required(fields)
2109 |> update_and_set_cache()
2112 defp set_domain_blocks(user, domain_blocks) do
2113 params = %{domain_blocks: domain_blocks}
2116 |> cast(params, [:domain_blocks])
2117 |> validate_required([:domain_blocks])
2118 |> update_and_set_cache()
2121 def block_domain(user, domain_blocked) do
2122 set_domain_blocks(user, Enum.uniq([domain_blocked | user.domain_blocks]))
2125 def unblock_domain(user, domain_blocked) do
2126 set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked))
2129 @spec add_to_block(User.t(), User.t()) ::
2130 {:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()}
2131 defp add_to_block(%User{} = user, %User{} = blocked) do
2132 UserRelationship.create_block(user, blocked)
2135 @spec add_to_block(User.t(), User.t()) ::
2136 {:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()}
2137 defp remove_from_block(%User{} = user, %User{} = blocked) do
2138 UserRelationship.delete_block(user, blocked)
2141 defp add_to_mutes(%User{} = user, %User{} = muted_user, notifications?) do
2142 with {:ok, user_mute} <- UserRelationship.create_mute(user, muted_user),
2143 {:ok, user_notification_mute} <-
2144 (notifications? && UserRelationship.create_notification_mute(user, muted_user)) ||
2146 {:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
2150 defp remove_from_mutes(user, %User{} = muted_user) do
2151 with {:ok, user_mute} <- UserRelationship.delete_mute(user, muted_user),
2152 {:ok, user_notification_mute} <-
2153 UserRelationship.delete_notification_mute(user, muted_user) do
2154 {:ok, [user_mute, user_notification_mute]}
2158 def set_invisible(user, invisible) do
2159 params = %{invisible: invisible}
2162 |> cast(params, [:invisible])
2163 |> validate_required([:invisible])
2164 |> update_and_set_cache()
2167 def sanitize_html(%User{} = user) do
2168 sanitize_html(user, nil)
2171 # User data that mastodon isn't filtering (treated as plaintext):
2174 def sanitize_html(%User{} = user, filter) do
2178 |> Enum.map(fn %{"name" => name, "value" => value} ->
2181 "value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
2186 |> Map.put(:bio, HTML.filter_tags(user.bio, filter))
2187 |> Map.put(:fields, fields)