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 # Should probably be renamed or removed
310 def ap_id(%User{nickname: nickname}), do: "#{Web.base_url()}/users/#{nickname}"
312 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
313 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
315 @spec ap_following(User.t()) :: String.t()
316 def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
317 def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
319 @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
320 def restrict_deactivated(query) do
321 from(u in query, where: u.deactivated != ^true)
324 defdelegate following_count(user), to: FollowingRelationship
326 defp truncate_fields_param(params) do
327 if Map.has_key?(params, :fields) do
328 Map.put(params, :fields, Enum.map(params[:fields], &truncate_field/1))
334 defp truncate_if_exists(params, key, max_length) do
335 if Map.has_key?(params, key) and is_binary(params[key]) do
336 {value, _chopped} = String.split_at(params[key], max_length)
337 Map.put(params, key, value)
343 defp fix_follower_address(%{follower_address: _, following_address: _} = params), do: params
345 defp fix_follower_address(%{nickname: nickname} = params),
346 do: Map.put(params, :follower_address, ap_followers(%User{nickname: nickname}))
348 defp fix_follower_address(params), do: params
350 def remote_user_creation(params) do
351 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
352 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
356 |> truncate_if_exists(:name, name_limit)
357 |> truncate_if_exists(:bio, bio_limit)
358 |> truncate_fields_param()
359 |> fix_follower_address()
380 :hide_followers_count,
391 |> validate_required([:name, :ap_id])
392 |> unique_constraint(:nickname)
393 |> validate_format(:nickname, @email_regex)
394 |> validate_length(:bio, max: bio_limit)
395 |> validate_length(:name, max: name_limit)
396 |> validate_fields(true)
399 def update_changeset(struct, params \\ %{}) do
400 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
401 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
416 :hide_followers_count,
419 :allow_following_move,
422 :skip_thread_containment,
425 :pleroma_settings_store,
431 |> unique_constraint(:nickname)
432 |> validate_format(:nickname, local_nickname_regex())
433 |> validate_length(:bio, max: bio_limit)
434 |> validate_length(:name, min: 1, max: name_limit)
436 |> put_change_if_present(:bio, &{:ok, parse_bio(&1, struct)})
437 |> put_change_if_present(:avatar, &put_upload(&1, :avatar))
438 |> put_change_if_present(:banner, &put_upload(&1, :banner))
439 |> put_change_if_present(:background, &put_upload(&1, :background))
440 |> put_change_if_present(
441 :pleroma_settings_store,
442 &{:ok, Map.merge(struct.pleroma_settings_store, &1)}
444 |> validate_fields(false)
447 defp put_fields(changeset) do
448 if raw_fields = get_change(changeset, :raw_fields) do
451 |> Enum.filter(fn %{"name" => n} -> n != "" end)
455 |> Enum.map(fn f -> Map.update!(f, "value", &parse_fields(&1)) end)
458 |> put_change(:raw_fields, raw_fields)
459 |> put_change(:fields, fields)
465 defp parse_fields(value) do
467 |> Formatter.linkify(mentions_format: :full)
471 defp put_change_if_present(changeset, map_field, value_function) do
472 if value = get_change(changeset, map_field) do
473 with {:ok, new_value} <- value_function.(value) do
474 put_change(changeset, map_field, new_value)
483 defp put_upload(value, type) do
484 with %Plug.Upload{} <- value,
485 {:ok, object} <- ActivityPub.upload(value, type: type) do
490 def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
491 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
492 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
494 params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
496 params = if remote?, do: truncate_fields_param(params), else: params
518 :allow_following_move,
520 :hide_followers_count,
526 |> unique_constraint(:nickname)
527 |> validate_format(:nickname, local_nickname_regex())
528 |> validate_length(:bio, max: bio_limit)
529 |> validate_length(:name, max: name_limit)
530 |> validate_fields(remote?)
533 def update_as_admin_changeset(struct, params) do
535 |> update_changeset(params)
536 |> cast(params, [:email])
537 |> delete_change(:also_known_as)
538 |> unique_constraint(:email)
539 |> validate_format(:email, @email_regex)
542 @spec update_as_admin(%User{}, map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
543 def update_as_admin(user, params) do
544 params = Map.put(params, "password_confirmation", params["password"])
545 changeset = update_as_admin_changeset(user, params)
547 if params["password"] do
548 reset_password(user, changeset, params)
550 User.update_and_set_cache(changeset)
554 def password_update_changeset(struct, params) do
556 |> cast(params, [:password, :password_confirmation])
557 |> validate_required([:password, :password_confirmation])
558 |> validate_confirmation(:password)
559 |> put_password_hash()
560 |> put_change(:password_reset_pending, false)
563 @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
564 def reset_password(%User{} = user, params) do
565 reset_password(user, user, params)
568 def reset_password(%User{id: user_id} = user, struct, params) do
571 |> Multi.update(:user, password_update_changeset(struct, params))
572 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
573 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
575 case Repo.transaction(multi) do
576 {:ok, %{user: user} = _} -> set_cache(user)
577 {:error, _, changeset, _} -> {:error, changeset}
581 def update_password_reset_pending(user, value) do
584 |> put_change(:password_reset_pending, value)
585 |> update_and_set_cache()
588 def force_password_reset_async(user) do
589 BackgroundWorker.enqueue("force_password_reset", %{"user_id" => user.id})
592 @spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
593 def force_password_reset(user), do: update_password_reset_pending(user, true)
595 def register_changeset(struct, params \\ %{}, opts \\ []) do
596 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
597 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
600 if is_nil(opts[:need_confirmation]) do
601 Pleroma.Config.get([:instance, :account_activation_required])
603 opts[:need_confirmation]
607 |> confirmation_changeset(need_confirmation: need_confirmation?)
608 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
609 |> validate_required([:name, :nickname, :password, :password_confirmation])
610 |> validate_confirmation(:password)
611 |> unique_constraint(:email)
612 |> unique_constraint(:nickname)
613 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
614 |> validate_format(:nickname, local_nickname_regex())
615 |> validate_format(:email, @email_regex)
616 |> validate_length(:bio, max: bio_limit)
617 |> validate_length(:name, min: 1, max: name_limit)
618 |> maybe_validate_required_email(opts[:external])
621 |> unique_constraint(:ap_id)
622 |> put_following_and_follower_address()
625 def maybe_validate_required_email(changeset, true), do: changeset
627 def maybe_validate_required_email(changeset, _) do
628 if Pleroma.Config.get([:instance, :account_activation_required]) do
629 validate_required(changeset, [:email])
635 defp put_ap_id(changeset) do
636 ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
637 put_change(changeset, :ap_id, ap_id)
640 defp put_following_and_follower_address(changeset) do
641 followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
644 |> put_change(:follower_address, followers)
647 defp autofollow_users(user) do
648 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
651 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
654 follow_all(user, autofollowed_users)
657 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
658 def register(%Ecto.Changeset{} = changeset) do
659 with {:ok, user} <- Repo.insert(changeset) do
660 post_register_action(user)
664 def post_register_action(%User{} = user) do
665 with {:ok, user} <- autofollow_users(user),
666 {:ok, user} <- set_cache(user),
667 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
668 {:ok, _} <- try_send_confirmation_email(user) do
673 def try_send_confirmation_email(%User{} = user) do
674 if user.confirmation_pending &&
675 Pleroma.Config.get([:instance, :account_activation_required]) do
677 |> Pleroma.Emails.UserEmail.account_confirmation_email()
678 |> Pleroma.Emails.Mailer.deliver_async()
686 def try_send_confirmation_email(users) do
687 Enum.each(users, &try_send_confirmation_email/1)
690 def needs_update?(%User{local: true}), do: false
692 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
694 def needs_update?(%User{local: false} = user) do
695 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
698 def needs_update?(_), do: true
700 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
701 def maybe_direct_follow(%User{} = follower, %User{local: true, locked: true} = followed) do
702 follow(follower, followed, "pending")
705 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
706 follow(follower, followed)
709 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
710 if not ap_enabled?(followed) do
711 follow(follower, followed)
717 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
718 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
719 def follow_all(follower, followeds) do
721 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
722 |> Enum.each(&follow(follower, &1, "accept"))
727 defdelegate following(user), to: FollowingRelationship
729 def follow(%User{} = follower, %User{} = followed, state \\ "accept") do
730 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
733 followed.deactivated ->
734 {:error, "Could not follow user: #{followed.nickname} is deactivated."}
736 deny_follow_blocked and blocks?(followed, follower) ->
737 {:error, "Could not follow user: #{followed.nickname} blocked you."}
740 FollowingRelationship.follow(follower, followed, state)
742 {:ok, _} = update_follower_count(followed)
745 |> update_following_count()
750 def unfollow(%User{ap_id: ap_id}, %User{ap_id: ap_id}) do
751 {:error, "Not subscribed!"}
754 def unfollow(%User{} = follower, %User{} = followed) do
755 case get_follow_state(follower, followed) do
756 state when state in ["accept", "pending"] ->
757 FollowingRelationship.unfollow(follower, followed)
758 {:ok, followed} = update_follower_count(followed)
762 |> update_following_count()
765 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
768 {:error, "Not subscribed!"}
772 defdelegate following?(follower, followed), to: FollowingRelationship
774 def get_follow_state(%User{} = follower, %User{} = following) do
775 following_relationship = FollowingRelationship.get(follower, following)
776 get_follow_state(follower, following, following_relationship)
779 def get_follow_state(
782 following_relationship
784 case {following_relationship, following.local} do
786 case Utils.fetch_latest_follow(follower, following) do
787 %{data: %{"state" => state}} when state in ["pending", "accept"] -> state
791 {%{state: state}, _} ->
799 def locked?(%User{} = user) do
804 Repo.get_by(User, id: id)
807 def get_by_ap_id(ap_id) do
808 Repo.get_by(User, ap_id: ap_id)
811 def get_all_by_ap_id(ap_ids) do
812 from(u in __MODULE__,
813 where: u.ap_id in ^ap_ids
818 def get_all_by_ids(ids) do
819 from(u in __MODULE__, where: u.id in ^ids)
823 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
824 # of the ap_id and the domain and tries to get that user
825 def get_by_guessed_nickname(ap_id) do
826 domain = URI.parse(ap_id).host
827 name = List.last(String.split(ap_id, "/"))
828 nickname = "#{name}@#{domain}"
830 get_cached_by_nickname(nickname)
833 def set_cache({:ok, user}), do: set_cache(user)
834 def set_cache({:error, err}), do: {:error, err}
836 def set_cache(%User{} = user) do
837 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
838 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
842 def update_and_set_cache(struct, params) do
844 |> update_changeset(params)
845 |> update_and_set_cache()
848 def update_and_set_cache(changeset) do
849 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
854 def invalidate_cache(user) do
855 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
856 Cachex.del(:user_cache, "nickname:#{user.nickname}")
859 @spec get_cached_by_ap_id(String.t()) :: User.t() | nil
860 def get_cached_by_ap_id(ap_id) do
861 key = "ap_id:#{ap_id}"
863 with {:ok, nil} <- Cachex.get(:user_cache, key),
864 user when not is_nil(user) <- get_by_ap_id(ap_id),
865 {:ok, true} <- Cachex.put(:user_cache, key, user) do
873 def get_cached_by_id(id) do
877 Cachex.fetch!(:user_cache, key, fn _ ->
881 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
882 {:commit, user.ap_id}
888 get_cached_by_ap_id(ap_id)
891 def get_cached_by_nickname(nickname) do
892 key = "nickname:#{nickname}"
894 Cachex.fetch!(:user_cache, key, fn ->
895 case get_or_fetch_by_nickname(nickname) do
896 {:ok, user} -> {:commit, user}
897 {:error, _error} -> {:ignore, nil}
902 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
903 restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
906 is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
907 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
909 restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
910 get_cached_by_nickname(nickname_or_id)
912 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
913 get_cached_by_nickname(nickname_or_id)
920 def get_by_nickname(nickname) do
921 Repo.get_by(User, nickname: nickname) ||
922 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
923 Repo.get_by(User, nickname: local_nickname(nickname))
927 def get_by_email(email), do: Repo.get_by(User, email: email)
929 def get_by_nickname_or_email(nickname_or_email) do
930 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
933 def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname)
935 def get_or_fetch_by_nickname(nickname) do
936 with %User{} = user <- get_by_nickname(nickname) do
940 with [_nick, _domain] <- String.split(nickname, "@"),
941 {:ok, user} <- fetch_by_nickname(nickname) do
944 _e -> {:error, "not found " <> nickname}
949 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
950 def get_followers_query(%User{} = user, nil) do
951 User.Query.build(%{followers: user, deactivated: false})
954 def get_followers_query(user, page) do
956 |> get_followers_query(nil)
957 |> User.Query.paginate(page, 20)
960 @spec get_followers_query(User.t()) :: Ecto.Query.t()
961 def get_followers_query(user), do: get_followers_query(user, nil)
963 @spec get_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
964 def get_followers(user, page \\ nil) do
966 |> get_followers_query(page)
970 @spec get_external_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
971 def get_external_followers(user, page \\ nil) do
973 |> get_followers_query(page)
974 |> User.Query.build(%{external: true})
978 def get_followers_ids(user, page \\ nil) do
980 |> get_followers_query(page)
985 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
986 def get_friends_query(%User{} = user, nil) do
987 User.Query.build(%{friends: user, deactivated: false})
990 def get_friends_query(user, page) do
992 |> get_friends_query(nil)
993 |> User.Query.paginate(page, 20)
996 @spec get_friends_query(User.t()) :: Ecto.Query.t()
997 def get_friends_query(user), do: get_friends_query(user, nil)
999 def get_friends(user, page \\ nil) do
1001 |> get_friends_query(page)
1005 def get_friends_ap_ids(user) do
1007 |> get_friends_query(nil)
1008 |> select([u], u.ap_id)
1012 def get_friends_ids(user, page \\ nil) do
1014 |> get_friends_query(page)
1015 |> select([u], u.id)
1019 defdelegate get_follow_requests(user), to: FollowingRelationship
1021 def increase_note_count(%User{} = user) do
1023 |> where(id: ^user.id)
1024 |> update([u], inc: [note_count: 1])
1026 |> Repo.update_all([])
1028 {1, [user]} -> set_cache(user)
1033 def decrease_note_count(%User{} = user) do
1035 |> where(id: ^user.id)
1038 note_count: fragment("greatest(0, note_count - 1)")
1042 |> Repo.update_all([])
1044 {1, [user]} -> set_cache(user)
1049 def update_note_count(%User{} = user, note_count \\ nil) do
1054 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
1060 |> cast(%{note_count: note_count}, [:note_count])
1061 |> update_and_set_cache()
1064 @spec maybe_fetch_follow_information(User.t()) :: User.t()
1065 def maybe_fetch_follow_information(user) do
1066 with {:ok, user} <- fetch_follow_information(user) do
1070 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
1076 def fetch_follow_information(user) do
1077 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
1079 |> follow_information_changeset(info)
1080 |> update_and_set_cache()
1084 defp follow_information_changeset(user, params) do
1091 :hide_followers_count,
1096 def update_follower_count(%User{} = user) do
1097 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
1098 follower_count_query =
1099 User.Query.build(%{followers: user, deactivated: false})
1100 |> select([u], %{count: count(u.id)})
1103 |> where(id: ^user.id)
1104 |> join(:inner, [u], s in subquery(follower_count_query))
1106 set: [follower_count: s.count]
1109 |> Repo.update_all([])
1111 {1, [user]} -> set_cache(user)
1115 {:ok, maybe_fetch_follow_information(user)}
1119 @spec update_following_count(User.t()) :: User.t()
1120 def update_following_count(%User{local: false} = user) do
1121 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
1122 maybe_fetch_follow_information(user)
1128 def update_following_count(%User{local: true} = user) do
1129 following_count = FollowingRelationship.following_count(user)
1132 |> follow_information_changeset(%{following_count: following_count})
1136 def set_unread_conversation_count(%User{local: true} = user) do
1137 unread_query = Participation.unread_conversation_count_for_user(user)
1140 |> join(:inner, [u], p in subquery(unread_query))
1142 set: [unread_conversation_count: p.count]
1144 |> where([u], u.id == ^user.id)
1146 |> Repo.update_all([])
1148 {1, [user]} -> set_cache(user)
1153 def set_unread_conversation_count(user), do: {:ok, user}
1155 def increment_unread_conversation_count(conversation, %User{local: true} = user) do
1157 Participation.unread_conversation_count_for_user(user)
1158 |> where([p], p.conversation_id == ^conversation.id)
1161 |> join(:inner, [u], p in subquery(unread_query))
1163 inc: [unread_conversation_count: 1]
1165 |> where([u], u.id == ^user.id)
1166 |> where([u, p], p.count == 0)
1168 |> Repo.update_all([])
1170 {1, [user]} -> set_cache(user)
1175 def increment_unread_conversation_count(_, user), do: {:ok, user}
1177 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
1178 def get_users_from_set(ap_ids, local_only \\ true) do
1179 criteria = %{ap_id: ap_ids, deactivated: false}
1180 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
1182 User.Query.build(criteria)
1186 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
1187 def get_recipients_from_activity(%Activity{recipients: to}) do
1188 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
1192 @spec mute(User.t(), User.t(), boolean()) ::
1193 {:ok, list(UserRelationship.t())} | {:error, String.t()}
1194 def mute(%User{} = muter, %User{} = mutee, notifications? \\ true) do
1195 add_to_mutes(muter, mutee, notifications?)
1198 def unmute(%User{} = muter, %User{} = mutee) do
1199 remove_from_mutes(muter, mutee)
1202 def subscribe(%User{} = subscriber, %User{} = target) do
1203 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
1205 if blocks?(target, subscriber) and deny_follow_blocked do
1206 {:error, "Could not subscribe: #{target.nickname} is blocking you"}
1208 # Note: the relationship is inverse: subscriber acts as relationship target
1209 UserRelationship.create_inverse_subscription(target, subscriber)
1213 def subscribe(%User{} = subscriber, %{ap_id: ap_id}) do
1214 with %User{} = subscribee <- get_cached_by_ap_id(ap_id) do
1215 subscribe(subscriber, subscribee)
1219 def unsubscribe(%User{} = unsubscriber, %User{} = target) do
1220 # Note: the relationship is inverse: subscriber acts as relationship target
1221 UserRelationship.delete_inverse_subscription(target, unsubscriber)
1224 def unsubscribe(%User{} = unsubscriber, %{ap_id: ap_id}) do
1225 with %User{} = user <- get_cached_by_ap_id(ap_id) do
1226 unsubscribe(unsubscriber, user)
1230 def block(%User{} = blocker, %User{} = blocked) do
1231 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
1233 if following?(blocker, blocked) do
1234 {:ok, blocker, _} = unfollow(blocker, blocked)
1240 # clear any requested follows as well
1242 case CommonAPI.reject_follow_request(blocked, blocker) do
1243 {:ok, %User{} = updated_blocked} -> updated_blocked
1247 unsubscribe(blocked, blocker)
1249 if following?(blocked, blocker), do: unfollow(blocked, blocker)
1251 {:ok, blocker} = update_follower_count(blocker)
1252 {:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
1253 add_to_block(blocker, blocked)
1256 # helper to handle the block given only an actor's AP id
1257 def block(%User{} = blocker, %{ap_id: ap_id}) do
1258 block(blocker, get_cached_by_ap_id(ap_id))
1261 def unblock(%User{} = blocker, %User{} = blocked) do
1262 remove_from_block(blocker, blocked)
1265 # helper to handle the block given only an actor's AP id
1266 def unblock(%User{} = blocker, %{ap_id: ap_id}) do
1267 unblock(blocker, get_cached_by_ap_id(ap_id))
1270 def mutes?(nil, _), do: false
1271 def mutes?(%User{} = user, %User{} = target), do: mutes_user?(user, target)
1273 def mutes_user?(%User{} = user, %User{} = target) do
1274 UserRelationship.mute_exists?(user, target)
1277 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
1278 def muted_notifications?(nil, _), do: false
1280 def muted_notifications?(%User{} = user, %User{} = target),
1281 do: UserRelationship.notification_mute_exists?(user, target)
1283 def blocks?(nil, _), do: false
1285 def blocks?(%User{} = user, %User{} = target) do
1286 blocks_user?(user, target) ||
1287 (!User.following?(user, target) && blocks_domain?(user, target))
1290 def blocks_user?(%User{} = user, %User{} = target) do
1291 UserRelationship.block_exists?(user, target)
1294 def blocks_user?(_, _), do: false
1296 def blocks_domain?(%User{} = user, %User{} = target) do
1297 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
1298 %{host: host} = URI.parse(target.ap_id)
1299 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1302 def blocks_domain?(_, _), do: false
1304 def subscribed_to?(%User{} = user, %User{} = target) do
1305 # Note: the relationship is inverse: subscriber acts as relationship target
1306 UserRelationship.inverse_subscription_exists?(target, user)
1309 def subscribed_to?(%User{} = user, %{ap_id: ap_id}) do
1310 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1311 subscribed_to?(user, target)
1316 Returns map of outgoing (blocked, muted etc.) relationships' user AP IDs by relation type.
1317 E.g. `outgoing_relationships_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}`
1319 @spec outgoing_relationships_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())}
1320 def outgoing_relationships_ap_ids(_user, []), do: %{}
1322 def outgoing_relationships_ap_ids(nil, _relationship_types), do: %{}
1324 def outgoing_relationships_ap_ids(%User{} = user, relationship_types)
1325 when is_list(relationship_types) do
1328 |> assoc(:outgoing_relationships)
1329 |> join(:inner, [user_rel], u in assoc(user_rel, :target))
1330 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1331 |> select([user_rel, u], [user_rel.relationship_type, fragment("array_agg(?)", u.ap_id)])
1332 |> group_by([user_rel, u], user_rel.relationship_type)
1334 |> Enum.into(%{}, fn [k, v] -> {k, v} end)
1339 fn rel_type -> {rel_type, db_result[rel_type] || []} end
1343 def incoming_relationships_ungrouped_ap_ids(user, relationship_types, ap_ids \\ nil)
1345 def incoming_relationships_ungrouped_ap_ids(_user, [], _ap_ids), do: []
1347 def incoming_relationships_ungrouped_ap_ids(nil, _relationship_types, _ap_ids), do: []
1349 def incoming_relationships_ungrouped_ap_ids(%User{} = user, relationship_types, ap_ids)
1350 when is_list(relationship_types) do
1352 |> assoc(:incoming_relationships)
1353 |> join(:inner, [user_rel], u in assoc(user_rel, :source))
1354 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1355 |> maybe_filter_on_ap_id(ap_ids)
1356 |> select([user_rel, u], u.ap_id)
1361 defp maybe_filter_on_ap_id(query, ap_ids) when is_list(ap_ids) do
1362 where(query, [user_rel, u], u.ap_id in ^ap_ids)
1365 defp maybe_filter_on_ap_id(query, _ap_ids), do: query
1367 def deactivate_async(user, status \\ true) do
1368 BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
1371 def deactivate(user, status \\ true)
1373 def deactivate(users, status) when is_list(users) do
1374 Repo.transaction(fn ->
1375 for user <- users, do: deactivate(user, status)
1379 def deactivate(%User{} = user, status) do
1380 with {:ok, user} <- set_activation_status(user, status) do
1383 |> Enum.filter(& &1.local)
1384 |> Enum.each(fn follower ->
1385 follower |> update_following_count() |> set_cache()
1388 # Only update local user counts, remote will be update during the next pull.
1391 |> Enum.filter(& &1.local)
1392 |> Enum.each(&update_follower_count/1)
1398 def update_notification_settings(%User{} = user, settings) do
1400 |> cast(%{notification_settings: settings}, [])
1401 |> cast_embed(:notification_settings)
1402 |> validate_required([:notification_settings])
1403 |> update_and_set_cache()
1406 def delete(users) when is_list(users) do
1407 for user <- users, do: delete(user)
1410 def delete(%User{} = user) do
1411 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1414 def perform(:force_password_reset, user), do: force_password_reset(user)
1416 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1417 def perform(:delete, %User{} = user) do
1418 {:ok, _user} = ActivityPub.delete(user)
1420 # Remove all relationships
1423 |> Enum.each(fn follower ->
1424 ActivityPub.unfollow(follower, user)
1425 unfollow(follower, user)
1430 |> Enum.each(fn followed ->
1431 ActivityPub.unfollow(user, followed)
1432 unfollow(user, followed)
1435 delete_user_activities(user)
1436 invalidate_cache(user)
1440 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1442 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1443 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1444 when is_list(blocked_identifiers) do
1446 blocked_identifiers,
1447 fn blocked_identifier ->
1448 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1449 {:ok, _user_block} <- block(blocker, blocked),
1450 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1454 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1461 def perform(:follow_import, %User{} = follower, followed_identifiers)
1462 when is_list(followed_identifiers) do
1464 followed_identifiers,
1465 fn followed_identifier ->
1466 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1467 {:ok, follower} <- maybe_direct_follow(follower, followed),
1468 {:ok, _} <- ActivityPub.follow(follower, followed) do
1472 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1479 @spec external_users_query() :: Ecto.Query.t()
1480 def external_users_query do
1488 @spec external_users(keyword()) :: [User.t()]
1489 def external_users(opts \\ []) do
1491 external_users_query()
1492 |> select([u], struct(u, [:id, :ap_id]))
1496 do: where(query, [u], u.id > ^opts[:max_id]),
1501 do: limit(query, ^opts[:limit]),
1507 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1508 BackgroundWorker.enqueue("blocks_import", %{
1509 "blocker_id" => blocker.id,
1510 "blocked_identifiers" => blocked_identifiers
1514 def follow_import(%User{} = follower, followed_identifiers)
1515 when is_list(followed_identifiers) do
1516 BackgroundWorker.enqueue("follow_import", %{
1517 "follower_id" => follower.id,
1518 "followed_identifiers" => followed_identifiers
1522 def delete_user_activities(%User{ap_id: ap_id}) do
1524 |> Activity.Queries.by_actor()
1525 |> RepoStreamer.chunk_stream(50)
1526 |> Stream.each(fn activities -> Enum.each(activities, &delete_activity/1) end)
1530 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1532 |> Object.normalize()
1533 |> ActivityPub.delete()
1536 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1537 object = Object.normalize(activity)
1540 |> get_cached_by_ap_id()
1541 |> ActivityPub.unlike(object)
1544 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1545 object = Object.normalize(activity)
1548 |> get_cached_by_ap_id()
1549 |> ActivityPub.unannounce(object)
1552 defp delete_activity(_activity), do: "Doing nothing"
1554 def html_filter_policy(%User{no_rich_text: true}) do
1555 Pleroma.HTML.Scrubber.TwitterText
1558 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1560 def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
1562 def get_or_fetch_by_ap_id(ap_id) do
1563 user = get_cached_by_ap_id(ap_id)
1565 if !is_nil(user) and !needs_update?(user) do
1568 fetch_by_ap_id(ap_id)
1573 Creates an internal service actor by URI if missing.
1574 Optionally takes nickname for addressing.
1576 @spec get_or_create_service_actor_by_ap_id(String.t(), String.t()) :: User.t() | nil
1577 def get_or_create_service_actor_by_ap_id(uri, nickname) do
1579 case get_cached_by_ap_id(uri) do
1581 with {:error, %{errors: errors}} <- create_service_actor(uri, nickname) do
1582 Logger.error("Cannot create service actor: #{uri}/.\n#{inspect(errors)}")
1586 %User{invisible: false} = user ->
1596 @spec set_invisible(User.t()) :: {:ok, User.t()}
1597 defp set_invisible(user) do
1599 |> change(%{invisible: true})
1600 |> update_and_set_cache()
1603 @spec create_service_actor(String.t(), String.t()) ::
1604 {:ok, User.t()} | {:error, Ecto.Changeset.t()}
1605 defp create_service_actor(uri, nickname) do
1611 follower_address: uri <> "/followers"
1614 |> unique_constraint(:nickname)
1620 def public_key(%{source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}}) do
1623 |> :public_key.pem_decode()
1625 |> :public_key.pem_entry_decode()
1630 def public_key(_), do: {:error, "not found key"}
1632 def get_public_key_for_ap_id(ap_id) do
1633 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1634 {:ok, public_key} <- public_key(user) do
1641 defp blank?(""), do: nil
1642 defp blank?(n), do: n
1644 def insert_or_update_user(data) do
1646 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1647 |> remote_user_creation()
1648 |> Repo.insert(on_conflict: {:replace_all_except, [:id]}, conflict_target: :nickname)
1652 def ap_enabled?(%User{local: true}), do: true
1653 def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled
1654 def ap_enabled?(_), do: false
1656 @doc "Gets or fetch a user by uri or nickname."
1657 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1658 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1659 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1661 # wait a period of time and return newest version of the User structs
1662 # this is because we have synchronous follow APIs and need to simulate them
1663 # with an async handshake
1664 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1665 with %User{} = a <- get_cached_by_id(a.id),
1666 %User{} = b <- get_cached_by_id(b.id) do
1673 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1674 with :ok <- :timer.sleep(timeout),
1675 %User{} = a <- get_cached_by_id(a.id),
1676 %User{} = b <- get_cached_by_id(b.id) do
1683 def parse_bio(bio) when is_binary(bio) and bio != "" do
1685 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1689 def parse_bio(_), do: ""
1691 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1692 # TODO: get profile URLs other than user.ap_id
1693 profile_urls = [user.ap_id]
1696 |> CommonUtils.format_input("text/plain",
1697 mentions_format: :full,
1698 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1703 def parse_bio(_, _), do: ""
1705 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1706 Repo.transaction(fn ->
1707 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1711 def tag(nickname, tags) when is_binary(nickname),
1712 do: tag(get_by_nickname(nickname), tags)
1714 def tag(%User{} = user, tags),
1715 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1717 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1718 Repo.transaction(fn ->
1719 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1723 def untag(nickname, tags) when is_binary(nickname),
1724 do: untag(get_by_nickname(nickname), tags)
1726 def untag(%User{} = user, tags),
1727 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1729 defp update_tags(%User{} = user, new_tags) do
1730 {:ok, updated_user} =
1732 |> change(%{tags: new_tags})
1733 |> update_and_set_cache()
1738 defp normalize_tags(tags) do
1741 |> Enum.map(&String.downcase/1)
1744 defp local_nickname_regex do
1745 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1746 @extended_local_nickname_regex
1748 @strict_local_nickname_regex
1752 def local_nickname(nickname_or_mention) do
1755 |> String.split("@")
1759 def full_nickname(nickname_or_mention),
1760 do: String.trim_leading(nickname_or_mention, "@")
1762 def error_user(ap_id) do
1766 nickname: "erroruser@example.com",
1767 inserted_at: NaiveDateTime.utc_now()
1771 @spec all_superusers() :: [User.t()]
1772 def all_superusers do
1773 User.Query.build(%{super_users: true, local: true, deactivated: false})
1777 def muting_reblogs?(%User{} = user, %User{} = target) do
1778 UserRelationship.reblog_mute_exists?(user, target)
1781 def showing_reblogs?(%User{} = user, %User{} = target) do
1782 not muting_reblogs?(user, target)
1786 The function returns a query to get users with no activity for given interval of days.
1787 Inactive users are those who didn't read any notification, or had any activity where
1788 the user is the activity's actor, during `inactivity_threshold` days.
1789 Deactivated users will not appear in this list.
1793 iex> Pleroma.User.list_inactive_users()
1796 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1797 def list_inactive_users_query(inactivity_threshold \\ 7) do
1798 negative_inactivity_threshold = -inactivity_threshold
1799 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1800 # Subqueries are not supported in `where` clauses, join gets too complicated.
1801 has_read_notifications =
1802 from(n in Pleroma.Notification,
1803 where: n.seen == true,
1805 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1808 |> Pleroma.Repo.all()
1810 from(u in Pleroma.User,
1811 left_join: a in Pleroma.Activity,
1812 on: u.ap_id == a.actor,
1813 where: not is_nil(u.nickname),
1814 where: u.deactivated != ^true,
1815 where: u.id not in ^has_read_notifications,
1818 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1819 is_nil(max(a.inserted_at))
1824 Enable or disable email notifications for user
1828 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => false}}, "digest", true)
1829 Pleroma.User{email_notifications: %{"digest" => true}}
1831 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => true}}, "digest", false)
1832 Pleroma.User{email_notifications: %{"digest" => false}}
1834 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1835 {:ok, t()} | {:error, Ecto.Changeset.t()}
1836 def switch_email_notifications(user, type, status) do
1837 User.update_email_notifications(user, %{type => status})
1841 Set `last_digest_emailed_at` value for the user to current time
1843 @spec touch_last_digest_emailed_at(t()) :: t()
1844 def touch_last_digest_emailed_at(user) do
1845 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1847 {:ok, updated_user} =
1849 |> change(%{last_digest_emailed_at: now})
1850 |> update_and_set_cache()
1855 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1856 def toggle_confirmation(%User{} = user) do
1858 |> confirmation_changeset(need_confirmation: !user.confirmation_pending)
1859 |> update_and_set_cache()
1862 @spec toggle_confirmation([User.t()]) :: [{:ok, User.t()} | {:error, Changeset.t()}]
1863 def toggle_confirmation(users) do
1864 Enum.map(users, &toggle_confirmation/1)
1867 def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
1871 def get_mascot(%{mascot: mascot}) when is_nil(mascot) do
1872 # use instance-default
1873 config = Pleroma.Config.get([:assets, :mascots])
1874 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1875 mascot = Keyword.get(config, default_mascot)
1878 "id" => "default-mascot",
1879 "url" => mascot[:url],
1880 "preview_url" => mascot[:url],
1882 "mime_type" => mascot[:mime_type]
1887 def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
1889 def ensure_keys_present(%User{} = user) do
1890 with {:ok, pem} <- Keys.generate_rsa_pem() do
1892 |> cast(%{keys: pem}, [:keys])
1893 |> validate_required([:keys])
1894 |> update_and_set_cache()
1898 def get_ap_ids_by_nicknames(nicknames) do
1900 where: u.nickname in ^nicknames,
1906 defdelegate search(query, opts \\ []), to: User.Search
1908 defp put_password_hash(
1909 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1911 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1914 defp put_password_hash(changeset), do: changeset
1916 def is_internal_user?(%User{nickname: nil}), do: true
1917 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1918 def is_internal_user?(_), do: false
1920 # A hack because user delete activities have a fake id for whatever reason
1921 # TODO: Get rid of this
1922 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
1924 def get_delivered_users_by_object_id(object_id) do
1926 inner_join: delivery in assoc(u, :deliveries),
1927 where: delivery.object_id == ^object_id
1932 def change_email(user, email) do
1934 |> cast(%{email: email}, [:email])
1935 |> validate_required([:email])
1936 |> unique_constraint(:email)
1937 |> validate_format(:email, @email_regex)
1938 |> update_and_set_cache()
1941 # Internal function; public one is `deactivate/2`
1942 defp set_activation_status(user, deactivated) do
1944 |> cast(%{deactivated: deactivated}, [:deactivated])
1945 |> update_and_set_cache()
1948 def update_banner(user, banner) do
1950 |> cast(%{banner: banner}, [:banner])
1951 |> update_and_set_cache()
1954 def update_background(user, background) do
1956 |> cast(%{background: background}, [:background])
1957 |> update_and_set_cache()
1960 def update_source_data(user, source_data) do
1962 |> cast(%{source_data: source_data}, [:source_data])
1963 |> update_and_set_cache()
1966 def roles(%{is_moderator: is_moderator, is_admin: is_admin}) do
1969 moderator: is_moderator
1973 # ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``.
1974 # For example: [{"name": "Pronoun", "value": "she/her"}, …]
1975 def fields(%{fields: nil, source_data: %{"attachment" => attachment}}) do
1976 limit = Pleroma.Config.get([:instance, :max_remote_account_fields], 0)
1979 |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
1980 |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
1984 def fields(%{fields: nil}), do: []
1986 def fields(%{fields: fields}), do: fields
1988 def validate_fields(changeset, remote? \\ false) do
1989 limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
1990 limit = Pleroma.Config.get([:instance, limit_name], 0)
1993 |> validate_length(:fields, max: limit)
1994 |> validate_change(:fields, fn :fields, fields ->
1995 if Enum.all?(fields, &valid_field?/1) do
2003 defp valid_field?(%{"name" => name, "value" => value}) do
2004 name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255)
2005 value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255)
2007 is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
2008 String.length(value) <= value_limit
2011 defp valid_field?(_), do: false
2013 defp truncate_field(%{"name" => name, "value" => value}) do
2015 String.split_at(name, Pleroma.Config.get([:instance, :account_field_name_length], 255))
2018 String.split_at(value, Pleroma.Config.get([:instance, :account_field_value_length], 255))
2020 %{"name" => name, "value" => value}
2023 def admin_api_update(user, params) do
2030 |> update_and_set_cache()
2033 @doc "Signs user out of all applications"
2034 def global_sign_out(user) do
2035 OAuth.Authorization.delete_user_authorizations(user)
2036 OAuth.Token.delete_user_tokens(user)
2039 def mascot_update(user, url) do
2041 |> cast(%{mascot: url}, [:mascot])
2042 |> validate_required([:mascot])
2043 |> update_and_set_cache()
2046 def mastodon_settings_update(user, settings) do
2048 |> cast(%{settings: settings}, [:settings])
2049 |> validate_required([:settings])
2050 |> update_and_set_cache()
2053 @spec confirmation_changeset(User.t(), keyword()) :: Changeset.t()
2054 def confirmation_changeset(user, need_confirmation: need_confirmation?) do
2056 if need_confirmation? do
2058 confirmation_pending: true,
2059 confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
2063 confirmation_pending: false,
2064 confirmation_token: nil
2068 cast(user, params, [:confirmation_pending, :confirmation_token])
2071 def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do
2072 if id not in user.pinned_activities do
2073 max_pinned_statuses = Pleroma.Config.get([:instance, :max_pinned_statuses], 0)
2074 params = %{pinned_activities: user.pinned_activities ++ [id]}
2077 |> cast(params, [:pinned_activities])
2078 |> validate_length(:pinned_activities,
2079 max: max_pinned_statuses,
2080 message: "You have already pinned the maximum number of statuses"
2085 |> update_and_set_cache()
2088 def remove_pinnned_activity(user, %Pleroma.Activity{id: id}) do
2089 params = %{pinned_activities: List.delete(user.pinned_activities, id)}
2092 |> cast(params, [:pinned_activities])
2093 |> update_and_set_cache()
2096 def update_email_notifications(user, settings) do
2097 email_notifications =
2098 user.email_notifications
2099 |> Map.merge(settings)
2100 |> Map.take(["digest"])
2102 params = %{email_notifications: email_notifications}
2103 fields = [:email_notifications]
2106 |> cast(params, fields)
2107 |> validate_required(fields)
2108 |> update_and_set_cache()
2111 defp set_domain_blocks(user, domain_blocks) do
2112 params = %{domain_blocks: domain_blocks}
2115 |> cast(params, [:domain_blocks])
2116 |> validate_required([:domain_blocks])
2117 |> update_and_set_cache()
2120 def block_domain(user, domain_blocked) do
2121 set_domain_blocks(user, Enum.uniq([domain_blocked | user.domain_blocks]))
2124 def unblock_domain(user, domain_blocked) do
2125 set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked))
2128 @spec add_to_block(User.t(), User.t()) ::
2129 {:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()}
2130 defp add_to_block(%User{} = user, %User{} = blocked) do
2131 UserRelationship.create_block(user, blocked)
2134 @spec add_to_block(User.t(), User.t()) ::
2135 {:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()}
2136 defp remove_from_block(%User{} = user, %User{} = blocked) do
2137 UserRelationship.delete_block(user, blocked)
2140 defp add_to_mutes(%User{} = user, %User{} = muted_user, notifications?) do
2141 with {:ok, user_mute} <- UserRelationship.create_mute(user, muted_user),
2142 {:ok, user_notification_mute} <-
2143 (notifications? && UserRelationship.create_notification_mute(user, muted_user)) ||
2145 {:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
2149 defp remove_from_mutes(user, %User{} = muted_user) do
2150 with {:ok, user_mute} <- UserRelationship.delete_mute(user, muted_user),
2151 {:ok, user_notification_mute} <-
2152 UserRelationship.delete_notification_mute(user, muted_user) do
2153 {:ok, [user_mute, user_notification_mute]}
2157 def set_invisible(user, invisible) do
2158 params = %{invisible: invisible}
2161 |> cast(params, [:invisible])
2162 |> validate_required([:invisible])
2163 |> update_and_set_cache()
2166 def sanitize_html(%User{} = user) do
2167 sanitize_html(user, nil)
2170 # User data that mastodon isn't filtering (treated as plaintext):
2173 def sanitize_html(%User{} = user, filter) do
2177 |> Enum.map(fn %{"name" => name, "value" => value} ->
2180 "value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
2185 |> Map.put(:bio, HTML.filter_tags(user.bio, filter))
2186 |> Map.put(:fields, fields)