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)
229 @doc "Returns status account"
230 @spec account_status(User.t()) :: account_status()
231 def account_status(%User{deactivated: true}), do: :deactivated
232 def account_status(%User{password_reset_pending: true}), do: :password_reset_pending
234 def account_status(%User{confirmation_pending: true}) do
235 case Config.get([:instance, :account_activation_required]) do
236 true -> :confirmation_pending
241 def account_status(%User{}), do: :active
243 @spec visible_for?(User.t(), User.t() | nil) :: boolean()
244 def visible_for?(user, for_user \\ nil)
246 def visible_for?(%User{invisible: true}, _), do: false
248 def visible_for?(%User{id: user_id}, %User{id: user_id}), do: true
250 def visible_for?(%User{local: local} = user, nil) do
256 if Config.get([:restrict_unauthenticated, :profiles, cfg_key]),
258 else: account_status(user) == :active
261 def visible_for?(%User{} = user, for_user) do
262 account_status(user) == :active || superuser?(for_user)
265 def visible_for?(_, _), do: false
267 @spec superuser?(User.t()) :: boolean()
268 def superuser?(%User{local: true, is_admin: true}), do: true
269 def superuser?(%User{local: true, is_moderator: true}), do: true
270 def superuser?(_), do: false
272 @spec invisible?(User.t()) :: boolean()
273 def invisible?(%User{invisible: true}), do: true
274 def invisible?(_), do: false
276 def avatar_url(user, options \\ []) do
278 %{"url" => [%{"href" => href} | _]} -> href
279 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
283 def banner_url(user, options \\ []) do
285 %{"url" => [%{"href" => href} | _]} -> href
286 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
290 def profile_url(%User{source_data: %{"url" => url}}), do: url
291 def profile_url(%User{ap_id: ap_id}), do: ap_id
292 def profile_url(_), do: nil
294 def ap_id(%User{nickname: nickname}), do: "#{Web.base_url()}/users/#{nickname}"
296 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
297 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
299 @spec ap_following(User.t()) :: Sring.t()
300 def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
301 def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
303 @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
304 def restrict_deactivated(query) do
305 from(u in query, where: u.deactivated != ^true)
308 defdelegate following_count(user), to: FollowingRelationship
310 defp truncate_fields_param(params) do
311 if Map.has_key?(params, :fields) do
312 Map.put(params, :fields, Enum.map(params[:fields], &truncate_field/1))
318 defp truncate_if_exists(params, key, max_length) do
319 if Map.has_key?(params, key) and is_binary(params[key]) do
320 {value, _chopped} = String.split_at(params[key], max_length)
321 Map.put(params, key, value)
327 def remote_user_creation(params) do
328 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
329 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
333 |> truncate_if_exists(:name, name_limit)
334 |> truncate_if_exists(:bio, bio_limit)
335 |> truncate_fields_param()
355 :hide_followers_count,
366 |> validate_required([:name, :ap_id])
367 |> unique_constraint(:nickname)
368 |> validate_format(:nickname, @email_regex)
369 |> validate_length(:bio, max: bio_limit)
370 |> validate_length(:name, max: name_limit)
371 |> validate_fields(true)
373 case params[:source_data] do
374 %{"followers" => followers, "following" => following} ->
376 |> put_change(:follower_address, followers)
377 |> put_change(:following_address, following)
380 followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
381 put_change(changeset, :follower_address, followers)
385 def update_changeset(struct, params \\ %{}) do
386 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
387 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
402 :hide_followers_count,
405 :allow_following_move,
408 :skip_thread_containment,
411 :pleroma_settings_store,
417 |> unique_constraint(:nickname)
418 |> validate_format(:nickname, local_nickname_regex())
419 |> validate_length(:bio, max: bio_limit)
420 |> validate_length(:name, min: 1, max: name_limit)
422 |> put_change_if_present(:bio, &{:ok, parse_bio(&1, struct)})
423 |> put_change_if_present(:avatar, &put_upload(&1, :avatar))
424 |> put_change_if_present(:banner, &put_upload(&1, :banner))
425 |> put_change_if_present(:background, &put_upload(&1, :background))
426 |> put_change_if_present(
427 :pleroma_settings_store,
428 &{:ok, Map.merge(struct.pleroma_settings_store, &1)}
430 |> validate_fields(false)
433 defp put_fields(changeset) do
434 if raw_fields = get_change(changeset, :raw_fields) do
437 |> Enum.filter(fn %{"name" => n} -> n != "" end)
441 |> Enum.map(fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
444 |> put_change(:raw_fields, raw_fields)
445 |> put_change(:fields, fields)
451 defp put_change_if_present(changeset, map_field, value_function) do
452 if value = get_change(changeset, map_field) do
453 with {:ok, new_value} <- value_function.(value) do
454 put_change(changeset, map_field, new_value)
463 defp put_upload(value, type) do
464 with %Plug.Upload{} <- value,
465 {:ok, object} <- ActivityPub.upload(value, type: type) do
470 def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
471 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
472 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
474 params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
476 params = if remote?, do: truncate_fields_param(params), else: params
498 :allow_following_move,
500 :hide_followers_count,
506 |> unique_constraint(:nickname)
507 |> validate_format(:nickname, local_nickname_regex())
508 |> validate_length(:bio, max: bio_limit)
509 |> validate_length(:name, max: name_limit)
510 |> validate_fields(remote?)
513 def update_as_admin_changeset(struct, params) do
515 |> update_changeset(params)
516 |> cast(params, [:email])
517 |> delete_change(:also_known_as)
518 |> unique_constraint(:email)
519 |> validate_format(:email, @email_regex)
522 @spec update_as_admin(%User{}, map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
523 def update_as_admin(user, params) do
524 params = Map.put(params, "password_confirmation", params["password"])
525 changeset = update_as_admin_changeset(user, params)
527 if params["password"] do
528 reset_password(user, changeset, params)
530 User.update_and_set_cache(changeset)
534 def password_update_changeset(struct, params) do
536 |> cast(params, [:password, :password_confirmation])
537 |> validate_required([:password, :password_confirmation])
538 |> validate_confirmation(:password)
539 |> put_password_hash()
540 |> put_change(:password_reset_pending, false)
543 @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
544 def reset_password(%User{} = user, params) do
545 reset_password(user, user, params)
548 def reset_password(%User{id: user_id} = user, struct, params) do
551 |> Multi.update(:user, password_update_changeset(struct, params))
552 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
553 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
555 case Repo.transaction(multi) do
556 {:ok, %{user: user} = _} -> set_cache(user)
557 {:error, _, changeset, _} -> {:error, changeset}
561 def update_password_reset_pending(user, value) do
564 |> put_change(:password_reset_pending, value)
565 |> update_and_set_cache()
568 def force_password_reset_async(user) do
569 BackgroundWorker.enqueue("force_password_reset", %{"user_id" => user.id})
572 @spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
573 def force_password_reset(user), do: update_password_reset_pending(user, true)
575 def register_changeset(struct, params \\ %{}, opts \\ []) do
576 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
577 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
580 if is_nil(opts[:need_confirmation]) do
581 Pleroma.Config.get([:instance, :account_activation_required])
583 opts[:need_confirmation]
587 |> confirmation_changeset(need_confirmation: need_confirmation?)
588 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
589 |> validate_required([:name, :nickname, :password, :password_confirmation])
590 |> validate_confirmation(:password)
591 |> unique_constraint(:email)
592 |> unique_constraint(:nickname)
593 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
594 |> validate_format(:nickname, local_nickname_regex())
595 |> validate_format(:email, @email_regex)
596 |> validate_length(:bio, max: bio_limit)
597 |> validate_length(:name, min: 1, max: name_limit)
598 |> maybe_validate_required_email(opts[:external])
601 |> unique_constraint(:ap_id)
602 |> put_following_and_follower_address()
605 def maybe_validate_required_email(changeset, true), do: changeset
607 def maybe_validate_required_email(changeset, _) do
608 if Pleroma.Config.get([:instance, :account_activation_required]) do
609 validate_required(changeset, [:email])
615 defp put_ap_id(changeset) do
616 ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
617 put_change(changeset, :ap_id, ap_id)
620 defp put_following_and_follower_address(changeset) do
621 followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
624 |> put_change(:follower_address, followers)
627 defp autofollow_users(user) do
628 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
631 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
634 follow_all(user, autofollowed_users)
637 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
638 def register(%Ecto.Changeset{} = changeset) do
639 with {:ok, user} <- Repo.insert(changeset) do
640 post_register_action(user)
644 def post_register_action(%User{} = user) do
645 with {:ok, user} <- autofollow_users(user),
646 {:ok, user} <- set_cache(user),
647 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
648 {:ok, _} <- try_send_confirmation_email(user) do
653 def try_send_confirmation_email(%User{} = user) do
654 if user.confirmation_pending &&
655 Pleroma.Config.get([:instance, :account_activation_required]) do
657 |> Pleroma.Emails.UserEmail.account_confirmation_email()
658 |> Pleroma.Emails.Mailer.deliver_async()
666 def try_send_confirmation_email(users) do
667 Enum.each(users, &try_send_confirmation_email/1)
670 def needs_update?(%User{local: true}), do: false
672 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
674 def needs_update?(%User{local: false} = user) do
675 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
678 def needs_update?(_), do: true
680 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
681 def maybe_direct_follow(%User{} = follower, %User{local: true, locked: true} = followed) do
682 follow(follower, followed, "pending")
685 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
686 follow(follower, followed)
689 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
690 if not ap_enabled?(followed) do
691 follow(follower, followed)
697 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
698 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
699 def follow_all(follower, followeds) do
701 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
702 |> Enum.each(&follow(follower, &1, "accept"))
707 defdelegate following(user), to: FollowingRelationship
709 def follow(%User{} = follower, %User{} = followed, state \\ "accept") do
710 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
713 followed.deactivated ->
714 {:error, "Could not follow user: #{followed.nickname} is deactivated."}
716 deny_follow_blocked and blocks?(followed, follower) ->
717 {:error, "Could not follow user: #{followed.nickname} blocked you."}
720 FollowingRelationship.follow(follower, followed, state)
722 {:ok, _} = update_follower_count(followed)
725 |> update_following_count()
730 def unfollow(%User{ap_id: ap_id}, %User{ap_id: ap_id}) do
731 {:error, "Not subscribed!"}
734 def unfollow(%User{} = follower, %User{} = followed) do
735 case get_follow_state(follower, followed) do
736 state when state in ["accept", "pending"] ->
737 FollowingRelationship.unfollow(follower, followed)
738 {:ok, followed} = update_follower_count(followed)
742 |> update_following_count()
745 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
748 {:error, "Not subscribed!"}
752 defdelegate following?(follower, followed), to: FollowingRelationship
754 def get_follow_state(%User{} = follower, %User{} = following) do
755 following_relationship = FollowingRelationship.get(follower, following)
757 case {following_relationship, following.local} do
759 case Utils.fetch_latest_follow(follower, following) do
760 %{data: %{"state" => state}} when state in ["pending", "accept"] -> state
764 {%{state: state}, _} ->
772 def locked?(%User{} = user) do
777 Repo.get_by(User, id: id)
780 def get_by_ap_id(ap_id) do
781 Repo.get_by(User, ap_id: ap_id)
784 def get_all_by_ap_id(ap_ids) do
785 from(u in __MODULE__,
786 where: u.ap_id in ^ap_ids
791 def get_all_by_ids(ids) do
792 from(u in __MODULE__, where: u.id in ^ids)
796 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
797 # of the ap_id and the domain and tries to get that user
798 def get_by_guessed_nickname(ap_id) do
799 domain = URI.parse(ap_id).host
800 name = List.last(String.split(ap_id, "/"))
801 nickname = "#{name}@#{domain}"
803 get_cached_by_nickname(nickname)
806 def set_cache({:ok, user}), do: set_cache(user)
807 def set_cache({:error, err}), do: {:error, err}
809 def set_cache(%User{} = user) do
810 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
811 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
815 def update_and_set_cache(struct, params) do
817 |> update_changeset(params)
818 |> update_and_set_cache()
821 def update_and_set_cache(changeset) do
822 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
827 def invalidate_cache(user) do
828 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
829 Cachex.del(:user_cache, "nickname:#{user.nickname}")
832 @spec get_cached_by_ap_id(String.t()) :: User.t() | nil
833 def get_cached_by_ap_id(ap_id) do
834 key = "ap_id:#{ap_id}"
836 with {:ok, nil} <- Cachex.get(:user_cache, key),
837 user when not is_nil(user) <- get_by_ap_id(ap_id),
838 {:ok, true} <- Cachex.put(:user_cache, key, user) do
846 def get_cached_by_id(id) do
850 Cachex.fetch!(:user_cache, key, fn _ ->
854 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
855 {:commit, user.ap_id}
861 get_cached_by_ap_id(ap_id)
864 def get_cached_by_nickname(nickname) do
865 key = "nickname:#{nickname}"
867 Cachex.fetch!(:user_cache, key, fn ->
868 case get_or_fetch_by_nickname(nickname) do
869 {:ok, user} -> {:commit, user}
870 {:error, _error} -> {:ignore, nil}
875 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
876 restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
879 is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
880 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
882 restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
883 get_cached_by_nickname(nickname_or_id)
885 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
886 get_cached_by_nickname(nickname_or_id)
893 def get_by_nickname(nickname) do
894 Repo.get_by(User, nickname: nickname) ||
895 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
896 Repo.get_by(User, nickname: local_nickname(nickname))
900 def get_by_email(email), do: Repo.get_by(User, email: email)
902 def get_by_nickname_or_email(nickname_or_email) do
903 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
906 def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname)
908 def get_or_fetch_by_nickname(nickname) do
909 with %User{} = user <- get_by_nickname(nickname) do
913 with [_nick, _domain] <- String.split(nickname, "@"),
914 {:ok, user} <- fetch_by_nickname(nickname) do
917 _e -> {:error, "not found " <> nickname}
922 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
923 def get_followers_query(%User{} = user, nil) do
924 User.Query.build(%{followers: user, deactivated: false})
927 def get_followers_query(user, page) do
929 |> get_followers_query(nil)
930 |> User.Query.paginate(page, 20)
933 @spec get_followers_query(User.t()) :: Ecto.Query.t()
934 def get_followers_query(user), do: get_followers_query(user, nil)
936 @spec get_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
937 def get_followers(user, page \\ nil) do
939 |> get_followers_query(page)
943 @spec get_external_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
944 def get_external_followers(user, page \\ nil) do
946 |> get_followers_query(page)
947 |> User.Query.build(%{external: true})
951 def get_followers_ids(user, page \\ nil) do
953 |> get_followers_query(page)
958 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
959 def get_friends_query(%User{} = user, nil) do
960 User.Query.build(%{friends: user, deactivated: false})
963 def get_friends_query(user, page) do
965 |> get_friends_query(nil)
966 |> User.Query.paginate(page, 20)
969 @spec get_friends_query(User.t()) :: Ecto.Query.t()
970 def get_friends_query(user), do: get_friends_query(user, nil)
972 def get_friends(user, page \\ nil) do
974 |> get_friends_query(page)
978 def get_friends_ap_ids(user) do
980 |> get_friends_query(nil)
981 |> select([u], u.ap_id)
985 def get_friends_ids(user, page \\ nil) do
987 |> get_friends_query(page)
992 defdelegate get_follow_requests(user), to: FollowingRelationship
994 def increase_note_count(%User{} = user) do
996 |> where(id: ^user.id)
997 |> update([u], inc: [note_count: 1])
999 |> Repo.update_all([])
1001 {1, [user]} -> set_cache(user)
1006 def decrease_note_count(%User{} = user) do
1008 |> where(id: ^user.id)
1011 note_count: fragment("greatest(0, note_count - 1)")
1015 |> Repo.update_all([])
1017 {1, [user]} -> set_cache(user)
1022 def update_note_count(%User{} = user, note_count \\ nil) do
1027 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
1033 |> cast(%{note_count: note_count}, [:note_count])
1034 |> update_and_set_cache()
1037 @spec maybe_fetch_follow_information(User.t()) :: User.t()
1038 def maybe_fetch_follow_information(user) do
1039 with {:ok, user} <- fetch_follow_information(user) do
1043 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
1049 def fetch_follow_information(user) do
1050 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
1052 |> follow_information_changeset(info)
1053 |> update_and_set_cache()
1057 defp follow_information_changeset(user, params) do
1064 :hide_followers_count,
1069 def update_follower_count(%User{} = user) do
1070 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
1071 follower_count_query =
1072 User.Query.build(%{followers: user, deactivated: false})
1073 |> select([u], %{count: count(u.id)})
1076 |> where(id: ^user.id)
1077 |> join(:inner, [u], s in subquery(follower_count_query))
1079 set: [follower_count: s.count]
1082 |> Repo.update_all([])
1084 {1, [user]} -> set_cache(user)
1088 {:ok, maybe_fetch_follow_information(user)}
1092 @spec update_following_count(User.t()) :: User.t()
1093 def update_following_count(%User{local: false} = user) do
1094 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
1095 maybe_fetch_follow_information(user)
1101 def update_following_count(%User{local: true} = user) do
1102 following_count = FollowingRelationship.following_count(user)
1105 |> follow_information_changeset(%{following_count: following_count})
1109 def set_unread_conversation_count(%User{local: true} = user) do
1110 unread_query = Participation.unread_conversation_count_for_user(user)
1113 |> join(:inner, [u], p in subquery(unread_query))
1115 set: [unread_conversation_count: p.count]
1117 |> where([u], u.id == ^user.id)
1119 |> Repo.update_all([])
1121 {1, [user]} -> set_cache(user)
1126 def set_unread_conversation_count(user), do: {:ok, user}
1128 def increment_unread_conversation_count(conversation, %User{local: true} = user) do
1130 Participation.unread_conversation_count_for_user(user)
1131 |> where([p], p.conversation_id == ^conversation.id)
1134 |> join(:inner, [u], p in subquery(unread_query))
1136 inc: [unread_conversation_count: 1]
1138 |> where([u], u.id == ^user.id)
1139 |> where([u, p], p.count == 0)
1141 |> Repo.update_all([])
1143 {1, [user]} -> set_cache(user)
1148 def increment_unread_conversation_count(_, user), do: {:ok, user}
1150 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
1151 def get_users_from_set(ap_ids, local_only \\ true) do
1152 criteria = %{ap_id: ap_ids, deactivated: false}
1153 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
1155 User.Query.build(criteria)
1159 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
1160 def get_recipients_from_activity(%Activity{recipients: to}) do
1161 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
1165 @spec mute(User.t(), User.t(), boolean()) ::
1166 {:ok, list(UserRelationship.t())} | {:error, String.t()}
1167 def mute(%User{} = muter, %User{} = mutee, notifications? \\ true) do
1168 add_to_mutes(muter, mutee, notifications?)
1171 def unmute(%User{} = muter, %User{} = mutee) do
1172 remove_from_mutes(muter, mutee)
1175 def subscribe(%User{} = subscriber, %User{} = target) do
1176 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
1178 if blocks?(target, subscriber) and deny_follow_blocked do
1179 {:error, "Could not subscribe: #{target.nickname} is blocking you"}
1181 # Note: the relationship is inverse: subscriber acts as relationship target
1182 UserRelationship.create_inverse_subscription(target, subscriber)
1186 def subscribe(%User{} = subscriber, %{ap_id: ap_id}) do
1187 with %User{} = subscribee <- get_cached_by_ap_id(ap_id) do
1188 subscribe(subscriber, subscribee)
1192 def unsubscribe(%User{} = unsubscriber, %User{} = target) do
1193 # Note: the relationship is inverse: subscriber acts as relationship target
1194 UserRelationship.delete_inverse_subscription(target, unsubscriber)
1197 def unsubscribe(%User{} = unsubscriber, %{ap_id: ap_id}) do
1198 with %User{} = user <- get_cached_by_ap_id(ap_id) do
1199 unsubscribe(unsubscriber, user)
1203 def block(%User{} = blocker, %User{} = blocked) do
1204 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
1206 if following?(blocker, blocked) do
1207 {:ok, blocker, _} = unfollow(blocker, blocked)
1213 # clear any requested follows as well
1215 case CommonAPI.reject_follow_request(blocked, blocker) do
1216 {:ok, %User{} = updated_blocked} -> updated_blocked
1220 unsubscribe(blocked, blocker)
1222 if following?(blocked, blocker), do: unfollow(blocked, blocker)
1224 {:ok, blocker} = update_follower_count(blocker)
1225 {:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
1226 add_to_block(blocker, blocked)
1229 # helper to handle the block given only an actor's AP id
1230 def block(%User{} = blocker, %{ap_id: ap_id}) do
1231 block(blocker, get_cached_by_ap_id(ap_id))
1234 def unblock(%User{} = blocker, %User{} = blocked) do
1235 remove_from_block(blocker, blocked)
1238 # helper to handle the block given only an actor's AP id
1239 def unblock(%User{} = blocker, %{ap_id: ap_id}) do
1240 unblock(blocker, get_cached_by_ap_id(ap_id))
1243 def mutes?(nil, _), do: false
1244 def mutes?(%User{} = user, %User{} = target), do: mutes_user?(user, target)
1246 def mutes_user?(%User{} = user, %User{} = target) do
1247 UserRelationship.mute_exists?(user, target)
1250 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
1251 def muted_notifications?(nil, _), do: false
1253 def muted_notifications?(%User{} = user, %User{} = target),
1254 do: UserRelationship.notification_mute_exists?(user, target)
1256 def blocks?(nil, _), do: false
1258 def blocks?(%User{} = user, %User{} = target) do
1259 blocks_user?(user, target) ||
1260 (!User.following?(user, target) && blocks_domain?(user, target))
1263 def blocks_user?(%User{} = user, %User{} = target) do
1264 UserRelationship.block_exists?(user, target)
1267 def blocks_user?(_, _), do: false
1269 def blocks_domain?(%User{} = user, %User{} = target) do
1270 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
1271 %{host: host} = URI.parse(target.ap_id)
1272 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1275 def blocks_domain?(_, _), do: false
1277 def subscribed_to?(%User{} = user, %User{} = target) do
1278 # Note: the relationship is inverse: subscriber acts as relationship target
1279 UserRelationship.inverse_subscription_exists?(target, user)
1282 def subscribed_to?(%User{} = user, %{ap_id: ap_id}) do
1283 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1284 subscribed_to?(user, target)
1289 Returns map of outgoing (blocked, muted etc.) relationships' user AP IDs by relation type.
1290 E.g. `outgoing_relationships_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}`
1292 @spec outgoing_relationships_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())}
1293 def outgoing_relationships_ap_ids(_user, []), do: %{}
1295 def outgoing_relationships_ap_ids(nil, _relationship_types), do: %{}
1297 def outgoing_relationships_ap_ids(%User{} = user, relationship_types)
1298 when is_list(relationship_types) do
1301 |> assoc(:outgoing_relationships)
1302 |> join(:inner, [user_rel], u in assoc(user_rel, :target))
1303 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1304 |> select([user_rel, u], [user_rel.relationship_type, fragment("array_agg(?)", u.ap_id)])
1305 |> group_by([user_rel, u], user_rel.relationship_type)
1307 |> Enum.into(%{}, fn [k, v] -> {k, v} end)
1312 fn rel_type -> {rel_type, db_result[rel_type] || []} end
1316 def incoming_relationships_ungrouped_ap_ids(user, relationship_types, ap_ids \\ nil)
1318 def incoming_relationships_ungrouped_ap_ids(_user, [], _ap_ids), do: []
1320 def incoming_relationships_ungrouped_ap_ids(nil, _relationship_types, _ap_ids), do: []
1322 def incoming_relationships_ungrouped_ap_ids(%User{} = user, relationship_types, ap_ids)
1323 when is_list(relationship_types) do
1325 |> assoc(:incoming_relationships)
1326 |> join(:inner, [user_rel], u in assoc(user_rel, :source))
1327 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1328 |> maybe_filter_on_ap_id(ap_ids)
1329 |> select([user_rel, u], u.ap_id)
1334 defp maybe_filter_on_ap_id(query, ap_ids) when is_list(ap_ids) do
1335 where(query, [user_rel, u], u.ap_id in ^ap_ids)
1338 defp maybe_filter_on_ap_id(query, _ap_ids), do: query
1340 def deactivate_async(user, status \\ true) do
1341 BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
1344 def deactivate(user, status \\ true)
1346 def deactivate(users, status) when is_list(users) do
1347 Repo.transaction(fn ->
1348 for user <- users, do: deactivate(user, status)
1352 def deactivate(%User{} = user, status) do
1353 with {:ok, user} <- set_activation_status(user, status) do
1356 |> Enum.filter(& &1.local)
1357 |> Enum.each(fn follower ->
1358 follower |> update_following_count() |> set_cache()
1361 # Only update local user counts, remote will be update during the next pull.
1364 |> Enum.filter(& &1.local)
1365 |> Enum.each(&update_follower_count/1)
1371 def update_notification_settings(%User{} = user, settings) do
1373 |> cast(%{notification_settings: settings}, [])
1374 |> cast_embed(:notification_settings)
1375 |> validate_required([:notification_settings])
1376 |> update_and_set_cache()
1379 def delete(users) when is_list(users) do
1380 for user <- users, do: delete(user)
1383 def delete(%User{} = user) do
1384 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1387 def perform(:force_password_reset, user), do: force_password_reset(user)
1389 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1390 def perform(:delete, %User{} = user) do
1391 {:ok, _user} = ActivityPub.delete(user)
1393 # Remove all relationships
1396 |> Enum.each(fn follower ->
1397 ActivityPub.unfollow(follower, user)
1398 unfollow(follower, user)
1403 |> Enum.each(fn followed ->
1404 ActivityPub.unfollow(user, followed)
1405 unfollow(user, followed)
1408 delete_user_activities(user)
1409 invalidate_cache(user)
1413 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1415 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1416 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1417 when is_list(blocked_identifiers) do
1419 blocked_identifiers,
1420 fn blocked_identifier ->
1421 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1422 {:ok, _user_block} <- block(blocker, blocked),
1423 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1427 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1434 def perform(:follow_import, %User{} = follower, followed_identifiers)
1435 when is_list(followed_identifiers) do
1437 followed_identifiers,
1438 fn followed_identifier ->
1439 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1440 {:ok, follower} <- maybe_direct_follow(follower, followed),
1441 {:ok, _} <- ActivityPub.follow(follower, followed) do
1445 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1452 @spec external_users_query() :: Ecto.Query.t()
1453 def external_users_query do
1461 @spec external_users(keyword()) :: [User.t()]
1462 def external_users(opts \\ []) do
1464 external_users_query()
1465 |> select([u], struct(u, [:id, :ap_id]))
1469 do: where(query, [u], u.id > ^opts[:max_id]),
1474 do: limit(query, ^opts[:limit]),
1480 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1481 BackgroundWorker.enqueue("blocks_import", %{
1482 "blocker_id" => blocker.id,
1483 "blocked_identifiers" => blocked_identifiers
1487 def follow_import(%User{} = follower, followed_identifiers)
1488 when is_list(followed_identifiers) do
1489 BackgroundWorker.enqueue("follow_import", %{
1490 "follower_id" => follower.id,
1491 "followed_identifiers" => followed_identifiers
1495 def delete_user_activities(%User{ap_id: ap_id}) do
1497 |> Activity.Queries.by_actor()
1498 |> RepoStreamer.chunk_stream(50)
1499 |> Stream.each(fn activities -> Enum.each(activities, &delete_activity/1) end)
1503 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1505 |> Object.normalize()
1506 |> ActivityPub.delete()
1509 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1510 object = Object.normalize(activity)
1513 |> get_cached_by_ap_id()
1514 |> ActivityPub.unlike(object)
1517 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1518 object = Object.normalize(activity)
1521 |> get_cached_by_ap_id()
1522 |> ActivityPub.unannounce(object)
1525 defp delete_activity(_activity), do: "Doing nothing"
1527 def html_filter_policy(%User{no_rich_text: true}) do
1528 Pleroma.HTML.Scrubber.TwitterText
1531 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1533 def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
1535 def get_or_fetch_by_ap_id(ap_id) do
1536 user = get_cached_by_ap_id(ap_id)
1538 if !is_nil(user) and !needs_update?(user) do
1541 fetch_by_ap_id(ap_id)
1546 Creates an internal service actor by URI if missing.
1547 Optionally takes nickname for addressing.
1549 @spec get_or_create_service_actor_by_ap_id(String.t(), String.t()) :: User.t() | nil
1550 def get_or_create_service_actor_by_ap_id(uri, nickname) do
1552 case get_cached_by_ap_id(uri) do
1554 with {:error, %{errors: errors}} <- create_service_actor(uri, nickname) do
1555 Logger.error("Cannot create service actor: #{uri}/.\n#{inspect(errors)}")
1559 %User{invisible: false} = user ->
1569 @spec set_invisible(User.t()) :: {:ok, User.t()}
1570 defp set_invisible(user) do
1572 |> change(%{invisible: true})
1573 |> update_and_set_cache()
1576 @spec create_service_actor(String.t(), String.t()) ::
1577 {:ok, User.t()} | {:error, Ecto.Changeset.t()}
1578 defp create_service_actor(uri, nickname) do
1584 follower_address: uri <> "/followers"
1587 |> unique_constraint(:nickname)
1593 def public_key(%{source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}}) do
1596 |> :public_key.pem_decode()
1598 |> :public_key.pem_entry_decode()
1603 def public_key(_), do: {:error, "not found key"}
1605 def get_public_key_for_ap_id(ap_id) do
1606 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1607 {:ok, public_key} <- public_key(user) do
1614 defp blank?(""), do: nil
1615 defp blank?(n), do: n
1617 def insert_or_update_user(data) do
1619 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1620 |> remote_user_creation()
1621 |> Repo.insert(on_conflict: {:replace_all_except, [:id]}, conflict_target: :nickname)
1625 def ap_enabled?(%User{local: true}), do: true
1626 def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled
1627 def ap_enabled?(_), do: false
1629 @doc "Gets or fetch a user by uri or nickname."
1630 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1631 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1632 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1634 # wait a period of time and return newest version of the User structs
1635 # this is because we have synchronous follow APIs and need to simulate them
1636 # with an async handshake
1637 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1638 with %User{} = a <- get_cached_by_id(a.id),
1639 %User{} = b <- get_cached_by_id(b.id) do
1646 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1647 with :ok <- :timer.sleep(timeout),
1648 %User{} = a <- get_cached_by_id(a.id),
1649 %User{} = b <- get_cached_by_id(b.id) do
1656 def parse_bio(bio) when is_binary(bio) and bio != "" do
1658 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1662 def parse_bio(_), do: ""
1664 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1665 # TODO: get profile URLs other than user.ap_id
1666 profile_urls = [user.ap_id]
1669 |> CommonUtils.format_input("text/plain",
1670 mentions_format: :full,
1671 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1676 def parse_bio(_, _), do: ""
1678 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1679 Repo.transaction(fn ->
1680 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1684 def tag(nickname, tags) when is_binary(nickname),
1685 do: tag(get_by_nickname(nickname), tags)
1687 def tag(%User{} = user, tags),
1688 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1690 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1691 Repo.transaction(fn ->
1692 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1696 def untag(nickname, tags) when is_binary(nickname),
1697 do: untag(get_by_nickname(nickname), tags)
1699 def untag(%User{} = user, tags),
1700 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1702 defp update_tags(%User{} = user, new_tags) do
1703 {:ok, updated_user} =
1705 |> change(%{tags: new_tags})
1706 |> update_and_set_cache()
1711 defp normalize_tags(tags) do
1714 |> Enum.map(&String.downcase/1)
1717 defp local_nickname_regex do
1718 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1719 @extended_local_nickname_regex
1721 @strict_local_nickname_regex
1725 def local_nickname(nickname_or_mention) do
1728 |> String.split("@")
1732 def full_nickname(nickname_or_mention),
1733 do: String.trim_leading(nickname_or_mention, "@")
1735 def error_user(ap_id) do
1739 nickname: "erroruser@example.com",
1740 inserted_at: NaiveDateTime.utc_now()
1744 @spec all_superusers() :: [User.t()]
1745 def all_superusers do
1746 User.Query.build(%{super_users: true, local: true, deactivated: false})
1750 def showing_reblogs?(%User{} = user, %User{} = target) do
1751 not UserRelationship.reblog_mute_exists?(user, target)
1755 The function returns a query to get users with no activity for given interval of days.
1756 Inactive users are those who didn't read any notification, or had any activity where
1757 the user is the activity's actor, during `inactivity_threshold` days.
1758 Deactivated users will not appear in this list.
1762 iex> Pleroma.User.list_inactive_users()
1765 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1766 def list_inactive_users_query(inactivity_threshold \\ 7) do
1767 negative_inactivity_threshold = -inactivity_threshold
1768 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1769 # Subqueries are not supported in `where` clauses, join gets too complicated.
1770 has_read_notifications =
1771 from(n in Pleroma.Notification,
1772 where: n.seen == true,
1774 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1777 |> Pleroma.Repo.all()
1779 from(u in Pleroma.User,
1780 left_join: a in Pleroma.Activity,
1781 on: u.ap_id == a.actor,
1782 where: not is_nil(u.nickname),
1783 where: u.deactivated != ^true,
1784 where: u.id not in ^has_read_notifications,
1787 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1788 is_nil(max(a.inserted_at))
1793 Enable or disable email notifications for user
1797 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => false}}, "digest", true)
1798 Pleroma.User{email_notifications: %{"digest" => true}}
1800 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => true}}, "digest", false)
1801 Pleroma.User{email_notifications: %{"digest" => false}}
1803 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1804 {:ok, t()} | {:error, Ecto.Changeset.t()}
1805 def switch_email_notifications(user, type, status) do
1806 User.update_email_notifications(user, %{type => status})
1810 Set `last_digest_emailed_at` value for the user to current time
1812 @spec touch_last_digest_emailed_at(t()) :: t()
1813 def touch_last_digest_emailed_at(user) do
1814 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1816 {:ok, updated_user} =
1818 |> change(%{last_digest_emailed_at: now})
1819 |> update_and_set_cache()
1824 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1825 def toggle_confirmation(%User{} = user) do
1827 |> confirmation_changeset(need_confirmation: !user.confirmation_pending)
1828 |> update_and_set_cache()
1831 @spec toggle_confirmation([User.t()]) :: [{:ok, User.t()} | {:error, Changeset.t()}]
1832 def toggle_confirmation(users) do
1833 Enum.map(users, &toggle_confirmation/1)
1836 def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
1840 def get_mascot(%{mascot: mascot}) when is_nil(mascot) do
1841 # use instance-default
1842 config = Pleroma.Config.get([:assets, :mascots])
1843 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1844 mascot = Keyword.get(config, default_mascot)
1847 "id" => "default-mascot",
1848 "url" => mascot[:url],
1849 "preview_url" => mascot[:url],
1851 "mime_type" => mascot[:mime_type]
1856 def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
1858 def ensure_keys_present(%User{} = user) do
1859 with {:ok, pem} <- Keys.generate_rsa_pem() do
1861 |> cast(%{keys: pem}, [:keys])
1862 |> validate_required([:keys])
1863 |> update_and_set_cache()
1867 def get_ap_ids_by_nicknames(nicknames) do
1869 where: u.nickname in ^nicknames,
1875 defdelegate search(query, opts \\ []), to: User.Search
1877 defp put_password_hash(
1878 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1880 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1883 defp put_password_hash(changeset), do: changeset
1885 def is_internal_user?(%User{nickname: nil}), do: true
1886 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1887 def is_internal_user?(_), do: false
1889 # A hack because user delete activities have a fake id for whatever reason
1890 # TODO: Get rid of this
1891 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
1893 def get_delivered_users_by_object_id(object_id) do
1895 inner_join: delivery in assoc(u, :deliveries),
1896 where: delivery.object_id == ^object_id
1901 def change_email(user, email) do
1903 |> cast(%{email: email}, [:email])
1904 |> validate_required([:email])
1905 |> unique_constraint(:email)
1906 |> validate_format(:email, @email_regex)
1907 |> update_and_set_cache()
1910 # Internal function; public one is `deactivate/2`
1911 defp set_activation_status(user, deactivated) do
1913 |> cast(%{deactivated: deactivated}, [:deactivated])
1914 |> update_and_set_cache()
1917 def update_banner(user, banner) do
1919 |> cast(%{banner: banner}, [:banner])
1920 |> update_and_set_cache()
1923 def update_background(user, background) do
1925 |> cast(%{background: background}, [:background])
1926 |> update_and_set_cache()
1929 def update_source_data(user, source_data) do
1931 |> cast(%{source_data: source_data}, [:source_data])
1932 |> update_and_set_cache()
1935 def roles(%{is_moderator: is_moderator, is_admin: is_admin}) do
1938 moderator: is_moderator
1942 # ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``.
1943 # For example: [{"name": "Pronoun", "value": "she/her"}, …]
1944 def fields(%{fields: nil, source_data: %{"attachment" => attachment}}) do
1945 limit = Pleroma.Config.get([:instance, :max_remote_account_fields], 0)
1948 |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
1949 |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
1953 def fields(%{fields: nil}), do: []
1955 def fields(%{fields: fields}), do: fields
1957 def sanitized_fields(%User{} = user) do
1960 |> Enum.map(fn %{"name" => name, "value" => value} ->
1963 "value" => Pleroma.HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
1968 def validate_fields(changeset, remote? \\ false) do
1969 limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
1970 limit = Pleroma.Config.get([:instance, limit_name], 0)
1973 |> validate_length(:fields, max: limit)
1974 |> validate_change(:fields, fn :fields, fields ->
1975 if Enum.all?(fields, &valid_field?/1) do
1983 defp valid_field?(%{"name" => name, "value" => value}) do
1984 name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255)
1985 value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255)
1987 is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
1988 String.length(value) <= value_limit
1991 defp valid_field?(_), do: false
1993 defp truncate_field(%{"name" => name, "value" => value}) do
1995 String.split_at(name, Pleroma.Config.get([:instance, :account_field_name_length], 255))
1998 String.split_at(value, Pleroma.Config.get([:instance, :account_field_value_length], 255))
2000 %{"name" => name, "value" => value}
2003 def admin_api_update(user, params) do
2010 |> update_and_set_cache()
2013 @doc "Signs user out of all applications"
2014 def global_sign_out(user) do
2015 OAuth.Authorization.delete_user_authorizations(user)
2016 OAuth.Token.delete_user_tokens(user)
2019 def mascot_update(user, url) do
2021 |> cast(%{mascot: url}, [:mascot])
2022 |> validate_required([:mascot])
2023 |> update_and_set_cache()
2026 def mastodon_settings_update(user, settings) do
2028 |> cast(%{settings: settings}, [:settings])
2029 |> validate_required([:settings])
2030 |> update_and_set_cache()
2033 @spec confirmation_changeset(User.t(), keyword()) :: Changeset.t()
2034 def confirmation_changeset(user, need_confirmation: need_confirmation?) do
2036 if need_confirmation? do
2038 confirmation_pending: true,
2039 confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
2043 confirmation_pending: false,
2044 confirmation_token: nil
2048 cast(user, params, [:confirmation_pending, :confirmation_token])
2051 def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do
2052 if id not in user.pinned_activities do
2053 max_pinned_statuses = Pleroma.Config.get([:instance, :max_pinned_statuses], 0)
2054 params = %{pinned_activities: user.pinned_activities ++ [id]}
2057 |> cast(params, [:pinned_activities])
2058 |> validate_length(:pinned_activities,
2059 max: max_pinned_statuses,
2060 message: "You have already pinned the maximum number of statuses"
2065 |> update_and_set_cache()
2068 def remove_pinnned_activity(user, %Pleroma.Activity{id: id}) do
2069 params = %{pinned_activities: List.delete(user.pinned_activities, id)}
2072 |> cast(params, [:pinned_activities])
2073 |> update_and_set_cache()
2076 def update_email_notifications(user, settings) do
2077 email_notifications =
2078 user.email_notifications
2079 |> Map.merge(settings)
2080 |> Map.take(["digest"])
2082 params = %{email_notifications: email_notifications}
2083 fields = [:email_notifications]
2086 |> cast(params, fields)
2087 |> validate_required(fields)
2088 |> update_and_set_cache()
2091 defp set_domain_blocks(user, domain_blocks) do
2092 params = %{domain_blocks: domain_blocks}
2095 |> cast(params, [:domain_blocks])
2096 |> validate_required([:domain_blocks])
2097 |> update_and_set_cache()
2100 def block_domain(user, domain_blocked) do
2101 set_domain_blocks(user, Enum.uniq([domain_blocked | user.domain_blocks]))
2104 def unblock_domain(user, domain_blocked) do
2105 set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked))
2108 @spec add_to_block(User.t(), User.t()) ::
2109 {:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()}
2110 defp add_to_block(%User{} = user, %User{} = blocked) do
2111 UserRelationship.create_block(user, blocked)
2114 @spec add_to_block(User.t(), User.t()) ::
2115 {:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()}
2116 defp remove_from_block(%User{} = user, %User{} = blocked) do
2117 UserRelationship.delete_block(user, blocked)
2120 defp add_to_mutes(%User{} = user, %User{} = muted_user, notifications?) do
2121 with {:ok, user_mute} <- UserRelationship.create_mute(user, muted_user),
2122 {:ok, user_notification_mute} <-
2123 (notifications? && UserRelationship.create_notification_mute(user, muted_user)) ||
2125 {:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
2129 defp remove_from_mutes(user, %User{} = muted_user) do
2130 with {:ok, user_mute} <- UserRelationship.delete_mute(user, muted_user),
2131 {:ok, user_notification_mute} <-
2132 UserRelationship.delete_notification_mute(user, muted_user) do
2133 {:ok, [user_mute, user_notification_mute]}
2137 def set_invisible(user, invisible) do
2138 params = %{invisible: invisible}
2141 |> cast(params, [:invisible])
2142 |> validate_required([:invisible])
2143 |> update_and_set_cache()
2146 def sanitize_html(%User{} = user) do
2147 sanitize_html(user, nil)
2150 # User data that mastodon isn't filtering (treated as plaintext):
2153 def sanitize_html(%User{} = user, filter) do
2157 |> Enum.map(fn %{"name" => name, "value" => value} ->
2160 "value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
2165 |> Map.put(:bio, HTML.filter_tags(user.bio, filter))
2166 |> Map.put(:fields, fields)