1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.User do
10 import Ecto, only: [assoc: 2]
14 alias Pleroma.Activity
16 alias Pleroma.Conversation.Participation
17 alias Pleroma.Delivery
18 alias Pleroma.FollowingRelationship
19 alias Pleroma.Formatter
22 alias Pleroma.Notification
24 alias Pleroma.Registration
26 alias Pleroma.RepoStreamer
28 alias Pleroma.UserRelationship
30 alias Pleroma.Web.ActivityPub.ActivityPub
31 alias Pleroma.Web.ActivityPub.Utils
32 alias Pleroma.Web.CommonAPI
33 alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
34 alias Pleroma.Web.OAuth
35 alias Pleroma.Web.RelMe
36 alias Pleroma.Workers.BackgroundWorker
40 @type t :: %__MODULE__{}
41 @type account_status :: :active | :deactivated | :password_reset_pending | :confirmation_pending
42 @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
44 # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
45 @email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
47 @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
48 @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
50 # AP ID user relationships (blocks, mutes etc.)
51 # Format: [rel_type: [outgoing_rel: :outgoing_rel_target, incoming_rel: :incoming_rel_source]]
52 @user_relationships_config [
54 blocker_blocks: :blocked_users,
55 blockee_blocks: :blocker_users
58 muter_mutes: :muted_users,
59 mutee_mutes: :muter_users
62 reblog_muter_mutes: :reblog_muted_users,
63 reblog_mutee_mutes: :reblog_muter_users
66 notification_muter_mutes: :notification_muted_users,
67 notification_mutee_mutes: :notification_muter_users
69 # Note: `inverse_subscription` relationship is inverse: subscriber acts as relationship target
70 inverse_subscription: [
71 subscribee_subscriptions: :subscriber_users,
72 subscriber_subscriptions: :subscribee_users
78 field(:email, :string)
80 field(:nickname, :string)
81 field(:password_hash, :string)
82 field(:password, :string, virtual: true)
83 field(:password_confirmation, :string, virtual: true)
85 field(:ap_id, :string)
87 field(:local, :boolean, default: true)
88 field(:follower_address, :string)
89 field(:following_address, :string)
90 field(:search_rank, :float, virtual: true)
91 field(:search_type, :integer, virtual: true)
92 field(:tags, {:array, :string}, default: [])
93 field(:last_refreshed_at, :naive_datetime_usec)
94 field(:last_digest_emailed_at, :naive_datetime)
95 field(:banner, :map, default: %{})
96 field(:background, :map, default: %{})
97 field(:source_data, :map, default: %{})
98 field(:note_count, :integer, default: 0)
99 field(:follower_count, :integer, default: 0)
100 field(:following_count, :integer, default: 0)
101 field(:locked, :boolean, default: false)
102 field(:confirmation_pending, :boolean, default: false)
103 field(:password_reset_pending, :boolean, default: false)
104 field(:confirmation_token, :string, default: nil)
105 field(:default_scope, :string, default: "public")
106 field(:domain_blocks, {:array, :string}, default: [])
107 field(:deactivated, :boolean, default: false)
108 field(:no_rich_text, :boolean, default: false)
109 field(:ap_enabled, :boolean, default: false)
110 field(:is_moderator, :boolean, default: false)
111 field(:is_admin, :boolean, default: false)
112 field(:show_role, :boolean, default: true)
113 field(:settings, :map, default: nil)
114 field(:magic_key, :string, default: nil)
115 field(:uri, :string, default: nil)
116 field(:hide_followers_count, :boolean, default: false)
117 field(:hide_follows_count, :boolean, default: false)
118 field(:hide_followers, :boolean, default: false)
119 field(:hide_follows, :boolean, default: false)
120 field(:hide_favorites, :boolean, default: true)
121 field(:unread_conversation_count, :integer, default: 0)
122 field(:pinned_activities, {:array, :string}, default: [])
123 field(:email_notifications, :map, default: %{"digest" => false})
124 field(:mascot, :map, default: nil)
125 field(:emoji, {:array, :map}, default: [])
126 field(:pleroma_settings_store, :map, default: %{})
127 field(:fields, {:array, :map}, default: [])
128 field(:raw_fields, {:array, :map}, default: [])
129 field(:discoverable, :boolean, default: false)
130 field(:invisible, :boolean, default: false)
131 field(:allow_following_move, :boolean, default: true)
132 field(:skip_thread_containment, :boolean, default: false)
133 field(:actor_type, :string, default: "Person")
134 field(:also_known_as, {:array, :string}, default: [])
137 :notification_settings,
138 Pleroma.User.NotificationSetting,
142 has_many(:notifications, Notification)
143 has_many(:registrations, Registration)
144 has_many(:deliveries, Delivery)
146 has_many(:outgoing_relationships, UserRelationship, foreign_key: :source_id)
147 has_many(:incoming_relationships, UserRelationship, foreign_key: :target_id)
149 for {relationship_type,
151 {outgoing_relation, outgoing_relation_target},
152 {incoming_relation, incoming_relation_source}
153 ]} <- @user_relationships_config do
154 # Definitions of `has_many` relations: :blocker_blocks, :muter_mutes, :reblog_muter_mutes,
155 # :notification_muter_mutes, :subscribee_subscriptions
156 has_many(outgoing_relation, UserRelationship,
157 foreign_key: :source_id,
158 where: [relationship_type: relationship_type]
161 # Definitions of `has_many` relations: :blockee_blocks, :mutee_mutes, :reblog_mutee_mutes,
162 # :notification_mutee_mutes, :subscriber_subscriptions
163 has_many(incoming_relation, UserRelationship,
164 foreign_key: :target_id,
165 where: [relationship_type: relationship_type]
168 # Definitions of `has_many` relations: :blocked_users, :muted_users, :reblog_muted_users,
169 # :notification_muted_users, :subscriber_users
170 has_many(outgoing_relation_target, through: [outgoing_relation, :target])
172 # Definitions of `has_many` relations: :blocker_users, :muter_users, :reblog_muter_users,
173 # :notification_muter_users, :subscribee_users
174 has_many(incoming_relation_source, through: [incoming_relation, :source])
177 # `:blocks` is deprecated (replaced with `blocked_users` relation)
178 field(:blocks, {:array, :string}, default: [])
179 # `:mutes` is deprecated (replaced with `muted_users` relation)
180 field(:mutes, {:array, :string}, default: [])
181 # `:muted_reblogs` is deprecated (replaced with `reblog_muted_users` relation)
182 field(:muted_reblogs, {:array, :string}, default: [])
183 # `:muted_notifications` is deprecated (replaced with `notification_muted_users` relation)
184 field(:muted_notifications, {:array, :string}, default: [])
185 # `:subscribers` is deprecated (replaced with `subscriber_users` relation)
186 field(:subscribers, {:array, :string}, default: [])
191 for {_relationship_type, [{_outgoing_relation, outgoing_relation_target}, _]} <-
192 @user_relationships_config do
193 # `def blocked_users_relation/2`, `def muted_users_relation/2`,
194 # `def reblog_muted_users_relation/2`, `def notification_muted_users/2`,
195 # `def subscriber_users/2`
196 def unquote(:"#{outgoing_relation_target}_relation")(user, restrict_deactivated? \\ false) do
197 target_users_query = assoc(user, unquote(outgoing_relation_target))
199 if restrict_deactivated? do
200 restrict_deactivated(target_users_query)
206 # `def blocked_users/2`, `def muted_users/2`, `def reblog_muted_users/2`,
207 # `def notification_muted_users/2`, `def subscriber_users/2`
208 def unquote(outgoing_relation_target)(user, restrict_deactivated? \\ false) do
210 |> apply(unquote(:"#{outgoing_relation_target}_relation"), [
212 restrict_deactivated?
217 # `def blocked_users_ap_ids/2`, `def muted_users_ap_ids/2`, `def reblog_muted_users_ap_ids/2`,
218 # `def notification_muted_users_ap_ids/2`, `def subscriber_users_ap_ids/2`
219 def unquote(:"#{outgoing_relation_target}_ap_ids")(user, restrict_deactivated? \\ false) do
221 |> apply(unquote(:"#{outgoing_relation_target}_relation"), [
223 restrict_deactivated?
225 |> select([u], u.ap_id)
231 Dumps Flake Id to SQL-compatible format (16-byte UUID).
232 E.g. "9pQtDGXuq4p3VlcJEm" -> <<0, 0, 1, 110, 179, 218, 42, 92, 213, 41, 44, 227, 95, 213, 0, 0>>
234 def binary_id(source_id) when is_binary(source_id) do
235 with {:ok, dumped_id} <- FlakeId.Ecto.CompatType.dump(source_id) do
242 def binary_id(source_ids) when is_list(source_ids) do
243 Enum.map(source_ids, &binary_id/1)
246 def binary_id(%User{} = user), do: binary_id(user.id)
248 @doc "Returns status account"
249 @spec account_status(User.t()) :: account_status()
250 def account_status(%User{deactivated: true}), do: :deactivated
251 def account_status(%User{password_reset_pending: true}), do: :password_reset_pending
253 def account_status(%User{confirmation_pending: true}) do
254 case Config.get([:instance, :account_activation_required]) do
255 true -> :confirmation_pending
260 def account_status(%User{}), do: :active
262 @spec visible_for?(User.t(), User.t() | nil) :: boolean()
263 def visible_for?(user, for_user \\ nil)
265 def visible_for?(%User{invisible: true}, _), do: false
267 def visible_for?(%User{id: user_id}, %User{id: user_id}), do: true
269 def visible_for?(%User{local: local} = user, nil) do
275 if Config.get([:restrict_unauthenticated, :profiles, cfg_key]),
277 else: account_status(user) == :active
280 def visible_for?(%User{} = user, for_user) do
281 account_status(user) == :active || superuser?(for_user)
284 def visible_for?(_, _), do: false
286 @spec superuser?(User.t()) :: boolean()
287 def superuser?(%User{local: true, is_admin: true}), do: true
288 def superuser?(%User{local: true, is_moderator: true}), do: true
289 def superuser?(_), do: false
291 @spec invisible?(User.t()) :: boolean()
292 def invisible?(%User{invisible: true}), do: true
293 def invisible?(_), do: false
295 def avatar_url(user, options \\ []) do
297 %{"url" => [%{"href" => href} | _]} -> href
298 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
302 def banner_url(user, options \\ []) do
304 %{"url" => [%{"href" => href} | _]} -> href
305 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
309 def ap_id(%User{nickname: nickname}), do: "#{Web.base_url()}/users/#{nickname}"
311 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
312 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
314 @spec ap_following(User.t()) :: String.t()
315 def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
316 def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
318 @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
319 def restrict_deactivated(query) do
320 from(u in query, where: u.deactivated != ^true)
323 defdelegate following_count(user), to: FollowingRelationship
325 defp truncate_fields_param(params) do
326 if Map.has_key?(params, :fields) do
327 Map.put(params, :fields, Enum.map(params[:fields], &truncate_field/1))
333 defp truncate_if_exists(params, key, max_length) do
334 if Map.has_key?(params, key) and is_binary(params[key]) do
335 {value, _chopped} = String.split_at(params[key], max_length)
336 Map.put(params, key, value)
342 def remote_user_changeset(struct \\ %User{local: false}, params) do
343 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
344 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
347 case params[:name] do
348 name when is_binary(name) and byte_size(name) > 0 -> name
349 _ -> params[:nickname]
354 |> Map.put(:name, name)
355 |> Map.put_new(:last_refreshed_at, NaiveDateTime.utc_now())
356 |> truncate_if_exists(:name, name_limit)
357 |> truncate_if_exists(:bio, bio_limit)
358 |> truncate_fields_param()
378 :hide_followers_count,
390 |> validate_required([:name, :ap_id])
391 |> unique_constraint(:nickname)
392 |> validate_format(:nickname, @email_regex)
393 |> validate_length(:bio, max: bio_limit)
394 |> validate_length(:name, max: name_limit)
395 |> validate_fields(true)
397 case params[:source_data] do
398 %{"followers" => followers, "following" => following} ->
400 |> put_change(:follower_address, followers)
401 |> put_change(:following_address, following)
404 followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
405 put_change(changeset, :follower_address, followers)
409 def update_changeset(struct, params \\ %{}) do
410 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
411 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
426 :hide_followers_count,
429 :allow_following_move,
432 :skip_thread_containment,
435 :pleroma_settings_store,
441 |> unique_constraint(:nickname)
442 |> validate_format(:nickname, local_nickname_regex())
443 |> validate_length(:bio, max: bio_limit)
444 |> validate_length(:name, min: 1, max: name_limit)
446 |> put_change_if_present(:bio, &{:ok, parse_bio(&1, struct)})
447 |> put_change_if_present(:avatar, &put_upload(&1, :avatar))
448 |> put_change_if_present(:banner, &put_upload(&1, :banner))
449 |> put_change_if_present(:background, &put_upload(&1, :background))
450 |> put_change_if_present(
451 :pleroma_settings_store,
452 &{:ok, Map.merge(struct.pleroma_settings_store, &1)}
454 |> validate_fields(false)
457 defp put_fields(changeset) do
458 if raw_fields = get_change(changeset, :raw_fields) do
461 |> Enum.filter(fn %{"name" => n} -> n != "" end)
465 |> Enum.map(fn f -> Map.update!(f, "value", &parse_fields(&1)) end)
468 |> put_change(:raw_fields, raw_fields)
469 |> put_change(:fields, fields)
475 defp parse_fields(value) do
477 |> Formatter.linkify(mentions_format: :full)
481 defp put_change_if_present(changeset, map_field, value_function) do
482 if value = get_change(changeset, map_field) do
483 with {:ok, new_value} <- value_function.(value) do
484 put_change(changeset, map_field, new_value)
493 defp put_upload(value, type) do
494 with %Plug.Upload{} <- value,
495 {:ok, object} <- ActivityPub.upload(value, type: type) do
500 def update_as_admin_changeset(struct, params) do
502 |> update_changeset(params)
503 |> cast(params, [:email])
504 |> delete_change(:also_known_as)
505 |> unique_constraint(:email)
506 |> validate_format(:email, @email_regex)
509 @spec update_as_admin(%User{}, map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
510 def update_as_admin(user, params) do
511 params = Map.put(params, "password_confirmation", params["password"])
512 changeset = update_as_admin_changeset(user, params)
514 if params["password"] do
515 reset_password(user, changeset, params)
517 User.update_and_set_cache(changeset)
521 def password_update_changeset(struct, params) do
523 |> cast(params, [:password, :password_confirmation])
524 |> validate_required([:password, :password_confirmation])
525 |> validate_confirmation(:password)
526 |> put_password_hash()
527 |> put_change(:password_reset_pending, false)
530 @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
531 def reset_password(%User{} = user, params) do
532 reset_password(user, user, params)
535 def reset_password(%User{id: user_id} = user, struct, params) do
538 |> Multi.update(:user, password_update_changeset(struct, params))
539 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
540 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
542 case Repo.transaction(multi) do
543 {:ok, %{user: user} = _} -> set_cache(user)
544 {:error, _, changeset, _} -> {:error, changeset}
548 def update_password_reset_pending(user, value) do
551 |> put_change(:password_reset_pending, value)
552 |> update_and_set_cache()
555 def force_password_reset_async(user) do
556 BackgroundWorker.enqueue("force_password_reset", %{"user_id" => user.id})
559 @spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
560 def force_password_reset(user), do: update_password_reset_pending(user, true)
562 def register_changeset(struct, params \\ %{}, opts \\ []) do
563 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
564 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
567 if is_nil(opts[:need_confirmation]) do
568 Pleroma.Config.get([:instance, :account_activation_required])
570 opts[:need_confirmation]
574 |> confirmation_changeset(need_confirmation: need_confirmation?)
575 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
576 |> validate_required([:name, :nickname, :password, :password_confirmation])
577 |> validate_confirmation(:password)
578 |> unique_constraint(:email)
579 |> unique_constraint(:nickname)
580 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
581 |> validate_format(:nickname, local_nickname_regex())
582 |> validate_format(:email, @email_regex)
583 |> validate_length(:bio, max: bio_limit)
584 |> validate_length(:name, min: 1, max: name_limit)
585 |> maybe_validate_required_email(opts[:external])
588 |> unique_constraint(:ap_id)
589 |> put_following_and_follower_address()
592 def maybe_validate_required_email(changeset, true), do: changeset
594 def maybe_validate_required_email(changeset, _) do
595 if Pleroma.Config.get([:instance, :account_activation_required]) do
596 validate_required(changeset, [:email])
602 defp put_ap_id(changeset) do
603 ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
604 put_change(changeset, :ap_id, ap_id)
607 defp put_following_and_follower_address(changeset) do
608 followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
611 |> put_change(:follower_address, followers)
614 defp autofollow_users(user) do
615 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
618 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
621 follow_all(user, autofollowed_users)
624 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
625 def register(%Ecto.Changeset{} = changeset) do
626 with {:ok, user} <- Repo.insert(changeset) do
627 post_register_action(user)
631 def post_register_action(%User{} = user) do
632 with {:ok, user} <- autofollow_users(user),
633 {:ok, user} <- set_cache(user),
634 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
635 {:ok, _} <- try_send_confirmation_email(user) do
640 def try_send_confirmation_email(%User{} = user) do
641 if user.confirmation_pending &&
642 Pleroma.Config.get([:instance, :account_activation_required]) do
644 |> Pleroma.Emails.UserEmail.account_confirmation_email()
645 |> Pleroma.Emails.Mailer.deliver_async()
653 def try_send_confirmation_email(users) do
654 Enum.each(users, &try_send_confirmation_email/1)
657 def needs_update?(%User{local: true}), do: false
659 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
661 def needs_update?(%User{local: false} = user) do
662 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
665 def needs_update?(_), do: true
667 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
668 def maybe_direct_follow(%User{} = follower, %User{local: true, locked: true} = followed) do
669 follow(follower, followed, :follow_pending)
672 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
673 follow(follower, followed)
676 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
677 if not ap_enabled?(followed) do
678 follow(follower, followed)
684 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
685 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
686 def follow_all(follower, followeds) do
688 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
689 |> Enum.each(&follow(follower, &1, :follow_accept))
694 defdelegate following(user), to: FollowingRelationship
696 def follow(%User{} = follower, %User{} = followed, state \\ :follow_accept) do
697 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
700 followed.deactivated ->
701 {:error, "Could not follow user: #{followed.nickname} is deactivated."}
703 deny_follow_blocked and blocks?(followed, follower) ->
704 {:error, "Could not follow user: #{followed.nickname} blocked you."}
707 FollowingRelationship.follow(follower, followed, state)
709 {:ok, _} = update_follower_count(followed)
712 |> update_following_count()
717 def unfollow(%User{ap_id: ap_id}, %User{ap_id: ap_id}) do
718 {:error, "Not subscribed!"}
721 def unfollow(%User{} = follower, %User{} = followed) do
722 case get_follow_state(follower, followed) do
723 state when state in [:follow_pending, :follow_accept] ->
724 FollowingRelationship.unfollow(follower, followed)
725 {:ok, followed} = update_follower_count(followed)
729 |> update_following_count()
732 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
735 {:error, "Not subscribed!"}
739 defdelegate following?(follower, followed), to: FollowingRelationship
741 @doc "Returns follow state as Pleroma.FollowingRelationship.State value"
742 def get_follow_state(%User{} = follower, %User{} = following) do
743 following_relationship = FollowingRelationship.get(follower, following)
744 get_follow_state(follower, following, following_relationship)
747 def get_follow_state(
750 following_relationship
752 case {following_relationship, following.local} do
754 case Utils.fetch_latest_follow(follower, following) do
755 %Activity{data: %{"state" => state}} when state in ["pending", "accept"] ->
756 FollowingRelationship.state_to_enum(state)
762 {%{state: state}, _} ->
770 def locked?(%User{} = user) do
775 Repo.get_by(User, id: id)
778 def get_by_ap_id(ap_id) do
779 Repo.get_by(User, ap_id: ap_id)
782 def get_all_by_ap_id(ap_ids) do
783 from(u in __MODULE__,
784 where: u.ap_id in ^ap_ids
789 def get_all_by_ids(ids) do
790 from(u in __MODULE__, where: u.id in ^ids)
794 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
795 # of the ap_id and the domain and tries to get that user
796 def get_by_guessed_nickname(ap_id) do
797 domain = URI.parse(ap_id).host
798 name = List.last(String.split(ap_id, "/"))
799 nickname = "#{name}@#{domain}"
801 get_cached_by_nickname(nickname)
804 def set_cache({:ok, user}), do: set_cache(user)
805 def set_cache({:error, err}), do: {:error, err}
807 def set_cache(%User{} = user) do
808 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
809 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
813 def update_and_set_cache(struct, params) do
815 |> update_changeset(params)
816 |> update_and_set_cache()
819 def update_and_set_cache(changeset) do
820 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
825 def invalidate_cache(user) do
826 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
827 Cachex.del(:user_cache, "nickname:#{user.nickname}")
830 @spec get_cached_by_ap_id(String.t()) :: User.t() | nil
831 def get_cached_by_ap_id(ap_id) do
832 key = "ap_id:#{ap_id}"
834 with {:ok, nil} <- Cachex.get(:user_cache, key),
835 user when not is_nil(user) <- get_by_ap_id(ap_id),
836 {:ok, true} <- Cachex.put(:user_cache, key, user) do
844 def get_cached_by_id(id) do
848 Cachex.fetch!(:user_cache, key, fn _ ->
852 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
853 {:commit, user.ap_id}
859 get_cached_by_ap_id(ap_id)
862 def get_cached_by_nickname(nickname) do
863 key = "nickname:#{nickname}"
865 Cachex.fetch!(:user_cache, key, fn ->
866 case get_or_fetch_by_nickname(nickname) do
867 {:ok, user} -> {:commit, user}
868 {:error, _error} -> {:ignore, nil}
873 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
874 restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
877 is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
878 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
880 restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
881 get_cached_by_nickname(nickname_or_id)
883 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
884 get_cached_by_nickname(nickname_or_id)
891 def get_by_nickname(nickname) do
892 Repo.get_by(User, nickname: nickname) ||
893 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
894 Repo.get_by(User, nickname: local_nickname(nickname))
898 def get_by_email(email), do: Repo.get_by(User, email: email)
900 def get_by_nickname_or_email(nickname_or_email) do
901 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
904 def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname)
906 def get_or_fetch_by_nickname(nickname) do
907 with %User{} = user <- get_by_nickname(nickname) do
911 with [_nick, _domain] <- String.split(nickname, "@"),
912 {:ok, user} <- fetch_by_nickname(nickname) do
915 _e -> {:error, "not found " <> nickname}
920 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
921 def get_followers_query(%User{} = user, nil) do
922 User.Query.build(%{followers: user, deactivated: false})
925 def get_followers_query(user, page) do
927 |> get_followers_query(nil)
928 |> User.Query.paginate(page, 20)
931 @spec get_followers_query(User.t()) :: Ecto.Query.t()
932 def get_followers_query(user), do: get_followers_query(user, nil)
934 @spec get_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
935 def get_followers(user, page \\ nil) do
937 |> get_followers_query(page)
941 @spec get_external_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
942 def get_external_followers(user, page \\ nil) do
944 |> get_followers_query(page)
945 |> User.Query.build(%{external: true})
949 def get_followers_ids(user, page \\ nil) do
951 |> get_followers_query(page)
956 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
957 def get_friends_query(%User{} = user, nil) do
958 User.Query.build(%{friends: user, deactivated: false})
961 def get_friends_query(user, page) do
963 |> get_friends_query(nil)
964 |> User.Query.paginate(page, 20)
967 @spec get_friends_query(User.t()) :: Ecto.Query.t()
968 def get_friends_query(user), do: get_friends_query(user, nil)
970 def get_friends(user, page \\ nil) do
972 |> get_friends_query(page)
976 def get_friends_ap_ids(user) do
978 |> get_friends_query(nil)
979 |> select([u], u.ap_id)
983 def get_friends_ids(user, page \\ nil) do
985 |> get_friends_query(page)
990 defdelegate get_follow_requests(user), to: FollowingRelationship
992 def increase_note_count(%User{} = user) do
994 |> where(id: ^user.id)
995 |> update([u], inc: [note_count: 1])
997 |> Repo.update_all([])
999 {1, [user]} -> set_cache(user)
1004 def decrease_note_count(%User{} = user) do
1006 |> where(id: ^user.id)
1009 note_count: fragment("greatest(0, note_count - 1)")
1013 |> Repo.update_all([])
1015 {1, [user]} -> set_cache(user)
1020 def update_note_count(%User{} = user, note_count \\ nil) do
1025 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
1031 |> cast(%{note_count: note_count}, [:note_count])
1032 |> update_and_set_cache()
1035 @spec maybe_fetch_follow_information(User.t()) :: User.t()
1036 def maybe_fetch_follow_information(user) do
1037 with {:ok, user} <- fetch_follow_information(user) do
1041 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
1047 def fetch_follow_information(user) do
1048 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
1050 |> follow_information_changeset(info)
1051 |> update_and_set_cache()
1055 defp follow_information_changeset(user, params) do
1062 :hide_followers_count,
1067 def update_follower_count(%User{} = user) do
1068 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
1069 follower_count_query =
1070 User.Query.build(%{followers: user, deactivated: false})
1071 |> select([u], %{count: count(u.id)})
1074 |> where(id: ^user.id)
1075 |> join(:inner, [u], s in subquery(follower_count_query))
1077 set: [follower_count: s.count]
1080 |> Repo.update_all([])
1082 {1, [user]} -> set_cache(user)
1086 {:ok, maybe_fetch_follow_information(user)}
1090 @spec update_following_count(User.t()) :: User.t()
1091 def update_following_count(%User{local: false} = user) do
1092 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
1093 maybe_fetch_follow_information(user)
1099 def update_following_count(%User{local: true} = user) do
1100 following_count = FollowingRelationship.following_count(user)
1103 |> follow_information_changeset(%{following_count: following_count})
1107 def set_unread_conversation_count(%User{local: true} = user) do
1108 unread_query = Participation.unread_conversation_count_for_user(user)
1111 |> join(:inner, [u], p in subquery(unread_query))
1113 set: [unread_conversation_count: p.count]
1115 |> where([u], u.id == ^user.id)
1117 |> Repo.update_all([])
1119 {1, [user]} -> set_cache(user)
1124 def set_unread_conversation_count(user), do: {:ok, user}
1126 def increment_unread_conversation_count(conversation, %User{local: true} = user) do
1128 Participation.unread_conversation_count_for_user(user)
1129 |> where([p], p.conversation_id == ^conversation.id)
1132 |> join(:inner, [u], p in subquery(unread_query))
1134 inc: [unread_conversation_count: 1]
1136 |> where([u], u.id == ^user.id)
1137 |> where([u, p], p.count == 0)
1139 |> Repo.update_all([])
1141 {1, [user]} -> set_cache(user)
1146 def increment_unread_conversation_count(_, user), do: {:ok, user}
1148 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
1149 def get_users_from_set(ap_ids, local_only \\ true) do
1150 criteria = %{ap_id: ap_ids, deactivated: false}
1151 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
1153 User.Query.build(criteria)
1157 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
1158 def get_recipients_from_activity(%Activity{recipients: to}) do
1159 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
1163 @spec mute(User.t(), User.t(), boolean()) ::
1164 {:ok, list(UserRelationship.t())} | {:error, String.t()}
1165 def mute(%User{} = muter, %User{} = mutee, notifications? \\ true) do
1166 add_to_mutes(muter, mutee, notifications?)
1169 def unmute(%User{} = muter, %User{} = mutee) do
1170 remove_from_mutes(muter, mutee)
1173 def subscribe(%User{} = subscriber, %User{} = target) do
1174 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
1176 if blocks?(target, subscriber) and deny_follow_blocked do
1177 {:error, "Could not subscribe: #{target.nickname} is blocking you"}
1179 # Note: the relationship is inverse: subscriber acts as relationship target
1180 UserRelationship.create_inverse_subscription(target, subscriber)
1184 def subscribe(%User{} = subscriber, %{ap_id: ap_id}) do
1185 with %User{} = subscribee <- get_cached_by_ap_id(ap_id) do
1186 subscribe(subscriber, subscribee)
1190 def unsubscribe(%User{} = unsubscriber, %User{} = target) do
1191 # Note: the relationship is inverse: subscriber acts as relationship target
1192 UserRelationship.delete_inverse_subscription(target, unsubscriber)
1195 def unsubscribe(%User{} = unsubscriber, %{ap_id: ap_id}) do
1196 with %User{} = user <- get_cached_by_ap_id(ap_id) do
1197 unsubscribe(unsubscriber, user)
1201 def block(%User{} = blocker, %User{} = blocked) do
1202 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
1204 if following?(blocker, blocked) do
1205 {:ok, blocker, _} = unfollow(blocker, blocked)
1211 # clear any requested follows as well
1213 case CommonAPI.reject_follow_request(blocked, blocker) do
1214 {:ok, %User{} = updated_blocked} -> updated_blocked
1218 unsubscribe(blocked, blocker)
1220 if following?(blocked, blocker), do: unfollow(blocked, blocker)
1222 {:ok, blocker} = update_follower_count(blocker)
1223 {:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
1224 add_to_block(blocker, blocked)
1227 # helper to handle the block given only an actor's AP id
1228 def block(%User{} = blocker, %{ap_id: ap_id}) do
1229 block(blocker, get_cached_by_ap_id(ap_id))
1232 def unblock(%User{} = blocker, %User{} = blocked) do
1233 remove_from_block(blocker, blocked)
1236 # helper to handle the block given only an actor's AP id
1237 def unblock(%User{} = blocker, %{ap_id: ap_id}) do
1238 unblock(blocker, get_cached_by_ap_id(ap_id))
1241 def mutes?(nil, _), do: false
1242 def mutes?(%User{} = user, %User{} = target), do: mutes_user?(user, target)
1244 def mutes_user?(%User{} = user, %User{} = target) do
1245 UserRelationship.mute_exists?(user, target)
1248 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
1249 def muted_notifications?(nil, _), do: false
1251 def muted_notifications?(%User{} = user, %User{} = target),
1252 do: UserRelationship.notification_mute_exists?(user, target)
1254 def blocks?(nil, _), do: false
1256 def blocks?(%User{} = user, %User{} = target) do
1257 blocks_user?(user, target) ||
1258 (blocks_domain?(user, target) and not User.following?(user, target))
1261 def blocks_user?(%User{} = user, %User{} = target) do
1262 UserRelationship.block_exists?(user, target)
1265 def blocks_user?(_, _), do: false
1267 def blocks_domain?(%User{} = user, %User{} = target) do
1268 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
1269 %{host: host} = URI.parse(target.ap_id)
1270 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1273 def blocks_domain?(_, _), do: false
1275 def subscribed_to?(%User{} = user, %User{} = target) do
1276 # Note: the relationship is inverse: subscriber acts as relationship target
1277 UserRelationship.inverse_subscription_exists?(target, user)
1280 def subscribed_to?(%User{} = user, %{ap_id: ap_id}) do
1281 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1282 subscribed_to?(user, target)
1287 Returns map of outgoing (blocked, muted etc.) relationships' user AP IDs by relation type.
1288 E.g. `outgoing_relationships_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}`
1290 @spec outgoing_relationships_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())}
1291 def outgoing_relationships_ap_ids(_user, []), do: %{}
1293 def outgoing_relationships_ap_ids(nil, _relationship_types), do: %{}
1295 def outgoing_relationships_ap_ids(%User{} = user, relationship_types)
1296 when is_list(relationship_types) do
1299 |> assoc(:outgoing_relationships)
1300 |> join(:inner, [user_rel], u in assoc(user_rel, :target))
1301 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1302 |> select([user_rel, u], [user_rel.relationship_type, fragment("array_agg(?)", u.ap_id)])
1303 |> group_by([user_rel, u], user_rel.relationship_type)
1305 |> Enum.into(%{}, fn [k, v] -> {k, v} end)
1310 fn rel_type -> {rel_type, db_result[rel_type] || []} end
1314 def incoming_relationships_ungrouped_ap_ids(user, relationship_types, ap_ids \\ nil)
1316 def incoming_relationships_ungrouped_ap_ids(_user, [], _ap_ids), do: []
1318 def incoming_relationships_ungrouped_ap_ids(nil, _relationship_types, _ap_ids), do: []
1320 def incoming_relationships_ungrouped_ap_ids(%User{} = user, relationship_types, ap_ids)
1321 when is_list(relationship_types) do
1323 |> assoc(:incoming_relationships)
1324 |> join(:inner, [user_rel], u in assoc(user_rel, :source))
1325 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1326 |> maybe_filter_on_ap_id(ap_ids)
1327 |> select([user_rel, u], u.ap_id)
1332 defp maybe_filter_on_ap_id(query, ap_ids) when is_list(ap_ids) do
1333 where(query, [user_rel, u], u.ap_id in ^ap_ids)
1336 defp maybe_filter_on_ap_id(query, _ap_ids), do: query
1338 def deactivate_async(user, status \\ true) do
1339 BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
1342 def deactivate(user, status \\ true)
1344 def deactivate(users, status) when is_list(users) do
1345 Repo.transaction(fn ->
1346 for user <- users, do: deactivate(user, status)
1350 def deactivate(%User{} = user, status) do
1351 with {:ok, user} <- set_activation_status(user, status) do
1354 |> Enum.filter(& &1.local)
1355 |> Enum.each(fn follower ->
1356 follower |> update_following_count() |> set_cache()
1359 # Only update local user counts, remote will be update during the next pull.
1362 |> Enum.filter(& &1.local)
1363 |> Enum.each(&update_follower_count/1)
1369 def update_notification_settings(%User{} = user, settings) do
1371 |> cast(%{notification_settings: settings}, [])
1372 |> cast_embed(:notification_settings)
1373 |> validate_required([:notification_settings])
1374 |> update_and_set_cache()
1377 def delete(users) when is_list(users) do
1378 for user <- users, do: delete(user)
1381 def delete(%User{} = user) do
1382 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1385 def perform(:force_password_reset, user), do: force_password_reset(user)
1387 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1388 def perform(:delete, %User{} = user) do
1389 {:ok, _user} = ActivityPub.delete(user)
1391 # Remove all relationships
1394 |> Enum.each(fn follower ->
1395 ActivityPub.unfollow(follower, user)
1396 unfollow(follower, user)
1401 |> Enum.each(fn followed ->
1402 ActivityPub.unfollow(user, followed)
1403 unfollow(user, followed)
1406 delete_user_activities(user)
1407 invalidate_cache(user)
1411 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1413 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1414 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1415 when is_list(blocked_identifiers) do
1417 blocked_identifiers,
1418 fn blocked_identifier ->
1419 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1420 {:ok, _user_block} <- block(blocker, blocked),
1421 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1425 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1432 def perform(:follow_import, %User{} = follower, followed_identifiers)
1433 when is_list(followed_identifiers) do
1435 followed_identifiers,
1436 fn followed_identifier ->
1437 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1438 {:ok, follower} <- maybe_direct_follow(follower, followed),
1439 {:ok, _} <- ActivityPub.follow(follower, followed) do
1443 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1450 @spec external_users_query() :: Ecto.Query.t()
1451 def external_users_query do
1459 @spec external_users(keyword()) :: [User.t()]
1460 def external_users(opts \\ []) do
1462 external_users_query()
1463 |> select([u], struct(u, [:id, :ap_id]))
1467 do: where(query, [u], u.id > ^opts[:max_id]),
1472 do: limit(query, ^opts[:limit]),
1478 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1479 BackgroundWorker.enqueue("blocks_import", %{
1480 "blocker_id" => blocker.id,
1481 "blocked_identifiers" => blocked_identifiers
1485 def follow_import(%User{} = follower, followed_identifiers)
1486 when is_list(followed_identifiers) do
1487 BackgroundWorker.enqueue("follow_import", %{
1488 "follower_id" => follower.id,
1489 "followed_identifiers" => followed_identifiers
1493 def delete_user_activities(%User{ap_id: ap_id}) do
1495 |> Activity.Queries.by_actor()
1496 |> RepoStreamer.chunk_stream(50)
1497 |> Stream.each(fn activities -> Enum.each(activities, &delete_activity/1) end)
1501 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1503 |> Object.normalize()
1504 |> ActivityPub.delete()
1507 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1508 object = Object.normalize(activity)
1511 |> get_cached_by_ap_id()
1512 |> ActivityPub.unlike(object)
1515 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1516 object = Object.normalize(activity)
1519 |> get_cached_by_ap_id()
1520 |> ActivityPub.unannounce(object)
1523 defp delete_activity(_activity), do: "Doing nothing"
1525 def html_filter_policy(%User{no_rich_text: true}) do
1526 Pleroma.HTML.Scrubber.TwitterText
1529 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1531 def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
1533 def get_or_fetch_by_ap_id(ap_id) do
1534 user = get_cached_by_ap_id(ap_id)
1536 if !is_nil(user) and !needs_update?(user) do
1539 fetch_by_ap_id(ap_id)
1544 Creates an internal service actor by URI if missing.
1545 Optionally takes nickname for addressing.
1547 @spec get_or_create_service_actor_by_ap_id(String.t(), String.t()) :: User.t() | nil
1548 def get_or_create_service_actor_by_ap_id(uri, nickname) do
1550 case get_cached_by_ap_id(uri) do
1552 with {:error, %{errors: errors}} <- create_service_actor(uri, nickname) do
1553 Logger.error("Cannot create service actor: #{uri}/.\n#{inspect(errors)}")
1557 %User{invisible: false} = user ->
1567 @spec set_invisible(User.t()) :: {:ok, User.t()}
1568 defp set_invisible(user) do
1570 |> change(%{invisible: true})
1571 |> update_and_set_cache()
1574 @spec create_service_actor(String.t(), String.t()) ::
1575 {:ok, User.t()} | {:error, Ecto.Changeset.t()}
1576 defp create_service_actor(uri, nickname) do
1582 follower_address: uri <> "/followers"
1585 |> unique_constraint(:nickname)
1591 def public_key(%{source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}}) do
1594 |> :public_key.pem_decode()
1596 |> :public_key.pem_entry_decode()
1601 def public_key(_), do: {:error, "not found key"}
1603 def get_public_key_for_ap_id(ap_id) do
1604 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1605 {:ok, public_key} <- public_key(user) do
1612 def ap_enabled?(%User{local: true}), do: true
1613 def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled
1614 def ap_enabled?(_), do: false
1616 @doc "Gets or fetch a user by uri or nickname."
1617 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1618 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1619 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1621 # wait a period of time and return newest version of the User structs
1622 # this is because we have synchronous follow APIs and need to simulate them
1623 # with an async handshake
1624 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1625 with %User{} = a <- get_cached_by_id(a.id),
1626 %User{} = b <- get_cached_by_id(b.id) do
1633 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1634 with :ok <- :timer.sleep(timeout),
1635 %User{} = a <- get_cached_by_id(a.id),
1636 %User{} = b <- get_cached_by_id(b.id) do
1643 def parse_bio(bio) when is_binary(bio) and bio != "" do
1645 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1649 def parse_bio(_), do: ""
1651 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1652 # TODO: get profile URLs other than user.ap_id
1653 profile_urls = [user.ap_id]
1656 |> CommonUtils.format_input("text/plain",
1657 mentions_format: :full,
1658 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1663 def parse_bio(_, _), do: ""
1665 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1666 Repo.transaction(fn ->
1667 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1671 def tag(nickname, tags) when is_binary(nickname),
1672 do: tag(get_by_nickname(nickname), tags)
1674 def tag(%User{} = user, tags),
1675 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1677 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1678 Repo.transaction(fn ->
1679 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1683 def untag(nickname, tags) when is_binary(nickname),
1684 do: untag(get_by_nickname(nickname), tags)
1686 def untag(%User{} = user, tags),
1687 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1689 defp update_tags(%User{} = user, new_tags) do
1690 {:ok, updated_user} =
1692 |> change(%{tags: new_tags})
1693 |> update_and_set_cache()
1698 defp normalize_tags(tags) do
1701 |> Enum.map(&String.downcase/1)
1704 defp local_nickname_regex do
1705 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1706 @extended_local_nickname_regex
1708 @strict_local_nickname_regex
1712 def local_nickname(nickname_or_mention) do
1715 |> String.split("@")
1719 def full_nickname(nickname_or_mention),
1720 do: String.trim_leading(nickname_or_mention, "@")
1722 def error_user(ap_id) do
1726 nickname: "erroruser@example.com",
1727 inserted_at: NaiveDateTime.utc_now()
1731 @spec all_superusers() :: [User.t()]
1732 def all_superusers do
1733 User.Query.build(%{super_users: true, local: true, deactivated: false})
1737 def muting_reblogs?(%User{} = user, %User{} = target) do
1738 UserRelationship.reblog_mute_exists?(user, target)
1741 def showing_reblogs?(%User{} = user, %User{} = target) do
1742 not muting_reblogs?(user, target)
1746 The function returns a query to get users with no activity for given interval of days.
1747 Inactive users are those who didn't read any notification, or had any activity where
1748 the user is the activity's actor, during `inactivity_threshold` days.
1749 Deactivated users will not appear in this list.
1753 iex> Pleroma.User.list_inactive_users()
1756 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1757 def list_inactive_users_query(inactivity_threshold \\ 7) do
1758 negative_inactivity_threshold = -inactivity_threshold
1759 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1760 # Subqueries are not supported in `where` clauses, join gets too complicated.
1761 has_read_notifications =
1762 from(n in Pleroma.Notification,
1763 where: n.seen == true,
1765 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1768 |> Pleroma.Repo.all()
1770 from(u in Pleroma.User,
1771 left_join: a in Pleroma.Activity,
1772 on: u.ap_id == a.actor,
1773 where: not is_nil(u.nickname),
1774 where: u.deactivated != ^true,
1775 where: u.id not in ^has_read_notifications,
1778 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1779 is_nil(max(a.inserted_at))
1784 Enable or disable email notifications for user
1788 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => false}}, "digest", true)
1789 Pleroma.User{email_notifications: %{"digest" => true}}
1791 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => true}}, "digest", false)
1792 Pleroma.User{email_notifications: %{"digest" => false}}
1794 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1795 {:ok, t()} | {:error, Ecto.Changeset.t()}
1796 def switch_email_notifications(user, type, status) do
1797 User.update_email_notifications(user, %{type => status})
1801 Set `last_digest_emailed_at` value for the user to current time
1803 @spec touch_last_digest_emailed_at(t()) :: t()
1804 def touch_last_digest_emailed_at(user) do
1805 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1807 {:ok, updated_user} =
1809 |> change(%{last_digest_emailed_at: now})
1810 |> update_and_set_cache()
1815 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1816 def toggle_confirmation(%User{} = user) do
1818 |> confirmation_changeset(need_confirmation: !user.confirmation_pending)
1819 |> update_and_set_cache()
1822 @spec toggle_confirmation([User.t()]) :: [{:ok, User.t()} | {:error, Changeset.t()}]
1823 def toggle_confirmation(users) do
1824 Enum.map(users, &toggle_confirmation/1)
1827 def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
1831 def get_mascot(%{mascot: mascot}) when is_nil(mascot) do
1832 # use instance-default
1833 config = Pleroma.Config.get([:assets, :mascots])
1834 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1835 mascot = Keyword.get(config, default_mascot)
1838 "id" => "default-mascot",
1839 "url" => mascot[:url],
1840 "preview_url" => mascot[:url],
1842 "mime_type" => mascot[:mime_type]
1847 def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
1849 def ensure_keys_present(%User{} = user) do
1850 with {:ok, pem} <- Keys.generate_rsa_pem() do
1852 |> cast(%{keys: pem}, [:keys])
1853 |> validate_required([:keys])
1854 |> update_and_set_cache()
1858 def get_ap_ids_by_nicknames(nicknames) do
1860 where: u.nickname in ^nicknames,
1866 defdelegate search(query, opts \\ []), to: User.Search
1868 defp put_password_hash(
1869 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1871 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1874 defp put_password_hash(changeset), do: changeset
1876 def is_internal_user?(%User{nickname: nil}), do: true
1877 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1878 def is_internal_user?(_), do: false
1880 # A hack because user delete activities have a fake id for whatever reason
1881 # TODO: Get rid of this
1882 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
1884 def get_delivered_users_by_object_id(object_id) do
1886 inner_join: delivery in assoc(u, :deliveries),
1887 where: delivery.object_id == ^object_id
1892 def change_email(user, email) do
1894 |> cast(%{email: email}, [:email])
1895 |> validate_required([:email])
1896 |> unique_constraint(:email)
1897 |> validate_format(:email, @email_regex)
1898 |> update_and_set_cache()
1901 # Internal function; public one is `deactivate/2`
1902 defp set_activation_status(user, deactivated) do
1904 |> cast(%{deactivated: deactivated}, [:deactivated])
1905 |> update_and_set_cache()
1908 def update_banner(user, banner) do
1910 |> cast(%{banner: banner}, [:banner])
1911 |> update_and_set_cache()
1914 def update_background(user, background) do
1916 |> cast(%{background: background}, [:background])
1917 |> update_and_set_cache()
1920 def update_source_data(user, source_data) do
1922 |> cast(%{source_data: source_data}, [:source_data])
1923 |> update_and_set_cache()
1926 def roles(%{is_moderator: is_moderator, is_admin: is_admin}) do
1929 moderator: is_moderator
1933 # ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``.
1934 # For example: [{"name": "Pronoun", "value": "she/her"}, …]
1935 def fields(%{fields: nil, source_data: %{"attachment" => attachment}}) do
1936 limit = Pleroma.Config.get([:instance, :max_remote_account_fields], 0)
1939 |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
1940 |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
1944 def fields(%{fields: nil}), do: []
1946 def fields(%{fields: fields}), do: fields
1948 def validate_fields(changeset, remote? \\ false) do
1949 limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
1950 limit = Pleroma.Config.get([:instance, limit_name], 0)
1953 |> validate_length(:fields, max: limit)
1954 |> validate_change(:fields, fn :fields, fields ->
1955 if Enum.all?(fields, &valid_field?/1) do
1963 defp valid_field?(%{"name" => name, "value" => value}) do
1964 name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255)
1965 value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255)
1967 is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
1968 String.length(value) <= value_limit
1971 defp valid_field?(_), do: false
1973 defp truncate_field(%{"name" => name, "value" => value}) do
1975 String.split_at(name, Pleroma.Config.get([:instance, :account_field_name_length], 255))
1978 String.split_at(value, Pleroma.Config.get([:instance, :account_field_value_length], 255))
1980 %{"name" => name, "value" => value}
1983 def admin_api_update(user, params) do
1990 |> update_and_set_cache()
1993 @doc "Signs user out of all applications"
1994 def global_sign_out(user) do
1995 OAuth.Authorization.delete_user_authorizations(user)
1996 OAuth.Token.delete_user_tokens(user)
1999 def mascot_update(user, url) do
2001 |> cast(%{mascot: url}, [:mascot])
2002 |> validate_required([:mascot])
2003 |> update_and_set_cache()
2006 def mastodon_settings_update(user, settings) do
2008 |> cast(%{settings: settings}, [:settings])
2009 |> validate_required([:settings])
2010 |> update_and_set_cache()
2013 @spec confirmation_changeset(User.t(), keyword()) :: Changeset.t()
2014 def confirmation_changeset(user, need_confirmation: need_confirmation?) do
2016 if need_confirmation? do
2018 confirmation_pending: true,
2019 confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
2023 confirmation_pending: false,
2024 confirmation_token: nil
2028 cast(user, params, [:confirmation_pending, :confirmation_token])
2031 def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do
2032 if id not in user.pinned_activities do
2033 max_pinned_statuses = Pleroma.Config.get([:instance, :max_pinned_statuses], 0)
2034 params = %{pinned_activities: user.pinned_activities ++ [id]}
2037 |> cast(params, [:pinned_activities])
2038 |> validate_length(:pinned_activities,
2039 max: max_pinned_statuses,
2040 message: "You have already pinned the maximum number of statuses"
2045 |> update_and_set_cache()
2048 def remove_pinnned_activity(user, %Pleroma.Activity{id: id}) do
2049 params = %{pinned_activities: List.delete(user.pinned_activities, id)}
2052 |> cast(params, [:pinned_activities])
2053 |> update_and_set_cache()
2056 def update_email_notifications(user, settings) do
2057 email_notifications =
2058 user.email_notifications
2059 |> Map.merge(settings)
2060 |> Map.take(["digest"])
2062 params = %{email_notifications: email_notifications}
2063 fields = [:email_notifications]
2066 |> cast(params, fields)
2067 |> validate_required(fields)
2068 |> update_and_set_cache()
2071 defp set_domain_blocks(user, domain_blocks) do
2072 params = %{domain_blocks: domain_blocks}
2075 |> cast(params, [:domain_blocks])
2076 |> validate_required([:domain_blocks])
2077 |> update_and_set_cache()
2080 def block_domain(user, domain_blocked) do
2081 set_domain_blocks(user, Enum.uniq([domain_blocked | user.domain_blocks]))
2084 def unblock_domain(user, domain_blocked) do
2085 set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked))
2088 @spec add_to_block(User.t(), User.t()) ::
2089 {:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()}
2090 defp add_to_block(%User{} = user, %User{} = blocked) do
2091 UserRelationship.create_block(user, blocked)
2094 @spec add_to_block(User.t(), User.t()) ::
2095 {:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()}
2096 defp remove_from_block(%User{} = user, %User{} = blocked) do
2097 UserRelationship.delete_block(user, blocked)
2100 defp add_to_mutes(%User{} = user, %User{} = muted_user, notifications?) do
2101 with {:ok, user_mute} <- UserRelationship.create_mute(user, muted_user),
2102 {:ok, user_notification_mute} <-
2103 (notifications? && UserRelationship.create_notification_mute(user, muted_user)) ||
2105 {:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
2109 defp remove_from_mutes(user, %User{} = muted_user) do
2110 with {:ok, user_mute} <- UserRelationship.delete_mute(user, muted_user),
2111 {:ok, user_notification_mute} <-
2112 UserRelationship.delete_notification_mute(user, muted_user) do
2113 {:ok, [user_mute, user_notification_mute]}
2117 def set_invisible(user, invisible) do
2118 params = %{invisible: invisible}
2121 |> cast(params, [:invisible])
2122 |> validate_required([:invisible])
2123 |> update_and_set_cache()
2126 def sanitize_html(%User{} = user) do
2127 sanitize_html(user, nil)
2130 # User data that mastodon isn't filtering (treated as plaintext):
2133 def sanitize_html(%User{} = user, filter) do
2137 |> Enum.map(fn %{"name" => name, "value" => value} ->
2140 "value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
2145 |> Map.put(:bio, HTML.filter_tags(user.bio, filter))
2146 |> Map.put(:fields, fields)