1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.User do
10 import Ecto, only: [assoc: 2]
14 alias Pleroma.Activity
16 alias Pleroma.Conversation.Participation
17 alias Pleroma.Delivery
18 alias Pleroma.FollowingRelationship
21 alias Pleroma.Notification
23 alias Pleroma.Registration
25 alias Pleroma.RepoStreamer
27 alias Pleroma.UserRelationship
29 alias Pleroma.Web.ActivityPub.ActivityPub
30 alias Pleroma.Web.ActivityPub.Utils
31 alias Pleroma.Web.CommonAPI
32 alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
33 alias Pleroma.Web.OAuth
34 alias Pleroma.Web.RelMe
35 alias Pleroma.Workers.BackgroundWorker
39 @type t :: %__MODULE__{}
40 @type account_status :: :active | :deactivated | :password_reset_pending | :confirmation_pending
41 @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
43 # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
44 @email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
46 @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
47 @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
49 # AP ID user relationships (blocks, mutes etc.)
50 # Format: [rel_type: [outgoing_rel: :outgoing_rel_target, incoming_rel: :incoming_rel_source]]
51 @user_relationships_config [
53 blocker_blocks: :blocked_users,
54 blockee_blocks: :blocker_users
57 muter_mutes: :muted_users,
58 mutee_mutes: :muter_users
61 reblog_muter_mutes: :reblog_muted_users,
62 reblog_mutee_mutes: :reblog_muter_users
65 notification_muter_mutes: :notification_muted_users,
66 notification_mutee_mutes: :notification_muter_users
68 # Note: `inverse_subscription` relationship is inverse: subscriber acts as relationship target
69 inverse_subscription: [
70 subscribee_subscriptions: :subscriber_users,
71 subscriber_subscriptions: :subscribee_users
77 field(:email, :string)
79 field(:nickname, :string)
80 field(:password_hash, :string)
81 field(:password, :string, virtual: true)
82 field(:password_confirmation, :string, virtual: true)
84 field(:ap_id, :string)
86 field(:local, :boolean, default: true)
87 field(:follower_address, :string)
88 field(:following_address, :string)
89 field(:search_rank, :float, virtual: true)
90 field(:search_type, :integer, virtual: true)
91 field(:tags, {:array, :string}, default: [])
92 field(:last_refreshed_at, :naive_datetime_usec)
93 field(:last_digest_emailed_at, :naive_datetime)
94 field(:banner, :map, default: %{})
95 field(:background, :map, default: %{})
96 field(:source_data, :map, default: %{})
97 field(:note_count, :integer, default: 0)
98 field(:follower_count, :integer, default: 0)
99 field(:following_count, :integer, default: 0)
100 field(:locked, :boolean, default: false)
101 field(:confirmation_pending, :boolean, default: false)
102 field(:password_reset_pending, :boolean, default: false)
103 field(:confirmation_token, :string, default: nil)
104 field(:default_scope, :string, default: "public")
105 field(:domain_blocks, {:array, :string}, default: [])
106 field(:deactivated, :boolean, default: false)
107 field(:no_rich_text, :boolean, default: false)
108 field(:ap_enabled, :boolean, default: false)
109 field(:is_moderator, :boolean, default: false)
110 field(:is_admin, :boolean, default: false)
111 field(:show_role, :boolean, default: true)
112 field(:settings, :map, default: nil)
113 field(:magic_key, :string, default: nil)
114 field(:uri, :string, default: nil)
115 field(:hide_followers_count, :boolean, default: false)
116 field(:hide_follows_count, :boolean, default: false)
117 field(:hide_followers, :boolean, default: false)
118 field(:hide_follows, :boolean, default: false)
119 field(:hide_favorites, :boolean, default: true)
120 field(:unread_conversation_count, :integer, default: 0)
121 field(:pinned_activities, {:array, :string}, default: [])
122 field(:email_notifications, :map, default: %{"digest" => false})
123 field(:mascot, :map, default: nil)
124 field(:emoji, {:array, :map}, default: [])
125 field(:pleroma_settings_store, :map, default: %{})
126 field(:fields, {:array, :map}, default: [])
127 field(:raw_fields, {:array, :map}, default: [])
128 field(:discoverable, :boolean, default: false)
129 field(:invisible, :boolean, default: false)
130 field(:allow_following_move, :boolean, default: true)
131 field(:skip_thread_containment, :boolean, default: false)
132 field(:actor_type, :string, default: "Person")
133 field(:also_known_as, {:array, :string}, default: [])
136 :notification_settings,
137 Pleroma.User.NotificationSetting,
141 has_many(:notifications, Notification)
142 has_many(:registrations, Registration)
143 has_many(:deliveries, Delivery)
145 has_many(:outgoing_relationships, UserRelationship, foreign_key: :source_id)
146 has_many(:incoming_relationships, UserRelationship, foreign_key: :target_id)
148 for {relationship_type,
150 {outgoing_relation, outgoing_relation_target},
151 {incoming_relation, incoming_relation_source}
152 ]} <- @user_relationships_config do
153 # Definitions of `has_many` relations: :blocker_blocks, :muter_mutes, :reblog_muter_mutes,
154 # :notification_muter_mutes, :subscribee_subscriptions
155 has_many(outgoing_relation, UserRelationship,
156 foreign_key: :source_id,
157 where: [relationship_type: relationship_type]
160 # Definitions of `has_many` relations: :blockee_blocks, :mutee_mutes, :reblog_mutee_mutes,
161 # :notification_mutee_mutes, :subscriber_subscriptions
162 has_many(incoming_relation, UserRelationship,
163 foreign_key: :target_id,
164 where: [relationship_type: relationship_type]
167 # Definitions of `has_many` relations: :blocked_users, :muted_users, :reblog_muted_users,
168 # :notification_muted_users, :subscriber_users
169 has_many(outgoing_relation_target, through: [outgoing_relation, :target])
171 # Definitions of `has_many` relations: :blocker_users, :muter_users, :reblog_muter_users,
172 # :notification_muter_users, :subscribee_users
173 has_many(incoming_relation_source, through: [incoming_relation, :source])
176 # `:blocks` is deprecated (replaced with `blocked_users` relation)
177 field(:blocks, {:array, :string}, default: [])
178 # `:mutes` is deprecated (replaced with `muted_users` relation)
179 field(:mutes, {:array, :string}, default: [])
180 # `:muted_reblogs` is deprecated (replaced with `reblog_muted_users` relation)
181 field(:muted_reblogs, {:array, :string}, default: [])
182 # `:muted_notifications` is deprecated (replaced with `notification_muted_users` relation)
183 field(:muted_notifications, {:array, :string}, default: [])
184 # `:subscribers` is deprecated (replaced with `subscriber_users` relation)
185 field(:subscribers, {:array, :string}, default: [])
190 for {_relationship_type, [{_outgoing_relation, outgoing_relation_target}, _]} <-
191 @user_relationships_config do
192 # `def blocked_users_relation/2`, `def muted_users_relation/2`,
193 # `def reblog_muted_users_relation/2`, `def notification_muted_users/2`,
194 # `def subscriber_users/2`
195 def unquote(:"#{outgoing_relation_target}_relation")(user, restrict_deactivated? \\ false) do
196 target_users_query = assoc(user, unquote(outgoing_relation_target))
198 if restrict_deactivated? do
199 restrict_deactivated(target_users_query)
205 # `def blocked_users/2`, `def muted_users/2`, `def reblog_muted_users/2`,
206 # `def notification_muted_users/2`, `def subscriber_users/2`
207 def unquote(outgoing_relation_target)(user, restrict_deactivated? \\ false) do
209 |> apply(unquote(:"#{outgoing_relation_target}_relation"), [
211 restrict_deactivated?
216 # `def blocked_users_ap_ids/2`, `def muted_users_ap_ids/2`, `def reblog_muted_users_ap_ids/2`,
217 # `def notification_muted_users_ap_ids/2`, `def subscriber_users_ap_ids/2`
218 def unquote(:"#{outgoing_relation_target}_ap_ids")(user, restrict_deactivated? \\ false) do
220 |> apply(unquote(:"#{outgoing_relation_target}_relation"), [
222 restrict_deactivated?
224 |> select([u], u.ap_id)
230 Dumps Flake Id to SQL-compatible format (16-byte UUID).
231 E.g. "9pQtDGXuq4p3VlcJEm" -> <<0, 0, 1, 110, 179, 218, 42, 92, 213, 41, 44, 227, 95, 213, 0, 0>>
233 def binary_id(source_id) when is_binary(source_id) do
234 with {:ok, dumped_id} <- FlakeId.Ecto.CompatType.dump(source_id) do
241 def binary_id(source_ids) when is_list(source_ids) do
242 Enum.map(source_ids, &binary_id/1)
245 def binary_id(%User{} = user), do: binary_id(user.id)
247 @doc "Returns status account"
248 @spec account_status(User.t()) :: account_status()
249 def account_status(%User{deactivated: true}), do: :deactivated
250 def account_status(%User{password_reset_pending: true}), do: :password_reset_pending
252 def account_status(%User{confirmation_pending: true}) do
253 case Config.get([:instance, :account_activation_required]) do
254 true -> :confirmation_pending
259 def account_status(%User{}), do: :active
261 @spec visible_for?(User.t(), User.t() | nil) :: boolean()
262 def visible_for?(user, for_user \\ nil)
264 def visible_for?(%User{invisible: true}, _), do: false
266 def visible_for?(%User{id: user_id}, %User{id: user_id}), do: true
268 def visible_for?(%User{local: local} = user, nil) do
274 if Config.get([:restrict_unauthenticated, :profiles, cfg_key]),
276 else: account_status(user) == :active
279 def visible_for?(%User{} = user, for_user) do
280 account_status(user) == :active || superuser?(for_user)
283 def visible_for?(_, _), do: false
285 @spec superuser?(User.t()) :: boolean()
286 def superuser?(%User{local: true, is_admin: true}), do: true
287 def superuser?(%User{local: true, is_moderator: true}), do: true
288 def superuser?(_), do: false
290 @spec invisible?(User.t()) :: boolean()
291 def invisible?(%User{invisible: true}), do: true
292 def invisible?(_), do: false
294 def avatar_url(user, options \\ []) do
296 %{"url" => [%{"href" => href} | _]} -> href
297 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
301 def banner_url(user, options \\ []) do
303 %{"url" => [%{"href" => href} | _]} -> href
304 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
308 def profile_url(%User{uri: url}) when url != nil, do: url
309 def profile_url(%User{source_data: %{"url" => url}}) when is_binary(url), do: url
310 def profile_url(%User{ap_id: ap_id}), do: ap_id
311 def profile_url(_), do: nil
313 def ap_id(%User{nickname: nickname}), do: "#{Web.base_url()}/users/#{nickname}"
315 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
316 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
318 @spec ap_following(User.t()) :: String.t()
319 def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
320 def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
322 @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
323 def restrict_deactivated(query) do
324 from(u in query, where: u.deactivated != ^true)
327 defdelegate following_count(user), to: FollowingRelationship
329 defp truncate_fields_param(params) do
330 if Map.has_key?(params, :fields) do
331 Map.put(params, :fields, Enum.map(params[:fields], &truncate_field/1))
337 defp truncate_if_exists(params, key, max_length) do
338 if Map.has_key?(params, key) and is_binary(params[key]) do
339 {value, _chopped} = String.split_at(params[key], max_length)
340 Map.put(params, key, value)
346 def remote_user_creation(params) do
347 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
348 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
352 |> truncate_if_exists(:name, name_limit)
353 |> truncate_if_exists(:bio, bio_limit)
354 |> truncate_fields_param()
374 :hide_followers_count,
385 |> validate_required([:name, :ap_id])
386 |> unique_constraint(:nickname)
387 |> validate_format(:nickname, @email_regex)
388 |> validate_length(:bio, max: bio_limit)
389 |> validate_length(:name, max: name_limit)
390 |> validate_fields(true)
392 case params[:source_data] do
393 %{"followers" => followers, "following" => following} ->
395 |> put_change(:follower_address, followers)
396 |> put_change(:following_address, following)
399 followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
400 put_change(changeset, :follower_address, followers)
404 def update_changeset(struct, params \\ %{}) do
405 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
406 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
421 :hide_followers_count,
424 :allow_following_move,
427 :skip_thread_containment,
430 :pleroma_settings_store,
436 |> unique_constraint(:nickname)
437 |> validate_format(:nickname, local_nickname_regex())
438 |> validate_length(:bio, max: bio_limit)
439 |> validate_length(:name, min: 1, max: name_limit)
441 |> put_change_if_present(:bio, &{:ok, parse_bio(&1, struct)})
442 |> put_change_if_present(:avatar, &put_upload(&1, :avatar))
443 |> put_change_if_present(:banner, &put_upload(&1, :banner))
444 |> put_change_if_present(:background, &put_upload(&1, :background))
445 |> put_change_if_present(
446 :pleroma_settings_store,
447 &{:ok, Map.merge(struct.pleroma_settings_store, &1)}
449 |> validate_fields(false)
452 defp put_fields(changeset) do
453 if raw_fields = get_change(changeset, :raw_fields) do
456 |> Enum.filter(fn %{"name" => n} -> n != "" end)
460 |> Enum.map(fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
463 |> put_change(:raw_fields, raw_fields)
464 |> put_change(:fields, fields)
470 defp put_change_if_present(changeset, map_field, value_function) do
471 if value = get_change(changeset, map_field) do
472 with {:ok, new_value} <- value_function.(value) do
473 put_change(changeset, map_field, new_value)
482 defp put_upload(value, type) do
483 with %Plug.Upload{} <- value,
484 {:ok, object} <- ActivityPub.upload(value, type: type) do
489 def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
490 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
491 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
493 params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
495 params = if remote?, do: truncate_fields_param(params), else: params
517 :allow_following_move,
519 :hide_followers_count,
525 |> unique_constraint(:nickname)
526 |> validate_format(:nickname, local_nickname_regex())
527 |> validate_length(:bio, max: bio_limit)
528 |> validate_length(:name, max: name_limit)
529 |> validate_fields(remote?)
532 def update_as_admin_changeset(struct, params) do
534 |> update_changeset(params)
535 |> cast(params, [:email])
536 |> delete_change(:also_known_as)
537 |> unique_constraint(:email)
538 |> validate_format(:email, @email_regex)
541 @spec update_as_admin(%User{}, map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
542 def update_as_admin(user, params) do
543 params = Map.put(params, "password_confirmation", params["password"])
544 changeset = update_as_admin_changeset(user, params)
546 if params["password"] do
547 reset_password(user, changeset, params)
549 User.update_and_set_cache(changeset)
553 def password_update_changeset(struct, params) do
555 |> cast(params, [:password, :password_confirmation])
556 |> validate_required([:password, :password_confirmation])
557 |> validate_confirmation(:password)
558 |> put_password_hash()
559 |> put_change(:password_reset_pending, false)
562 @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
563 def reset_password(%User{} = user, params) do
564 reset_password(user, user, params)
567 def reset_password(%User{id: user_id} = user, struct, params) do
570 |> Multi.update(:user, password_update_changeset(struct, params))
571 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
572 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
574 case Repo.transaction(multi) do
575 {:ok, %{user: user} = _} -> set_cache(user)
576 {:error, _, changeset, _} -> {:error, changeset}
580 def update_password_reset_pending(user, value) do
583 |> put_change(:password_reset_pending, value)
584 |> update_and_set_cache()
587 def force_password_reset_async(user) do
588 BackgroundWorker.enqueue("force_password_reset", %{"user_id" => user.id})
591 @spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
592 def force_password_reset(user), do: update_password_reset_pending(user, true)
594 def register_changeset(struct, params \\ %{}, opts \\ []) do
595 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
596 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
599 if is_nil(opts[:need_confirmation]) do
600 Pleroma.Config.get([:instance, :account_activation_required])
602 opts[:need_confirmation]
606 |> confirmation_changeset(need_confirmation: need_confirmation?)
607 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
608 |> validate_required([:name, :nickname, :password, :password_confirmation])
609 |> validate_confirmation(:password)
610 |> unique_constraint(:email)
611 |> unique_constraint(:nickname)
612 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
613 |> validate_format(:nickname, local_nickname_regex())
614 |> validate_format(:email, @email_regex)
615 |> validate_length(:bio, max: bio_limit)
616 |> validate_length(:name, min: 1, max: name_limit)
617 |> maybe_validate_required_email(opts[:external])
620 |> unique_constraint(:ap_id)
621 |> put_following_and_follower_address()
624 def maybe_validate_required_email(changeset, true), do: changeset
626 def maybe_validate_required_email(changeset, _) do
627 if Pleroma.Config.get([:instance, :account_activation_required]) do
628 validate_required(changeset, [:email])
634 defp put_ap_id(changeset) do
635 ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
636 put_change(changeset, :ap_id, ap_id)
639 defp put_following_and_follower_address(changeset) do
640 followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
643 |> put_change(:follower_address, followers)
646 defp autofollow_users(user) do
647 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
650 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
653 follow_all(user, autofollowed_users)
656 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
657 def register(%Ecto.Changeset{} = changeset) do
658 with {:ok, user} <- Repo.insert(changeset) do
659 post_register_action(user)
663 def post_register_action(%User{} = user) do
664 with {:ok, user} <- autofollow_users(user),
665 {:ok, user} <- set_cache(user),
666 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
667 {:ok, _} <- try_send_confirmation_email(user) do
672 def try_send_confirmation_email(%User{} = user) do
673 if user.confirmation_pending &&
674 Pleroma.Config.get([:instance, :account_activation_required]) do
676 |> Pleroma.Emails.UserEmail.account_confirmation_email()
677 |> Pleroma.Emails.Mailer.deliver_async()
685 def try_send_confirmation_email(users) do
686 Enum.each(users, &try_send_confirmation_email/1)
689 def needs_update?(%User{local: true}), do: false
691 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
693 def needs_update?(%User{local: false} = user) do
694 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
697 def needs_update?(_), do: true
699 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
700 def maybe_direct_follow(%User{} = follower, %User{local: true, locked: true} = followed) do
701 follow(follower, followed, "pending")
704 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
705 follow(follower, followed)
708 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
709 if not ap_enabled?(followed) do
710 follow(follower, followed)
716 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
717 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
718 def follow_all(follower, followeds) do
720 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
721 |> Enum.each(&follow(follower, &1, "accept"))
726 defdelegate following(user), to: FollowingRelationship
728 def follow(%User{} = follower, %User{} = followed, state \\ "accept") do
729 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
732 followed.deactivated ->
733 {:error, "Could not follow user: #{followed.nickname} is deactivated."}
735 deny_follow_blocked and blocks?(followed, follower) ->
736 {:error, "Could not follow user: #{followed.nickname} blocked you."}
739 FollowingRelationship.follow(follower, followed, state)
741 {:ok, _} = update_follower_count(followed)
744 |> update_following_count()
749 def unfollow(%User{ap_id: ap_id}, %User{ap_id: ap_id}) do
750 {:error, "Not subscribed!"}
753 def unfollow(%User{} = follower, %User{} = followed) do
754 case get_follow_state(follower, followed) do
755 state when state in ["accept", "pending"] ->
756 FollowingRelationship.unfollow(follower, followed)
757 {:ok, followed} = update_follower_count(followed)
761 |> update_following_count()
764 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
767 {:error, "Not subscribed!"}
771 defdelegate following?(follower, followed), to: FollowingRelationship
773 def get_follow_state(%User{} = follower, %User{} = following) do
774 following_relationship = FollowingRelationship.get(follower, following)
775 get_follow_state(follower, following, following_relationship)
778 def get_follow_state(
781 following_relationship
783 case {following_relationship, following.local} do
785 case Utils.fetch_latest_follow(follower, following) do
786 %{data: %{"state" => state}} when state in ["pending", "accept"] -> state
790 {%{state: state}, _} ->
798 def locked?(%User{} = user) do
803 Repo.get_by(User, id: id)
806 def get_by_ap_id(ap_id) do
807 Repo.get_by(User, ap_id: ap_id)
810 def get_all_by_ap_id(ap_ids) do
811 from(u in __MODULE__,
812 where: u.ap_id in ^ap_ids
817 def get_all_by_ids(ids) do
818 from(u in __MODULE__, where: u.id in ^ids)
822 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
823 # of the ap_id and the domain and tries to get that user
824 def get_by_guessed_nickname(ap_id) do
825 domain = URI.parse(ap_id).host
826 name = List.last(String.split(ap_id, "/"))
827 nickname = "#{name}@#{domain}"
829 get_cached_by_nickname(nickname)
832 def set_cache({:ok, user}), do: set_cache(user)
833 def set_cache({:error, err}), do: {:error, err}
835 def set_cache(%User{} = user) do
836 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
837 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
841 def update_and_set_cache(struct, params) do
843 |> update_changeset(params)
844 |> update_and_set_cache()
847 def update_and_set_cache(changeset) do
848 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
853 def invalidate_cache(user) do
854 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
855 Cachex.del(:user_cache, "nickname:#{user.nickname}")
858 @spec get_cached_by_ap_id(String.t()) :: User.t() | nil
859 def get_cached_by_ap_id(ap_id) do
860 key = "ap_id:#{ap_id}"
862 with {:ok, nil} <- Cachex.get(:user_cache, key),
863 user when not is_nil(user) <- get_by_ap_id(ap_id),
864 {:ok, true} <- Cachex.put(:user_cache, key, user) do
872 def get_cached_by_id(id) do
876 Cachex.fetch!(:user_cache, key, fn _ ->
880 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
881 {:commit, user.ap_id}
887 get_cached_by_ap_id(ap_id)
890 def get_cached_by_nickname(nickname) do
891 key = "nickname:#{nickname}"
893 Cachex.fetch!(:user_cache, key, fn ->
894 case get_or_fetch_by_nickname(nickname) do
895 {:ok, user} -> {:commit, user}
896 {:error, _error} -> {:ignore, nil}
901 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
902 restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
905 is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
906 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
908 restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
909 get_cached_by_nickname(nickname_or_id)
911 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
912 get_cached_by_nickname(nickname_or_id)
919 def get_by_nickname(nickname) do
920 Repo.get_by(User, nickname: nickname) ||
921 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
922 Repo.get_by(User, nickname: local_nickname(nickname))
926 def get_by_email(email), do: Repo.get_by(User, email: email)
928 def get_by_nickname_or_email(nickname_or_email) do
929 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
932 def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname)
934 def get_or_fetch_by_nickname(nickname) do
935 with %User{} = user <- get_by_nickname(nickname) do
939 with [_nick, _domain] <- String.split(nickname, "@"),
940 {:ok, user} <- fetch_by_nickname(nickname) do
943 _e -> {:error, "not found " <> nickname}
948 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
949 def get_followers_query(%User{} = user, nil) do
950 User.Query.build(%{followers: user, deactivated: false})
953 def get_followers_query(user, page) do
955 |> get_followers_query(nil)
956 |> User.Query.paginate(page, 20)
959 @spec get_followers_query(User.t()) :: Ecto.Query.t()
960 def get_followers_query(user), do: get_followers_query(user, nil)
962 @spec get_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
963 def get_followers(user, page \\ nil) do
965 |> get_followers_query(page)
969 @spec get_external_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
970 def get_external_followers(user, page \\ nil) do
972 |> get_followers_query(page)
973 |> User.Query.build(%{external: true})
977 def get_followers_ids(user, page \\ nil) do
979 |> get_followers_query(page)
984 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
985 def get_friends_query(%User{} = user, nil) do
986 User.Query.build(%{friends: user, deactivated: false})
989 def get_friends_query(user, page) do
991 |> get_friends_query(nil)
992 |> User.Query.paginate(page, 20)
995 @spec get_friends_query(User.t()) :: Ecto.Query.t()
996 def get_friends_query(user), do: get_friends_query(user, nil)
998 def get_friends(user, page \\ nil) do
1000 |> get_friends_query(page)
1004 def get_friends_ap_ids(user) do
1006 |> get_friends_query(nil)
1007 |> select([u], u.ap_id)
1011 def get_friends_ids(user, page \\ nil) do
1013 |> get_friends_query(page)
1014 |> select([u], u.id)
1018 defdelegate get_follow_requests(user), to: FollowingRelationship
1020 def increase_note_count(%User{} = user) do
1022 |> where(id: ^user.id)
1023 |> update([u], inc: [note_count: 1])
1025 |> Repo.update_all([])
1027 {1, [user]} -> set_cache(user)
1032 def decrease_note_count(%User{} = user) do
1034 |> where(id: ^user.id)
1037 note_count: fragment("greatest(0, note_count - 1)")
1041 |> Repo.update_all([])
1043 {1, [user]} -> set_cache(user)
1048 def update_note_count(%User{} = user, note_count \\ nil) do
1053 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
1059 |> cast(%{note_count: note_count}, [:note_count])
1060 |> update_and_set_cache()
1063 @spec maybe_fetch_follow_information(User.t()) :: User.t()
1064 def maybe_fetch_follow_information(user) do
1065 with {:ok, user} <- fetch_follow_information(user) do
1069 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
1075 def fetch_follow_information(user) do
1076 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
1078 |> follow_information_changeset(info)
1079 |> update_and_set_cache()
1083 defp follow_information_changeset(user, params) do
1090 :hide_followers_count,
1095 def update_follower_count(%User{} = user) do
1096 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
1097 follower_count_query =
1098 User.Query.build(%{followers: user, deactivated: false})
1099 |> select([u], %{count: count(u.id)})
1102 |> where(id: ^user.id)
1103 |> join(:inner, [u], s in subquery(follower_count_query))
1105 set: [follower_count: s.count]
1108 |> Repo.update_all([])
1110 {1, [user]} -> set_cache(user)
1114 {:ok, maybe_fetch_follow_information(user)}
1118 @spec update_following_count(User.t()) :: User.t()
1119 def update_following_count(%User{local: false} = user) do
1120 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
1121 maybe_fetch_follow_information(user)
1127 def update_following_count(%User{local: true} = user) do
1128 following_count = FollowingRelationship.following_count(user)
1131 |> follow_information_changeset(%{following_count: following_count})
1135 def set_unread_conversation_count(%User{local: true} = user) do
1136 unread_query = Participation.unread_conversation_count_for_user(user)
1139 |> join(:inner, [u], p in subquery(unread_query))
1141 set: [unread_conversation_count: p.count]
1143 |> where([u], u.id == ^user.id)
1145 |> Repo.update_all([])
1147 {1, [user]} -> set_cache(user)
1152 def set_unread_conversation_count(user), do: {:ok, user}
1154 def increment_unread_conversation_count(conversation, %User{local: true} = user) do
1156 Participation.unread_conversation_count_for_user(user)
1157 |> where([p], p.conversation_id == ^conversation.id)
1160 |> join(:inner, [u], p in subquery(unread_query))
1162 inc: [unread_conversation_count: 1]
1164 |> where([u], u.id == ^user.id)
1165 |> where([u, p], p.count == 0)
1167 |> Repo.update_all([])
1169 {1, [user]} -> set_cache(user)
1174 def increment_unread_conversation_count(_, user), do: {:ok, user}
1176 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
1177 def get_users_from_set(ap_ids, local_only \\ true) do
1178 criteria = %{ap_id: ap_ids, deactivated: false}
1179 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
1181 User.Query.build(criteria)
1185 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
1186 def get_recipients_from_activity(%Activity{recipients: to}) do
1187 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
1191 @spec mute(User.t(), User.t(), boolean()) ::
1192 {:ok, list(UserRelationship.t())} | {:error, String.t()}
1193 def mute(%User{} = muter, %User{} = mutee, notifications? \\ true) do
1194 add_to_mutes(muter, mutee, notifications?)
1197 def unmute(%User{} = muter, %User{} = mutee) do
1198 remove_from_mutes(muter, mutee)
1201 def subscribe(%User{} = subscriber, %User{} = target) do
1202 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
1204 if blocks?(target, subscriber) and deny_follow_blocked do
1205 {:error, "Could not subscribe: #{target.nickname} is blocking you"}
1207 # Note: the relationship is inverse: subscriber acts as relationship target
1208 UserRelationship.create_inverse_subscription(target, subscriber)
1212 def subscribe(%User{} = subscriber, %{ap_id: ap_id}) do
1213 with %User{} = subscribee <- get_cached_by_ap_id(ap_id) do
1214 subscribe(subscriber, subscribee)
1218 def unsubscribe(%User{} = unsubscriber, %User{} = target) do
1219 # Note: the relationship is inverse: subscriber acts as relationship target
1220 UserRelationship.delete_inverse_subscription(target, unsubscriber)
1223 def unsubscribe(%User{} = unsubscriber, %{ap_id: ap_id}) do
1224 with %User{} = user <- get_cached_by_ap_id(ap_id) do
1225 unsubscribe(unsubscriber, user)
1229 def block(%User{} = blocker, %User{} = blocked) do
1230 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
1232 if following?(blocker, blocked) do
1233 {:ok, blocker, _} = unfollow(blocker, blocked)
1239 # clear any requested follows as well
1241 case CommonAPI.reject_follow_request(blocked, blocker) do
1242 {:ok, %User{} = updated_blocked} -> updated_blocked
1246 unsubscribe(blocked, blocker)
1248 if following?(blocked, blocker), do: unfollow(blocked, blocker)
1250 {:ok, blocker} = update_follower_count(blocker)
1251 {:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
1252 add_to_block(blocker, blocked)
1255 # helper to handle the block given only an actor's AP id
1256 def block(%User{} = blocker, %{ap_id: ap_id}) do
1257 block(blocker, get_cached_by_ap_id(ap_id))
1260 def unblock(%User{} = blocker, %User{} = blocked) do
1261 remove_from_block(blocker, blocked)
1264 # helper to handle the block given only an actor's AP id
1265 def unblock(%User{} = blocker, %{ap_id: ap_id}) do
1266 unblock(blocker, get_cached_by_ap_id(ap_id))
1269 def mutes?(nil, _), do: false
1270 def mutes?(%User{} = user, %User{} = target), do: mutes_user?(user, target)
1272 def mutes_user?(%User{} = user, %User{} = target) do
1273 UserRelationship.mute_exists?(user, target)
1276 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
1277 def muted_notifications?(nil, _), do: false
1279 def muted_notifications?(%User{} = user, %User{} = target),
1280 do: UserRelationship.notification_mute_exists?(user, target)
1282 def blocks?(nil, _), do: false
1284 def blocks?(%User{} = user, %User{} = target) do
1285 blocks_user?(user, target) ||
1286 (!User.following?(user, target) && blocks_domain?(user, target))
1289 def blocks_user?(%User{} = user, %User{} = target) do
1290 UserRelationship.block_exists?(user, target)
1293 def blocks_user?(_, _), do: false
1295 def blocks_domain?(%User{} = user, %User{} = target) do
1296 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
1297 %{host: host} = URI.parse(target.ap_id)
1298 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1301 def blocks_domain?(_, _), do: false
1303 def subscribed_to?(%User{} = user, %User{} = target) do
1304 # Note: the relationship is inverse: subscriber acts as relationship target
1305 UserRelationship.inverse_subscription_exists?(target, user)
1308 def subscribed_to?(%User{} = user, %{ap_id: ap_id}) do
1309 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1310 subscribed_to?(user, target)
1315 Returns map of outgoing (blocked, muted etc.) relationships' user AP IDs by relation type.
1316 E.g. `outgoing_relationships_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}`
1318 @spec outgoing_relationships_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())}
1319 def outgoing_relationships_ap_ids(_user, []), do: %{}
1321 def outgoing_relationships_ap_ids(nil, _relationship_types), do: %{}
1323 def outgoing_relationships_ap_ids(%User{} = user, relationship_types)
1324 when is_list(relationship_types) do
1327 |> assoc(:outgoing_relationships)
1328 |> join(:inner, [user_rel], u in assoc(user_rel, :target))
1329 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1330 |> select([user_rel, u], [user_rel.relationship_type, fragment("array_agg(?)", u.ap_id)])
1331 |> group_by([user_rel, u], user_rel.relationship_type)
1333 |> Enum.into(%{}, fn [k, v] -> {k, v} end)
1338 fn rel_type -> {rel_type, db_result[rel_type] || []} end
1342 def incoming_relationships_ungrouped_ap_ids(user, relationship_types, ap_ids \\ nil)
1344 def incoming_relationships_ungrouped_ap_ids(_user, [], _ap_ids), do: []
1346 def incoming_relationships_ungrouped_ap_ids(nil, _relationship_types, _ap_ids), do: []
1348 def incoming_relationships_ungrouped_ap_ids(%User{} = user, relationship_types, ap_ids)
1349 when is_list(relationship_types) do
1351 |> assoc(:incoming_relationships)
1352 |> join(:inner, [user_rel], u in assoc(user_rel, :source))
1353 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1354 |> maybe_filter_on_ap_id(ap_ids)
1355 |> select([user_rel, u], u.ap_id)
1360 defp maybe_filter_on_ap_id(query, ap_ids) when is_list(ap_ids) do
1361 where(query, [user_rel, u], u.ap_id in ^ap_ids)
1364 defp maybe_filter_on_ap_id(query, _ap_ids), do: query
1366 def deactivate_async(user, status \\ true) do
1367 BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
1370 def deactivate(user, status \\ true)
1372 def deactivate(users, status) when is_list(users) do
1373 Repo.transaction(fn ->
1374 for user <- users, do: deactivate(user, status)
1378 def deactivate(%User{} = user, status) do
1379 with {:ok, user} <- set_activation_status(user, status) do
1382 |> Enum.filter(& &1.local)
1383 |> Enum.each(fn follower ->
1384 follower |> update_following_count() |> set_cache()
1387 # Only update local user counts, remote will be update during the next pull.
1390 |> Enum.filter(& &1.local)
1391 |> Enum.each(&update_follower_count/1)
1397 def update_notification_settings(%User{} = user, settings) do
1399 |> cast(%{notification_settings: settings}, [])
1400 |> cast_embed(:notification_settings)
1401 |> validate_required([:notification_settings])
1402 |> update_and_set_cache()
1405 def delete(users) when is_list(users) do
1406 for user <- users, do: delete(user)
1409 def delete(%User{} = user) do
1410 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1413 def perform(:force_password_reset, user), do: force_password_reset(user)
1415 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1416 def perform(:delete, %User{} = user) do
1417 {:ok, _user} = ActivityPub.delete(user)
1419 # Remove all relationships
1422 |> Enum.each(fn follower ->
1423 ActivityPub.unfollow(follower, user)
1424 unfollow(follower, user)
1429 |> Enum.each(fn followed ->
1430 ActivityPub.unfollow(user, followed)
1431 unfollow(user, followed)
1434 delete_user_activities(user)
1435 invalidate_cache(user)
1439 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1441 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1442 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1443 when is_list(blocked_identifiers) do
1445 blocked_identifiers,
1446 fn blocked_identifier ->
1447 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1448 {:ok, _user_block} <- block(blocker, blocked),
1449 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1453 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1460 def perform(:follow_import, %User{} = follower, followed_identifiers)
1461 when is_list(followed_identifiers) do
1463 followed_identifiers,
1464 fn followed_identifier ->
1465 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1466 {:ok, follower} <- maybe_direct_follow(follower, followed),
1467 {:ok, _} <- ActivityPub.follow(follower, followed) do
1471 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1478 @spec external_users_query() :: Ecto.Query.t()
1479 def external_users_query do
1487 @spec external_users(keyword()) :: [User.t()]
1488 def external_users(opts \\ []) do
1490 external_users_query()
1491 |> select([u], struct(u, [:id, :ap_id]))
1495 do: where(query, [u], u.id > ^opts[:max_id]),
1500 do: limit(query, ^opts[:limit]),
1506 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1507 BackgroundWorker.enqueue("blocks_import", %{
1508 "blocker_id" => blocker.id,
1509 "blocked_identifiers" => blocked_identifiers
1513 def follow_import(%User{} = follower, followed_identifiers)
1514 when is_list(followed_identifiers) do
1515 BackgroundWorker.enqueue("follow_import", %{
1516 "follower_id" => follower.id,
1517 "followed_identifiers" => followed_identifiers
1521 def delete_user_activities(%User{ap_id: ap_id}) do
1523 |> Activity.Queries.by_actor()
1524 |> RepoStreamer.chunk_stream(50)
1525 |> Stream.each(fn activities -> Enum.each(activities, &delete_activity/1) end)
1529 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1531 |> Object.normalize()
1532 |> ActivityPub.delete()
1535 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1536 object = Object.normalize(activity)
1539 |> get_cached_by_ap_id()
1540 |> ActivityPub.unlike(object)
1543 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1544 object = Object.normalize(activity)
1547 |> get_cached_by_ap_id()
1548 |> ActivityPub.unannounce(object)
1551 defp delete_activity(_activity), do: "Doing nothing"
1553 def html_filter_policy(%User{no_rich_text: true}) do
1554 Pleroma.HTML.Scrubber.TwitterText
1557 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1559 def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
1561 def get_or_fetch_by_ap_id(ap_id) do
1562 user = get_cached_by_ap_id(ap_id)
1564 if !is_nil(user) and !needs_update?(user) do
1567 fetch_by_ap_id(ap_id)
1572 Creates an internal service actor by URI if missing.
1573 Optionally takes nickname for addressing.
1575 @spec get_or_create_service_actor_by_ap_id(String.t(), String.t()) :: User.t() | nil
1576 def get_or_create_service_actor_by_ap_id(uri, nickname) do
1578 case get_cached_by_ap_id(uri) do
1580 with {:error, %{errors: errors}} <- create_service_actor(uri, nickname) do
1581 Logger.error("Cannot create service actor: #{uri}/.\n#{inspect(errors)}")
1585 %User{invisible: false} = user ->
1595 @spec set_invisible(User.t()) :: {:ok, User.t()}
1596 defp set_invisible(user) do
1598 |> change(%{invisible: true})
1599 |> update_and_set_cache()
1602 @spec create_service_actor(String.t(), String.t()) ::
1603 {:ok, User.t()} | {:error, Ecto.Changeset.t()}
1604 defp create_service_actor(uri, nickname) do
1610 follower_address: uri <> "/followers"
1613 |> unique_constraint(:nickname)
1619 def public_key(%{source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}}) do
1622 |> :public_key.pem_decode()
1624 |> :public_key.pem_entry_decode()
1629 def public_key(_), do: {:error, "not found key"}
1631 def get_public_key_for_ap_id(ap_id) do
1632 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1633 {:ok, public_key} <- public_key(user) do
1640 defp blank?(""), do: nil
1641 defp blank?(n), do: n
1643 def insert_or_update_user(data) do
1645 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1646 |> remote_user_creation()
1647 |> Repo.insert(on_conflict: {:replace_all_except, [:id]}, conflict_target: :nickname)
1651 def ap_enabled?(%User{local: true}), do: true
1652 def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled
1653 def ap_enabled?(_), do: false
1655 @doc "Gets or fetch a user by uri or nickname."
1656 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1657 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1658 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1660 # wait a period of time and return newest version of the User structs
1661 # this is because we have synchronous follow APIs and need to simulate them
1662 # with an async handshake
1663 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1664 with %User{} = a <- get_cached_by_id(a.id),
1665 %User{} = b <- get_cached_by_id(b.id) do
1672 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1673 with :ok <- :timer.sleep(timeout),
1674 %User{} = a <- get_cached_by_id(a.id),
1675 %User{} = b <- get_cached_by_id(b.id) do
1682 def parse_bio(bio) when is_binary(bio) and bio != "" do
1684 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1688 def parse_bio(_), do: ""
1690 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1691 # TODO: get profile URLs other than user.ap_id
1692 profile_urls = [user.ap_id]
1695 |> CommonUtils.format_input("text/plain",
1696 mentions_format: :full,
1697 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1702 def parse_bio(_, _), do: ""
1704 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1705 Repo.transaction(fn ->
1706 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1710 def tag(nickname, tags) when is_binary(nickname),
1711 do: tag(get_by_nickname(nickname), tags)
1713 def tag(%User{} = user, tags),
1714 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1716 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1717 Repo.transaction(fn ->
1718 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1722 def untag(nickname, tags) when is_binary(nickname),
1723 do: untag(get_by_nickname(nickname), tags)
1725 def untag(%User{} = user, tags),
1726 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1728 defp update_tags(%User{} = user, new_tags) do
1729 {:ok, updated_user} =
1731 |> change(%{tags: new_tags})
1732 |> update_and_set_cache()
1737 defp normalize_tags(tags) do
1740 |> Enum.map(&String.downcase/1)
1743 defp local_nickname_regex do
1744 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1745 @extended_local_nickname_regex
1747 @strict_local_nickname_regex
1751 def local_nickname(nickname_or_mention) do
1754 |> String.split("@")
1758 def full_nickname(nickname_or_mention),
1759 do: String.trim_leading(nickname_or_mention, "@")
1761 def error_user(ap_id) do
1765 nickname: "erroruser@example.com",
1766 inserted_at: NaiveDateTime.utc_now()
1770 @spec all_superusers() :: [User.t()]
1771 def all_superusers do
1772 User.Query.build(%{super_users: true, local: true, deactivated: false})
1776 def muting_reblogs?(%User{} = user, %User{} = target) do
1777 UserRelationship.reblog_mute_exists?(user, target)
1780 def showing_reblogs?(%User{} = user, %User{} = target) do
1781 not muting_reblogs?(user, target)
1785 The function returns a query to get users with no activity for given interval of days.
1786 Inactive users are those who didn't read any notification, or had any activity where
1787 the user is the activity's actor, during `inactivity_threshold` days.
1788 Deactivated users will not appear in this list.
1792 iex> Pleroma.User.list_inactive_users()
1795 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1796 def list_inactive_users_query(inactivity_threshold \\ 7) do
1797 negative_inactivity_threshold = -inactivity_threshold
1798 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1799 # Subqueries are not supported in `where` clauses, join gets too complicated.
1800 has_read_notifications =
1801 from(n in Pleroma.Notification,
1802 where: n.seen == true,
1804 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1807 |> Pleroma.Repo.all()
1809 from(u in Pleroma.User,
1810 left_join: a in Pleroma.Activity,
1811 on: u.ap_id == a.actor,
1812 where: not is_nil(u.nickname),
1813 where: u.deactivated != ^true,
1814 where: u.id not in ^has_read_notifications,
1817 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1818 is_nil(max(a.inserted_at))
1823 Enable or disable email notifications for user
1827 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => false}}, "digest", true)
1828 Pleroma.User{email_notifications: %{"digest" => true}}
1830 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => true}}, "digest", false)
1831 Pleroma.User{email_notifications: %{"digest" => false}}
1833 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1834 {:ok, t()} | {:error, Ecto.Changeset.t()}
1835 def switch_email_notifications(user, type, status) do
1836 User.update_email_notifications(user, %{type => status})
1840 Set `last_digest_emailed_at` value for the user to current time
1842 @spec touch_last_digest_emailed_at(t()) :: t()
1843 def touch_last_digest_emailed_at(user) do
1844 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1846 {:ok, updated_user} =
1848 |> change(%{last_digest_emailed_at: now})
1849 |> update_and_set_cache()
1854 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1855 def toggle_confirmation(%User{} = user) do
1857 |> confirmation_changeset(need_confirmation: !user.confirmation_pending)
1858 |> update_and_set_cache()
1861 @spec toggle_confirmation([User.t()]) :: [{:ok, User.t()} | {:error, Changeset.t()}]
1862 def toggle_confirmation(users) do
1863 Enum.map(users, &toggle_confirmation/1)
1866 def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
1870 def get_mascot(%{mascot: mascot}) when is_nil(mascot) do
1871 # use instance-default
1872 config = Pleroma.Config.get([:assets, :mascots])
1873 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1874 mascot = Keyword.get(config, default_mascot)
1877 "id" => "default-mascot",
1878 "url" => mascot[:url],
1879 "preview_url" => mascot[:url],
1881 "mime_type" => mascot[:mime_type]
1886 def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
1888 def ensure_keys_present(%User{} = user) do
1889 with {:ok, pem} <- Keys.generate_rsa_pem() do
1891 |> cast(%{keys: pem}, [:keys])
1892 |> validate_required([:keys])
1893 |> update_and_set_cache()
1897 def get_ap_ids_by_nicknames(nicknames) do
1899 where: u.nickname in ^nicknames,
1905 defdelegate search(query, opts \\ []), to: User.Search
1907 defp put_password_hash(
1908 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1910 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1913 defp put_password_hash(changeset), do: changeset
1915 def is_internal_user?(%User{nickname: nil}), do: true
1916 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1917 def is_internal_user?(_), do: false
1919 # A hack because user delete activities have a fake id for whatever reason
1920 # TODO: Get rid of this
1921 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
1923 def get_delivered_users_by_object_id(object_id) do
1925 inner_join: delivery in assoc(u, :deliveries),
1926 where: delivery.object_id == ^object_id
1931 def change_email(user, email) do
1933 |> cast(%{email: email}, [:email])
1934 |> validate_required([:email])
1935 |> unique_constraint(:email)
1936 |> validate_format(:email, @email_regex)
1937 |> update_and_set_cache()
1940 # Internal function; public one is `deactivate/2`
1941 defp set_activation_status(user, deactivated) do
1943 |> cast(%{deactivated: deactivated}, [:deactivated])
1944 |> update_and_set_cache()
1947 def update_banner(user, banner) do
1949 |> cast(%{banner: banner}, [:banner])
1950 |> update_and_set_cache()
1953 def update_background(user, background) do
1955 |> cast(%{background: background}, [:background])
1956 |> update_and_set_cache()
1959 def update_source_data(user, source_data) do
1961 |> cast(%{source_data: source_data}, [:source_data])
1962 |> update_and_set_cache()
1965 def roles(%{is_moderator: is_moderator, is_admin: is_admin}) do
1968 moderator: is_moderator
1972 # ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``.
1973 # For example: [{"name": "Pronoun", "value": "she/her"}, …]
1974 def fields(%{fields: nil, source_data: %{"attachment" => attachment}}) do
1975 limit = Pleroma.Config.get([:instance, :max_remote_account_fields], 0)
1978 |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
1979 |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
1983 def fields(%{fields: nil}), do: []
1985 def fields(%{fields: fields}), do: fields
1987 def sanitized_fields(%User{} = user) do
1990 |> Enum.map(fn %{"name" => name, "value" => value} ->
1993 "value" => Pleroma.HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
1998 def validate_fields(changeset, remote? \\ false) do
1999 limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
2000 limit = Pleroma.Config.get([:instance, limit_name], 0)
2003 |> validate_length(:fields, max: limit)
2004 |> validate_change(:fields, fn :fields, fields ->
2005 if Enum.all?(fields, &valid_field?/1) do
2013 defp valid_field?(%{"name" => name, "value" => value}) do
2014 name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255)
2015 value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255)
2017 is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
2018 String.length(value) <= value_limit
2021 defp valid_field?(_), do: false
2023 defp truncate_field(%{"name" => name, "value" => value}) do
2025 String.split_at(name, Pleroma.Config.get([:instance, :account_field_name_length], 255))
2028 String.split_at(value, Pleroma.Config.get([:instance, :account_field_value_length], 255))
2030 %{"name" => name, "value" => value}
2033 def admin_api_update(user, params) do
2040 |> update_and_set_cache()
2043 @doc "Signs user out of all applications"
2044 def global_sign_out(user) do
2045 OAuth.Authorization.delete_user_authorizations(user)
2046 OAuth.Token.delete_user_tokens(user)
2049 def mascot_update(user, url) do
2051 |> cast(%{mascot: url}, [:mascot])
2052 |> validate_required([:mascot])
2053 |> update_and_set_cache()
2056 def mastodon_settings_update(user, settings) do
2058 |> cast(%{settings: settings}, [:settings])
2059 |> validate_required([:settings])
2060 |> update_and_set_cache()
2063 @spec confirmation_changeset(User.t(), keyword()) :: Changeset.t()
2064 def confirmation_changeset(user, need_confirmation: need_confirmation?) do
2066 if need_confirmation? do
2068 confirmation_pending: true,
2069 confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
2073 confirmation_pending: false,
2074 confirmation_token: nil
2078 cast(user, params, [:confirmation_pending, :confirmation_token])
2081 def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do
2082 if id not in user.pinned_activities do
2083 max_pinned_statuses = Pleroma.Config.get([:instance, :max_pinned_statuses], 0)
2084 params = %{pinned_activities: user.pinned_activities ++ [id]}
2087 |> cast(params, [:pinned_activities])
2088 |> validate_length(:pinned_activities,
2089 max: max_pinned_statuses,
2090 message: "You have already pinned the maximum number of statuses"
2095 |> update_and_set_cache()
2098 def remove_pinnned_activity(user, %Pleroma.Activity{id: id}) do
2099 params = %{pinned_activities: List.delete(user.pinned_activities, id)}
2102 |> cast(params, [:pinned_activities])
2103 |> update_and_set_cache()
2106 def update_email_notifications(user, settings) do
2107 email_notifications =
2108 user.email_notifications
2109 |> Map.merge(settings)
2110 |> Map.take(["digest"])
2112 params = %{email_notifications: email_notifications}
2113 fields = [:email_notifications]
2116 |> cast(params, fields)
2117 |> validate_required(fields)
2118 |> update_and_set_cache()
2121 defp set_domain_blocks(user, domain_blocks) do
2122 params = %{domain_blocks: domain_blocks}
2125 |> cast(params, [:domain_blocks])
2126 |> validate_required([:domain_blocks])
2127 |> update_and_set_cache()
2130 def block_domain(user, domain_blocked) do
2131 set_domain_blocks(user, Enum.uniq([domain_blocked | user.domain_blocks]))
2134 def unblock_domain(user, domain_blocked) do
2135 set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked))
2138 @spec add_to_block(User.t(), User.t()) ::
2139 {:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()}
2140 defp add_to_block(%User{} = user, %User{} = blocked) do
2141 UserRelationship.create_block(user, blocked)
2144 @spec add_to_block(User.t(), User.t()) ::
2145 {:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()}
2146 defp remove_from_block(%User{} = user, %User{} = blocked) do
2147 UserRelationship.delete_block(user, blocked)
2150 defp add_to_mutes(%User{} = user, %User{} = muted_user, notifications?) do
2151 with {:ok, user_mute} <- UserRelationship.create_mute(user, muted_user),
2152 {:ok, user_notification_mute} <-
2153 (notifications? && UserRelationship.create_notification_mute(user, muted_user)) ||
2155 {:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
2159 defp remove_from_mutes(user, %User{} = muted_user) do
2160 with {:ok, user_mute} <- UserRelationship.delete_mute(user, muted_user),
2161 {:ok, user_notification_mute} <-
2162 UserRelationship.delete_notification_mute(user, muted_user) do
2163 {:ok, [user_mute, user_notification_mute]}
2167 def set_invisible(user, invisible) do
2168 params = %{invisible: invisible}
2171 |> cast(params, [:invisible])
2172 |> validate_required([:invisible])
2173 |> update_and_set_cache()
2176 def sanitize_html(%User{} = user) do
2177 sanitize_html(user, nil)
2180 # User data that mastodon isn't filtering (treated as plaintext):
2183 def sanitize_html(%User{} = user, filter) do
2187 |> Enum.map(fn %{"name" => name, "value" => value} ->
2190 "value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
2195 |> Map.put(:bio, HTML.filter_tags(user.bio, filter))
2196 |> Map.put(:fields, fields)