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
19 alias Pleroma.FollowingRelationship
20 alias Pleroma.Formatter
23 alias Pleroma.Notification
25 alias Pleroma.Registration
27 alias Pleroma.RepoStreamer
29 alias Pleroma.UserRelationship
31 alias Pleroma.Web.ActivityPub.ActivityPub
32 alias Pleroma.Web.ActivityPub.ObjectValidators.Types
33 alias Pleroma.Web.ActivityPub.Utils
34 alias Pleroma.Web.CommonAPI
35 alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
36 alias Pleroma.Web.OAuth
37 alias Pleroma.Web.RelMe
38 alias Pleroma.Workers.BackgroundWorker
42 @type t :: %__MODULE__{}
43 @type account_status :: :active | :deactivated | :password_reset_pending | :confirmation_pending
44 @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
46 # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
47 @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])?)*$/
49 @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
50 @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
52 # AP ID user relationships (blocks, mutes etc.)
53 # Format: [rel_type: [outgoing_rel: :outgoing_rel_target, incoming_rel: :incoming_rel_source]]
54 @user_relationships_config [
56 blocker_blocks: :blocked_users,
57 blockee_blocks: :blocker_users
60 muter_mutes: :muted_users,
61 mutee_mutes: :muter_users
64 reblog_muter_mutes: :reblog_muted_users,
65 reblog_mutee_mutes: :reblog_muter_users
68 notification_muter_mutes: :notification_muted_users,
69 notification_mutee_mutes: :notification_muter_users
71 # Note: `inverse_subscription` relationship is inverse: subscriber acts as relationship target
72 inverse_subscription: [
73 subscribee_subscriptions: :subscriber_users,
74 subscriber_subscriptions: :subscribee_users
80 field(:email, :string)
82 field(:nickname, :string)
83 field(:password_hash, :string)
84 field(:password, :string, virtual: true)
85 field(:password_confirmation, :string, virtual: true)
87 field(:public_key, :string)
88 field(:ap_id, :string)
90 field(:local, :boolean, default: true)
91 field(:follower_address, :string)
92 field(:following_address, :string)
93 field(:search_rank, :float, virtual: true)
94 field(:search_type, :integer, virtual: true)
95 field(:tags, {:array, :string}, default: [])
96 field(:last_refreshed_at, :naive_datetime_usec)
97 field(:last_digest_emailed_at, :naive_datetime)
98 field(:banner, :map, default: %{})
99 field(:background, :map, default: %{})
100 field(:source_data, :map, default: %{})
101 field(:note_count, :integer, default: 0)
102 field(:follower_count, :integer, default: 0)
103 field(:following_count, :integer, default: 0)
104 field(:locked, :boolean, default: false)
105 field(:confirmation_pending, :boolean, default: false)
106 field(:password_reset_pending, :boolean, default: false)
107 field(:confirmation_token, :string, default: nil)
108 field(:default_scope, :string, default: "public")
109 field(:domain_blocks, {:array, :string}, default: [])
110 field(:deactivated, :boolean, default: false)
111 field(:no_rich_text, :boolean, default: false)
112 field(:ap_enabled, :boolean, default: false)
113 field(:is_moderator, :boolean, default: false)
114 field(:is_admin, :boolean, default: false)
115 field(:show_role, :boolean, default: true)
116 field(:settings, :map, default: nil)
117 field(:magic_key, :string, default: nil)
118 field(:uri, Types.Uri, default: nil)
119 field(:hide_followers_count, :boolean, default: false)
120 field(:hide_follows_count, :boolean, default: false)
121 field(:hide_followers, :boolean, default: false)
122 field(:hide_follows, :boolean, default: false)
123 field(:hide_favorites, :boolean, default: true)
124 field(:unread_conversation_count, :integer, default: 0)
125 field(:pinned_activities, {:array, :string}, default: [])
126 field(:email_notifications, :map, default: %{"digest" => false})
127 field(:mascot, :map, default: nil)
128 field(:emoji, :map, default: %{})
129 field(:pleroma_settings_store, :map, default: %{})
130 field(:fields, {:array, :map}, default: [])
131 field(:raw_fields, {:array, :map}, default: [])
132 field(:discoverable, :boolean, default: false)
133 field(:invisible, :boolean, default: false)
134 field(:allow_following_move, :boolean, default: true)
135 field(:skip_thread_containment, :boolean, default: false)
136 field(:actor_type, :string, default: "Person")
137 field(:also_known_as, {:array, :string}, default: [])
138 field(:inbox, :string)
139 field(:shared_inbox, :string)
142 :notification_settings,
143 Pleroma.User.NotificationSetting,
147 has_many(:notifications, Notification)
148 has_many(:registrations, Registration)
149 has_many(:deliveries, Delivery)
151 has_many(:outgoing_relationships, UserRelationship, foreign_key: :source_id)
152 has_many(:incoming_relationships, UserRelationship, foreign_key: :target_id)
154 for {relationship_type,
156 {outgoing_relation, outgoing_relation_target},
157 {incoming_relation, incoming_relation_source}
158 ]} <- @user_relationships_config do
159 # Definitions of `has_many` relations: :blocker_blocks, :muter_mutes, :reblog_muter_mutes,
160 # :notification_muter_mutes, :subscribee_subscriptions
161 has_many(outgoing_relation, UserRelationship,
162 foreign_key: :source_id,
163 where: [relationship_type: relationship_type]
166 # Definitions of `has_many` relations: :blockee_blocks, :mutee_mutes, :reblog_mutee_mutes,
167 # :notification_mutee_mutes, :subscriber_subscriptions
168 has_many(incoming_relation, UserRelationship,
169 foreign_key: :target_id,
170 where: [relationship_type: relationship_type]
173 # Definitions of `has_many` relations: :blocked_users, :muted_users, :reblog_muted_users,
174 # :notification_muted_users, :subscriber_users
175 has_many(outgoing_relation_target, through: [outgoing_relation, :target])
177 # Definitions of `has_many` relations: :blocker_users, :muter_users, :reblog_muter_users,
178 # :notification_muter_users, :subscribee_users
179 has_many(incoming_relation_source, through: [incoming_relation, :source])
182 # `:blocks` is deprecated (replaced with `blocked_users` relation)
183 field(:blocks, {:array, :string}, default: [])
184 # `:mutes` is deprecated (replaced with `muted_users` relation)
185 field(:mutes, {:array, :string}, default: [])
186 # `:muted_reblogs` is deprecated (replaced with `reblog_muted_users` relation)
187 field(:muted_reblogs, {:array, :string}, default: [])
188 # `:muted_notifications` is deprecated (replaced with `notification_muted_users` relation)
189 field(:muted_notifications, {:array, :string}, default: [])
190 # `:subscribers` is deprecated (replaced with `subscriber_users` relation)
191 field(:subscribers, {:array, :string}, default: [])
196 for {_relationship_type, [{_outgoing_relation, outgoing_relation_target}, _]} <-
197 @user_relationships_config do
198 # `def blocked_users_relation/2`, `def muted_users_relation/2`,
199 # `def reblog_muted_users_relation/2`, `def notification_muted_users/2`,
200 # `def subscriber_users/2`
201 def unquote(:"#{outgoing_relation_target}_relation")(user, restrict_deactivated? \\ false) do
202 target_users_query = assoc(user, unquote(outgoing_relation_target))
204 if restrict_deactivated? do
205 restrict_deactivated(target_users_query)
211 # `def blocked_users/2`, `def muted_users/2`, `def reblog_muted_users/2`,
212 # `def notification_muted_users/2`, `def subscriber_users/2`
213 def unquote(outgoing_relation_target)(user, restrict_deactivated? \\ false) do
215 |> apply(unquote(:"#{outgoing_relation_target}_relation"), [
217 restrict_deactivated?
222 # `def blocked_users_ap_ids/2`, `def muted_users_ap_ids/2`, `def reblog_muted_users_ap_ids/2`,
223 # `def notification_muted_users_ap_ids/2`, `def subscriber_users_ap_ids/2`
224 def unquote(:"#{outgoing_relation_target}_ap_ids")(user, restrict_deactivated? \\ false) do
226 |> apply(unquote(:"#{outgoing_relation_target}_relation"), [
228 restrict_deactivated?
230 |> select([u], u.ap_id)
236 Dumps Flake Id to SQL-compatible format (16-byte UUID).
237 E.g. "9pQtDGXuq4p3VlcJEm" -> <<0, 0, 1, 110, 179, 218, 42, 92, 213, 41, 44, 227, 95, 213, 0, 0>>
239 def binary_id(source_id) when is_binary(source_id) do
240 with {:ok, dumped_id} <- FlakeId.Ecto.CompatType.dump(source_id) do
247 def binary_id(source_ids) when is_list(source_ids) do
248 Enum.map(source_ids, &binary_id/1)
251 def binary_id(%User{} = user), do: binary_id(user.id)
253 @doc "Returns status account"
254 @spec account_status(User.t()) :: account_status()
255 def account_status(%User{deactivated: true}), do: :deactivated
256 def account_status(%User{password_reset_pending: true}), do: :password_reset_pending
258 def account_status(%User{confirmation_pending: true}) do
259 case Config.get([:instance, :account_activation_required]) do
260 true -> :confirmation_pending
265 def account_status(%User{}), do: :active
267 @spec visible_for?(User.t(), User.t() | nil) :: boolean()
268 def visible_for?(user, for_user \\ nil)
270 def visible_for?(%User{invisible: true}, _), do: false
272 def visible_for?(%User{id: user_id}, %User{id: user_id}), do: true
274 def visible_for?(%User{local: local} = user, nil) do
280 if Config.get([:restrict_unauthenticated, :profiles, cfg_key]),
282 else: account_status(user) == :active
285 def visible_for?(%User{} = user, for_user) do
286 account_status(user) == :active || superuser?(for_user)
289 def visible_for?(_, _), do: false
291 @spec superuser?(User.t()) :: boolean()
292 def superuser?(%User{local: true, is_admin: true}), do: true
293 def superuser?(%User{local: true, is_moderator: true}), do: true
294 def superuser?(_), do: false
296 @spec invisible?(User.t()) :: boolean()
297 def invisible?(%User{invisible: true}), do: true
298 def invisible?(_), do: false
300 def avatar_url(user, options \\ []) do
302 %{"url" => [%{"href" => href} | _]} -> href
303 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
307 def banner_url(user, options \\ []) do
309 %{"url" => [%{"href" => href} | _]} -> href
310 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
314 # Should probably be renamed or removed
315 def ap_id(%User{nickname: nickname}), do: "#{Web.base_url()}/users/#{nickname}"
317 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
318 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
320 @spec ap_following(User.t()) :: String.t()
321 def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
322 def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
324 @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
325 def restrict_deactivated(query) do
326 from(u in query, where: u.deactivated != ^true)
329 defdelegate following_count(user), to: FollowingRelationship
331 defp truncate_fields_param(params) do
332 if Map.has_key?(params, :fields) do
333 Map.put(params, :fields, Enum.map(params[:fields], &truncate_field/1))
339 defp truncate_if_exists(params, key, max_length) do
340 if Map.has_key?(params, key) and is_binary(params[key]) do
341 {value, _chopped} = String.split_at(params[key], max_length)
342 Map.put(params, key, value)
348 defp fix_follower_address(%{follower_address: _, following_address: _} = params), do: params
350 defp fix_follower_address(%{nickname: nickname} = params),
351 do: Map.put(params, :follower_address, ap_followers(%User{nickname: nickname}))
353 defp fix_follower_address(params), do: params
355 def remote_user_creation(params) do
356 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
357 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
361 |> truncate_if_exists(:name, name_limit)
362 |> truncate_if_exists(:bio, bio_limit)
363 |> truncate_fields_param()
364 |> fix_follower_address()
389 :hide_followers_count,
400 |> validate_required([:name, :ap_id])
401 |> unique_constraint(:nickname)
402 |> validate_format(:nickname, @email_regex)
403 |> validate_length(:bio, max: bio_limit)
404 |> validate_length(:name, max: name_limit)
405 |> validate_fields(true)
408 def update_changeset(struct, params \\ %{}) do
409 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
410 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
429 :hide_followers_count,
432 :allow_following_move,
435 :skip_thread_containment,
438 :pleroma_settings_store,
444 |> unique_constraint(:nickname)
445 |> validate_format(:nickname, local_nickname_regex())
446 |> validate_length(:bio, max: bio_limit)
447 |> validate_length(:name, min: 1, max: name_limit)
450 |> put_change_if_present(:bio, &{:ok, parse_bio(&1, struct)})
451 |> put_change_if_present(:avatar, &put_upload(&1, :avatar))
452 |> put_change_if_present(:banner, &put_upload(&1, :banner))
453 |> put_change_if_present(:background, &put_upload(&1, :background))
454 |> put_change_if_present(
455 :pleroma_settings_store,
456 &{:ok, Map.merge(struct.pleroma_settings_store, &1)}
458 |> validate_fields(false)
461 defp put_fields(changeset) do
462 if raw_fields = get_change(changeset, :raw_fields) do
465 |> Enum.filter(fn %{"name" => n} -> n != "" end)
469 |> Enum.map(fn f -> Map.update!(f, "value", &parse_fields(&1)) end)
472 |> put_change(:raw_fields, raw_fields)
473 |> put_change(:fields, fields)
479 defp parse_fields(value) do
481 |> Formatter.linkify(mentions_format: :full)
485 defp put_emoji(changeset) do
486 bio = get_change(changeset, :bio)
487 name = get_change(changeset, :name)
490 emoji = Map.merge(Emoji.Formatter.get_emoji_map(bio), Emoji.Formatter.get_emoji_map(name))
491 put_change(changeset, :emoji, emoji)
497 defp put_change_if_present(changeset, map_field, value_function) do
498 if value = get_change(changeset, map_field) do
499 with {:ok, new_value} <- value_function.(value) do
500 put_change(changeset, map_field, new_value)
509 defp put_upload(value, type) do
510 with %Plug.Upload{} <- value,
511 {:ok, object} <- ActivityPub.upload(value, type: type) do
516 def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
517 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
518 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
520 params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
522 params = if remote?, do: truncate_fields_param(params), else: params
548 :allow_following_move,
550 :hide_followers_count,
556 |> unique_constraint(:nickname)
557 |> validate_format(:nickname, local_nickname_regex())
558 |> validate_length(:bio, max: bio_limit)
559 |> validate_length(:name, max: name_limit)
560 |> validate_fields(remote?)
563 def update_as_admin_changeset(struct, params) do
565 |> update_changeset(params)
566 |> cast(params, [:email])
567 |> delete_change(:also_known_as)
568 |> unique_constraint(:email)
569 |> validate_format(:email, @email_regex)
572 @spec update_as_admin(%User{}, map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
573 def update_as_admin(user, params) do
574 params = Map.put(params, "password_confirmation", params["password"])
575 changeset = update_as_admin_changeset(user, params)
577 if params["password"] do
578 reset_password(user, changeset, params)
580 User.update_and_set_cache(changeset)
584 def password_update_changeset(struct, params) do
586 |> cast(params, [:password, :password_confirmation])
587 |> validate_required([:password, :password_confirmation])
588 |> validate_confirmation(:password)
589 |> put_password_hash()
590 |> put_change(:password_reset_pending, false)
593 @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
594 def reset_password(%User{} = user, params) do
595 reset_password(user, user, params)
598 def reset_password(%User{id: user_id} = user, struct, params) do
601 |> Multi.update(:user, password_update_changeset(struct, params))
602 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
603 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
605 case Repo.transaction(multi) do
606 {:ok, %{user: user} = _} -> set_cache(user)
607 {:error, _, changeset, _} -> {:error, changeset}
611 def update_password_reset_pending(user, value) do
614 |> put_change(:password_reset_pending, value)
615 |> update_and_set_cache()
618 def force_password_reset_async(user) do
619 BackgroundWorker.enqueue("force_password_reset", %{"user_id" => user.id})
622 @spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
623 def force_password_reset(user), do: update_password_reset_pending(user, true)
625 def register_changeset(struct, params \\ %{}, opts \\ []) do
626 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
627 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
630 if is_nil(opts[:need_confirmation]) do
631 Pleroma.Config.get([:instance, :account_activation_required])
633 opts[:need_confirmation]
637 |> confirmation_changeset(need_confirmation: need_confirmation?)
638 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation, :emoji])
639 |> validate_required([:name, :nickname, :password, :password_confirmation])
640 |> validate_confirmation(:password)
641 |> unique_constraint(:email)
642 |> unique_constraint(:nickname)
643 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
644 |> validate_format(:nickname, local_nickname_regex())
645 |> validate_format(:email, @email_regex)
646 |> validate_length(:bio, max: bio_limit)
647 |> validate_length(:name, min: 1, max: name_limit)
648 |> maybe_validate_required_email(opts[:external])
651 |> unique_constraint(:ap_id)
652 |> put_following_and_follower_address()
655 def maybe_validate_required_email(changeset, true), do: changeset
657 def maybe_validate_required_email(changeset, _) do
658 if Pleroma.Config.get([:instance, :account_activation_required]) do
659 validate_required(changeset, [:email])
665 defp put_ap_id(changeset) do
666 ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
667 put_change(changeset, :ap_id, ap_id)
670 defp put_following_and_follower_address(changeset) do
671 followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
674 |> put_change(:follower_address, followers)
677 defp autofollow_users(user) do
678 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
681 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
684 follow_all(user, autofollowed_users)
687 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
688 def register(%Ecto.Changeset{} = changeset) do
689 with {:ok, user} <- Repo.insert(changeset) do
690 post_register_action(user)
694 def post_register_action(%User{} = user) do
695 with {:ok, user} <- autofollow_users(user),
696 {:ok, user} <- set_cache(user),
697 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
698 {:ok, _} <- try_send_confirmation_email(user) do
703 def try_send_confirmation_email(%User{} = user) do
704 if user.confirmation_pending &&
705 Pleroma.Config.get([:instance, :account_activation_required]) do
707 |> Pleroma.Emails.UserEmail.account_confirmation_email()
708 |> Pleroma.Emails.Mailer.deliver_async()
716 def try_send_confirmation_email(users) do
717 Enum.each(users, &try_send_confirmation_email/1)
720 def needs_update?(%User{local: true}), do: false
722 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
724 def needs_update?(%User{local: false} = user) do
725 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
728 def needs_update?(_), do: true
730 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
731 def maybe_direct_follow(%User{} = follower, %User{local: true, locked: true} = followed) do
732 follow(follower, followed, "pending")
735 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
736 follow(follower, followed)
739 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
740 if not ap_enabled?(followed) do
741 follow(follower, followed)
747 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
748 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
749 def follow_all(follower, followeds) do
751 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
752 |> Enum.each(&follow(follower, &1, "accept"))
757 defdelegate following(user), to: FollowingRelationship
759 def follow(%User{} = follower, %User{} = followed, state \\ "accept") do
760 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
763 followed.deactivated ->
764 {:error, "Could not follow user: #{followed.nickname} is deactivated."}
766 deny_follow_blocked and blocks?(followed, follower) ->
767 {:error, "Could not follow user: #{followed.nickname} blocked you."}
770 FollowingRelationship.follow(follower, followed, state)
772 {:ok, _} = update_follower_count(followed)
775 |> update_following_count()
780 def unfollow(%User{ap_id: ap_id}, %User{ap_id: ap_id}) do
781 {:error, "Not subscribed!"}
784 def unfollow(%User{} = follower, %User{} = followed) do
785 case get_follow_state(follower, followed) do
786 state when state in ["accept", "pending"] ->
787 FollowingRelationship.unfollow(follower, followed)
788 {:ok, followed} = update_follower_count(followed)
792 |> update_following_count()
795 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
798 {:error, "Not subscribed!"}
802 defdelegate following?(follower, followed), to: FollowingRelationship
804 def get_follow_state(%User{} = follower, %User{} = following) do
805 following_relationship = FollowingRelationship.get(follower, following)
806 get_follow_state(follower, following, following_relationship)
809 def get_follow_state(
812 following_relationship
814 case {following_relationship, following.local} do
816 case Utils.fetch_latest_follow(follower, following) do
817 %{data: %{"state" => state}} when state in ["pending", "accept"] -> state
821 {%{state: state}, _} ->
829 def locked?(%User{} = user) do
834 Repo.get_by(User, id: id)
837 def get_by_ap_id(ap_id) do
838 Repo.get_by(User, ap_id: ap_id)
841 def get_all_by_ap_id(ap_ids) do
842 from(u in __MODULE__,
843 where: u.ap_id in ^ap_ids
848 def get_all_by_ids(ids) do
849 from(u in __MODULE__, where: u.id in ^ids)
853 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
854 # of the ap_id and the domain and tries to get that user
855 def get_by_guessed_nickname(ap_id) do
856 domain = URI.parse(ap_id).host
857 name = List.last(String.split(ap_id, "/"))
858 nickname = "#{name}@#{domain}"
860 get_cached_by_nickname(nickname)
863 def set_cache({:ok, user}), do: set_cache(user)
864 def set_cache({:error, err}), do: {:error, err}
866 def set_cache(%User{} = user) do
867 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
868 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
872 def update_and_set_cache(struct, params) do
874 |> update_changeset(params)
875 |> update_and_set_cache()
878 def update_and_set_cache(changeset) do
879 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
884 def invalidate_cache(user) do
885 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
886 Cachex.del(:user_cache, "nickname:#{user.nickname}")
889 @spec get_cached_by_ap_id(String.t()) :: User.t() | nil
890 def get_cached_by_ap_id(ap_id) do
891 key = "ap_id:#{ap_id}"
893 with {:ok, nil} <- Cachex.get(:user_cache, key),
894 user when not is_nil(user) <- get_by_ap_id(ap_id),
895 {:ok, true} <- Cachex.put(:user_cache, key, user) do
903 def get_cached_by_id(id) do
907 Cachex.fetch!(:user_cache, key, fn _ ->
911 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
912 {:commit, user.ap_id}
918 get_cached_by_ap_id(ap_id)
921 def get_cached_by_nickname(nickname) do
922 key = "nickname:#{nickname}"
924 Cachex.fetch!(:user_cache, key, fn ->
925 case get_or_fetch_by_nickname(nickname) do
926 {:ok, user} -> {:commit, user}
927 {:error, _error} -> {:ignore, nil}
932 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
933 restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
936 is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
937 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
939 restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
940 get_cached_by_nickname(nickname_or_id)
942 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
943 get_cached_by_nickname(nickname_or_id)
950 def get_by_nickname(nickname) do
951 Repo.get_by(User, nickname: nickname) ||
952 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
953 Repo.get_by(User, nickname: local_nickname(nickname))
957 def get_by_email(email), do: Repo.get_by(User, email: email)
959 def get_by_nickname_or_email(nickname_or_email) do
960 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
963 def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname)
965 def get_or_fetch_by_nickname(nickname) do
966 with %User{} = user <- get_by_nickname(nickname) do
970 with [_nick, _domain] <- String.split(nickname, "@"),
971 {:ok, user} <- fetch_by_nickname(nickname) do
974 _e -> {:error, "not found " <> nickname}
979 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
980 def get_followers_query(%User{} = user, nil) do
981 User.Query.build(%{followers: user, deactivated: false})
984 def get_followers_query(user, page) do
986 |> get_followers_query(nil)
987 |> User.Query.paginate(page, 20)
990 @spec get_followers_query(User.t()) :: Ecto.Query.t()
991 def get_followers_query(user), do: get_followers_query(user, nil)
993 @spec get_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
994 def get_followers(user, page \\ nil) do
996 |> get_followers_query(page)
1000 @spec get_external_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
1001 def get_external_followers(user, page \\ nil) do
1003 |> get_followers_query(page)
1004 |> User.Query.build(%{external: true})
1008 def get_followers_ids(user, page \\ nil) do
1010 |> get_followers_query(page)
1011 |> select([u], u.id)
1015 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
1016 def get_friends_query(%User{} = user, nil) do
1017 User.Query.build(%{friends: user, deactivated: false})
1020 def get_friends_query(user, page) do
1022 |> get_friends_query(nil)
1023 |> User.Query.paginate(page, 20)
1026 @spec get_friends_query(User.t()) :: Ecto.Query.t()
1027 def get_friends_query(user), do: get_friends_query(user, nil)
1029 def get_friends(user, page \\ nil) do
1031 |> get_friends_query(page)
1035 def get_friends_ap_ids(user) do
1037 |> get_friends_query(nil)
1038 |> select([u], u.ap_id)
1042 def get_friends_ids(user, page \\ nil) do
1044 |> get_friends_query(page)
1045 |> select([u], u.id)
1049 defdelegate get_follow_requests(user), to: FollowingRelationship
1051 def increase_note_count(%User{} = user) do
1053 |> where(id: ^user.id)
1054 |> update([u], inc: [note_count: 1])
1056 |> Repo.update_all([])
1058 {1, [user]} -> set_cache(user)
1063 def decrease_note_count(%User{} = user) do
1065 |> where(id: ^user.id)
1068 note_count: fragment("greatest(0, note_count - 1)")
1072 |> Repo.update_all([])
1074 {1, [user]} -> set_cache(user)
1079 def update_note_count(%User{} = user, note_count \\ nil) do
1084 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
1090 |> cast(%{note_count: note_count}, [:note_count])
1091 |> update_and_set_cache()
1094 @spec maybe_fetch_follow_information(User.t()) :: User.t()
1095 def maybe_fetch_follow_information(user) do
1096 with {:ok, user} <- fetch_follow_information(user) do
1100 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
1106 def fetch_follow_information(user) do
1107 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
1109 |> follow_information_changeset(info)
1110 |> update_and_set_cache()
1114 defp follow_information_changeset(user, params) do
1121 :hide_followers_count,
1126 def update_follower_count(%User{} = user) do
1127 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
1128 follower_count_query =
1129 User.Query.build(%{followers: user, deactivated: false})
1130 |> select([u], %{count: count(u.id)})
1133 |> where(id: ^user.id)
1134 |> join(:inner, [u], s in subquery(follower_count_query))
1136 set: [follower_count: s.count]
1139 |> Repo.update_all([])
1141 {1, [user]} -> set_cache(user)
1145 {:ok, maybe_fetch_follow_information(user)}
1149 @spec update_following_count(User.t()) :: User.t()
1150 def update_following_count(%User{local: false} = user) do
1151 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
1152 maybe_fetch_follow_information(user)
1158 def update_following_count(%User{local: true} = user) do
1159 following_count = FollowingRelationship.following_count(user)
1162 |> follow_information_changeset(%{following_count: following_count})
1166 def set_unread_conversation_count(%User{local: true} = user) do
1167 unread_query = Participation.unread_conversation_count_for_user(user)
1170 |> join(:inner, [u], p in subquery(unread_query))
1172 set: [unread_conversation_count: p.count]
1174 |> where([u], u.id == ^user.id)
1176 |> Repo.update_all([])
1178 {1, [user]} -> set_cache(user)
1183 def set_unread_conversation_count(user), do: {:ok, user}
1185 def increment_unread_conversation_count(conversation, %User{local: true} = user) do
1187 Participation.unread_conversation_count_for_user(user)
1188 |> where([p], p.conversation_id == ^conversation.id)
1191 |> join(:inner, [u], p in subquery(unread_query))
1193 inc: [unread_conversation_count: 1]
1195 |> where([u], u.id == ^user.id)
1196 |> where([u, p], p.count == 0)
1198 |> Repo.update_all([])
1200 {1, [user]} -> set_cache(user)
1205 def increment_unread_conversation_count(_, user), do: {:ok, user}
1207 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
1208 def get_users_from_set(ap_ids, local_only \\ true) do
1209 criteria = %{ap_id: ap_ids, deactivated: false}
1210 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
1212 User.Query.build(criteria)
1216 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
1217 def get_recipients_from_activity(%Activity{recipients: to}) do
1218 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
1222 @spec mute(User.t(), User.t(), boolean()) ::
1223 {:ok, list(UserRelationship.t())} | {:error, String.t()}
1224 def mute(%User{} = muter, %User{} = mutee, notifications? \\ true) do
1225 add_to_mutes(muter, mutee, notifications?)
1228 def unmute(%User{} = muter, %User{} = mutee) do
1229 remove_from_mutes(muter, mutee)
1232 def subscribe(%User{} = subscriber, %User{} = target) do
1233 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
1235 if blocks?(target, subscriber) and deny_follow_blocked do
1236 {:error, "Could not subscribe: #{target.nickname} is blocking you"}
1238 # Note: the relationship is inverse: subscriber acts as relationship target
1239 UserRelationship.create_inverse_subscription(target, subscriber)
1243 def subscribe(%User{} = subscriber, %{ap_id: ap_id}) do
1244 with %User{} = subscribee <- get_cached_by_ap_id(ap_id) do
1245 subscribe(subscriber, subscribee)
1249 def unsubscribe(%User{} = unsubscriber, %User{} = target) do
1250 # Note: the relationship is inverse: subscriber acts as relationship target
1251 UserRelationship.delete_inverse_subscription(target, unsubscriber)
1254 def unsubscribe(%User{} = unsubscriber, %{ap_id: ap_id}) do
1255 with %User{} = user <- get_cached_by_ap_id(ap_id) do
1256 unsubscribe(unsubscriber, user)
1260 def block(%User{} = blocker, %User{} = blocked) do
1261 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
1263 if following?(blocker, blocked) do
1264 {:ok, blocker, _} = unfollow(blocker, blocked)
1270 # clear any requested follows as well
1272 case CommonAPI.reject_follow_request(blocked, blocker) do
1273 {:ok, %User{} = updated_blocked} -> updated_blocked
1277 unsubscribe(blocked, blocker)
1279 if following?(blocked, blocker), do: unfollow(blocked, blocker)
1281 {:ok, blocker} = update_follower_count(blocker)
1282 {:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
1283 add_to_block(blocker, blocked)
1286 # helper to handle the block given only an actor's AP id
1287 def block(%User{} = blocker, %{ap_id: ap_id}) do
1288 block(blocker, get_cached_by_ap_id(ap_id))
1291 def unblock(%User{} = blocker, %User{} = blocked) do
1292 remove_from_block(blocker, blocked)
1295 # helper to handle the block given only an actor's AP id
1296 def unblock(%User{} = blocker, %{ap_id: ap_id}) do
1297 unblock(blocker, get_cached_by_ap_id(ap_id))
1300 def mutes?(nil, _), do: false
1301 def mutes?(%User{} = user, %User{} = target), do: mutes_user?(user, target)
1303 def mutes_user?(%User{} = user, %User{} = target) do
1304 UserRelationship.mute_exists?(user, target)
1307 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
1308 def muted_notifications?(nil, _), do: false
1310 def muted_notifications?(%User{} = user, %User{} = target),
1311 do: UserRelationship.notification_mute_exists?(user, target)
1313 def blocks?(nil, _), do: false
1315 def blocks?(%User{} = user, %User{} = target) do
1316 blocks_user?(user, target) ||
1317 (!User.following?(user, target) && blocks_domain?(user, target))
1320 def blocks_user?(%User{} = user, %User{} = target) do
1321 UserRelationship.block_exists?(user, target)
1324 def blocks_user?(_, _), do: false
1326 def blocks_domain?(%User{} = user, %User{} = target) do
1327 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
1328 %{host: host} = URI.parse(target.ap_id)
1329 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1332 def blocks_domain?(_, _), do: false
1334 def subscribed_to?(%User{} = user, %User{} = target) do
1335 # Note: the relationship is inverse: subscriber acts as relationship target
1336 UserRelationship.inverse_subscription_exists?(target, user)
1339 def subscribed_to?(%User{} = user, %{ap_id: ap_id}) do
1340 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1341 subscribed_to?(user, target)
1346 Returns map of outgoing (blocked, muted etc.) relationships' user AP IDs by relation type.
1347 E.g. `outgoing_relationships_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}`
1349 @spec outgoing_relationships_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())}
1350 def outgoing_relationships_ap_ids(_user, []), do: %{}
1352 def outgoing_relationships_ap_ids(nil, _relationship_types), do: %{}
1354 def outgoing_relationships_ap_ids(%User{} = user, relationship_types)
1355 when is_list(relationship_types) do
1358 |> assoc(:outgoing_relationships)
1359 |> join(:inner, [user_rel], u in assoc(user_rel, :target))
1360 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1361 |> select([user_rel, u], [user_rel.relationship_type, fragment("array_agg(?)", u.ap_id)])
1362 |> group_by([user_rel, u], user_rel.relationship_type)
1364 |> Enum.into(%{}, fn [k, v] -> {k, v} end)
1369 fn rel_type -> {rel_type, db_result[rel_type] || []} end
1373 def incoming_relationships_ungrouped_ap_ids(user, relationship_types, ap_ids \\ nil)
1375 def incoming_relationships_ungrouped_ap_ids(_user, [], _ap_ids), do: []
1377 def incoming_relationships_ungrouped_ap_ids(nil, _relationship_types, _ap_ids), do: []
1379 def incoming_relationships_ungrouped_ap_ids(%User{} = user, relationship_types, ap_ids)
1380 when is_list(relationship_types) do
1382 |> assoc(:incoming_relationships)
1383 |> join(:inner, [user_rel], u in assoc(user_rel, :source))
1384 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1385 |> maybe_filter_on_ap_id(ap_ids)
1386 |> select([user_rel, u], u.ap_id)
1391 defp maybe_filter_on_ap_id(query, ap_ids) when is_list(ap_ids) do
1392 where(query, [user_rel, u], u.ap_id in ^ap_ids)
1395 defp maybe_filter_on_ap_id(query, _ap_ids), do: query
1397 def deactivate_async(user, status \\ true) do
1398 BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
1401 def deactivate(user, status \\ true)
1403 def deactivate(users, status) when is_list(users) do
1404 Repo.transaction(fn ->
1405 for user <- users, do: deactivate(user, status)
1409 def deactivate(%User{} = user, status) do
1410 with {:ok, user} <- set_activation_status(user, status) do
1413 |> Enum.filter(& &1.local)
1414 |> Enum.each(fn follower ->
1415 follower |> update_following_count() |> set_cache()
1418 # Only update local user counts, remote will be update during the next pull.
1421 |> Enum.filter(& &1.local)
1422 |> Enum.each(&update_follower_count/1)
1428 def update_notification_settings(%User{} = user, settings) do
1430 |> cast(%{notification_settings: settings}, [])
1431 |> cast_embed(:notification_settings)
1432 |> validate_required([:notification_settings])
1433 |> update_and_set_cache()
1436 def delete(users) when is_list(users) do
1437 for user <- users, do: delete(user)
1440 def delete(%User{} = user) do
1441 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1444 def perform(:force_password_reset, user), do: force_password_reset(user)
1446 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1447 def perform(:delete, %User{} = user) do
1448 {:ok, _user} = ActivityPub.delete(user)
1450 # Remove all relationships
1453 |> Enum.each(fn follower ->
1454 ActivityPub.unfollow(follower, user)
1455 unfollow(follower, user)
1460 |> Enum.each(fn followed ->
1461 ActivityPub.unfollow(user, followed)
1462 unfollow(user, followed)
1465 delete_user_activities(user)
1466 invalidate_cache(user)
1470 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1472 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1473 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1474 when is_list(blocked_identifiers) do
1476 blocked_identifiers,
1477 fn blocked_identifier ->
1478 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1479 {:ok, _user_block} <- block(blocker, blocked),
1480 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1484 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1491 def perform(:follow_import, %User{} = follower, followed_identifiers)
1492 when is_list(followed_identifiers) do
1494 followed_identifiers,
1495 fn followed_identifier ->
1496 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1497 {:ok, follower} <- maybe_direct_follow(follower, followed),
1498 {:ok, _} <- ActivityPub.follow(follower, followed) do
1502 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1509 @spec external_users_query() :: Ecto.Query.t()
1510 def external_users_query do
1518 @spec external_users(keyword()) :: [User.t()]
1519 def external_users(opts \\ []) do
1521 external_users_query()
1522 |> select([u], struct(u, [:id, :ap_id]))
1526 do: where(query, [u], u.id > ^opts[:max_id]),
1531 do: limit(query, ^opts[:limit]),
1537 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1538 BackgroundWorker.enqueue("blocks_import", %{
1539 "blocker_id" => blocker.id,
1540 "blocked_identifiers" => blocked_identifiers
1544 def follow_import(%User{} = follower, followed_identifiers)
1545 when is_list(followed_identifiers) do
1546 BackgroundWorker.enqueue("follow_import", %{
1547 "follower_id" => follower.id,
1548 "followed_identifiers" => followed_identifiers
1552 def delete_user_activities(%User{ap_id: ap_id}) do
1554 |> Activity.Queries.by_actor()
1555 |> RepoStreamer.chunk_stream(50)
1556 |> Stream.each(fn activities -> Enum.each(activities, &delete_activity/1) end)
1560 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1562 |> Object.normalize()
1563 |> ActivityPub.delete()
1566 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1567 object = Object.normalize(activity)
1570 |> get_cached_by_ap_id()
1571 |> ActivityPub.unlike(object)
1574 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1575 object = Object.normalize(activity)
1578 |> get_cached_by_ap_id()
1579 |> ActivityPub.unannounce(object)
1582 defp delete_activity(_activity), do: "Doing nothing"
1584 def html_filter_policy(%User{no_rich_text: true}) do
1585 Pleroma.HTML.Scrubber.TwitterText
1588 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1590 def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
1592 def get_or_fetch_by_ap_id(ap_id) do
1593 user = get_cached_by_ap_id(ap_id)
1595 if !is_nil(user) and !needs_update?(user) do
1598 fetch_by_ap_id(ap_id)
1603 Creates an internal service actor by URI if missing.
1604 Optionally takes nickname for addressing.
1606 @spec get_or_create_service_actor_by_ap_id(String.t(), String.t()) :: User.t() | nil
1607 def get_or_create_service_actor_by_ap_id(uri, nickname) do
1609 case get_cached_by_ap_id(uri) do
1611 with {:error, %{errors: errors}} <- create_service_actor(uri, nickname) do
1612 Logger.error("Cannot create service actor: #{uri}/.\n#{inspect(errors)}")
1616 %User{invisible: false} = user ->
1626 @spec set_invisible(User.t()) :: {:ok, User.t()}
1627 defp set_invisible(user) do
1629 |> change(%{invisible: true})
1630 |> update_and_set_cache()
1633 @spec create_service_actor(String.t(), String.t()) ::
1634 {:ok, User.t()} | {:error, Ecto.Changeset.t()}
1635 defp create_service_actor(uri, nickname) do
1641 follower_address: uri <> "/followers"
1644 |> unique_constraint(:nickname)
1649 def public_key(%{public_key: public_key_pem}) when is_binary(public_key_pem) do
1652 |> :public_key.pem_decode()
1654 |> :public_key.pem_entry_decode()
1659 def public_key(_), do: {:error, "key not found"}
1661 def get_public_key_for_ap_id(ap_id) do
1662 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1663 {:ok, public_key} <- public_key(user) do
1670 defp blank?(""), do: nil
1671 defp blank?(n), do: n
1673 def insert_or_update_user(data) do
1675 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1676 |> remote_user_creation()
1677 |> Repo.insert(on_conflict: {:replace_all_except, [:id]}, conflict_target: :nickname)
1681 def ap_enabled?(%User{local: true}), do: true
1682 def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled
1683 def ap_enabled?(_), do: false
1685 @doc "Gets or fetch a user by uri or nickname."
1686 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1687 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1688 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1690 # wait a period of time and return newest version of the User structs
1691 # this is because we have synchronous follow APIs and need to simulate them
1692 # with an async handshake
1693 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1694 with %User{} = a <- get_cached_by_id(a.id),
1695 %User{} = b <- get_cached_by_id(b.id) do
1702 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1703 with :ok <- :timer.sleep(timeout),
1704 %User{} = a <- get_cached_by_id(a.id),
1705 %User{} = b <- get_cached_by_id(b.id) do
1712 def parse_bio(bio) when is_binary(bio) and bio != "" do
1714 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1718 def parse_bio(_), do: ""
1720 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1721 # TODO: get profile URLs other than user.ap_id
1722 profile_urls = [user.ap_id]
1725 |> CommonUtils.format_input("text/plain",
1726 mentions_format: :full,
1727 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1732 def parse_bio(_, _), do: ""
1734 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1735 Repo.transaction(fn ->
1736 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1740 def tag(nickname, tags) when is_binary(nickname),
1741 do: tag(get_by_nickname(nickname), tags)
1743 def tag(%User{} = user, tags),
1744 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1746 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1747 Repo.transaction(fn ->
1748 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1752 def untag(nickname, tags) when is_binary(nickname),
1753 do: untag(get_by_nickname(nickname), tags)
1755 def untag(%User{} = user, tags),
1756 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1758 defp update_tags(%User{} = user, new_tags) do
1759 {:ok, updated_user} =
1761 |> change(%{tags: new_tags})
1762 |> update_and_set_cache()
1767 defp normalize_tags(tags) do
1770 |> Enum.map(&String.downcase/1)
1773 defp local_nickname_regex do
1774 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1775 @extended_local_nickname_regex
1777 @strict_local_nickname_regex
1781 def local_nickname(nickname_or_mention) do
1784 |> String.split("@")
1788 def full_nickname(nickname_or_mention),
1789 do: String.trim_leading(nickname_or_mention, "@")
1791 def error_user(ap_id) do
1795 nickname: "erroruser@example.com",
1796 inserted_at: NaiveDateTime.utc_now()
1800 @spec all_superusers() :: [User.t()]
1801 def all_superusers do
1802 User.Query.build(%{super_users: true, local: true, deactivated: false})
1806 def muting_reblogs?(%User{} = user, %User{} = target) do
1807 UserRelationship.reblog_mute_exists?(user, target)
1810 def showing_reblogs?(%User{} = user, %User{} = target) do
1811 not muting_reblogs?(user, target)
1815 The function returns a query to get users with no activity for given interval of days.
1816 Inactive users are those who didn't read any notification, or had any activity where
1817 the user is the activity's actor, during `inactivity_threshold` days.
1818 Deactivated users will not appear in this list.
1822 iex> Pleroma.User.list_inactive_users()
1825 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1826 def list_inactive_users_query(inactivity_threshold \\ 7) do
1827 negative_inactivity_threshold = -inactivity_threshold
1828 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1829 # Subqueries are not supported in `where` clauses, join gets too complicated.
1830 has_read_notifications =
1831 from(n in Pleroma.Notification,
1832 where: n.seen == true,
1834 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1837 |> Pleroma.Repo.all()
1839 from(u in Pleroma.User,
1840 left_join: a in Pleroma.Activity,
1841 on: u.ap_id == a.actor,
1842 where: not is_nil(u.nickname),
1843 where: u.deactivated != ^true,
1844 where: u.id not in ^has_read_notifications,
1847 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1848 is_nil(max(a.inserted_at))
1853 Enable or disable email notifications for user
1857 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => false}}, "digest", true)
1858 Pleroma.User{email_notifications: %{"digest" => true}}
1860 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => true}}, "digest", false)
1861 Pleroma.User{email_notifications: %{"digest" => false}}
1863 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1864 {:ok, t()} | {:error, Ecto.Changeset.t()}
1865 def switch_email_notifications(user, type, status) do
1866 User.update_email_notifications(user, %{type => status})
1870 Set `last_digest_emailed_at` value for the user to current time
1872 @spec touch_last_digest_emailed_at(t()) :: t()
1873 def touch_last_digest_emailed_at(user) do
1874 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1876 {:ok, updated_user} =
1878 |> change(%{last_digest_emailed_at: now})
1879 |> update_and_set_cache()
1884 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1885 def toggle_confirmation(%User{} = user) do
1887 |> confirmation_changeset(need_confirmation: !user.confirmation_pending)
1888 |> update_and_set_cache()
1891 @spec toggle_confirmation([User.t()]) :: [{:ok, User.t()} | {:error, Changeset.t()}]
1892 def toggle_confirmation(users) do
1893 Enum.map(users, &toggle_confirmation/1)
1896 def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
1900 def get_mascot(%{mascot: mascot}) when is_nil(mascot) do
1901 # use instance-default
1902 config = Pleroma.Config.get([:assets, :mascots])
1903 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1904 mascot = Keyword.get(config, default_mascot)
1907 "id" => "default-mascot",
1908 "url" => mascot[:url],
1909 "preview_url" => mascot[:url],
1911 "mime_type" => mascot[:mime_type]
1916 def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
1918 def ensure_keys_present(%User{} = user) do
1919 with {:ok, pem} <- Keys.generate_rsa_pem() do
1921 |> cast(%{keys: pem}, [:keys])
1922 |> validate_required([:keys])
1923 |> update_and_set_cache()
1927 def get_ap_ids_by_nicknames(nicknames) do
1929 where: u.nickname in ^nicknames,
1935 defdelegate search(query, opts \\ []), to: User.Search
1937 defp put_password_hash(
1938 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1940 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1943 defp put_password_hash(changeset), do: changeset
1945 def is_internal_user?(%User{nickname: nil}), do: true
1946 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1947 def is_internal_user?(_), do: false
1949 # A hack because user delete activities have a fake id for whatever reason
1950 # TODO: Get rid of this
1951 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
1953 def get_delivered_users_by_object_id(object_id) do
1955 inner_join: delivery in assoc(u, :deliveries),
1956 where: delivery.object_id == ^object_id
1961 def change_email(user, email) do
1963 |> cast(%{email: email}, [:email])
1964 |> validate_required([:email])
1965 |> unique_constraint(:email)
1966 |> validate_format(:email, @email_regex)
1967 |> update_and_set_cache()
1970 # Internal function; public one is `deactivate/2`
1971 defp set_activation_status(user, deactivated) do
1973 |> cast(%{deactivated: deactivated}, [:deactivated])
1974 |> update_and_set_cache()
1977 def update_banner(user, banner) do
1979 |> cast(%{banner: banner}, [:banner])
1980 |> update_and_set_cache()
1983 def update_background(user, background) do
1985 |> cast(%{background: background}, [:background])
1986 |> update_and_set_cache()
1989 def roles(%{is_moderator: is_moderator, is_admin: is_admin}) do
1992 moderator: is_moderator
1996 # ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``.
1997 # For example: [{"name": "Pronoun", "value": "she/her"}, …]
1998 def fields(%{fields: nil, source_data: %{"attachment" => attachment}}) do
1999 limit = Pleroma.Config.get([:instance, :max_remote_account_fields], 0)
2002 |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
2003 |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
2007 def fields(%{fields: nil}), do: []
2009 def fields(%{fields: fields}), do: fields
2011 def validate_fields(changeset, remote? \\ false) do
2012 limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
2013 limit = Pleroma.Config.get([:instance, limit_name], 0)
2016 |> validate_length(:fields, max: limit)
2017 |> validate_change(:fields, fn :fields, fields ->
2018 if Enum.all?(fields, &valid_field?/1) do
2026 defp valid_field?(%{"name" => name, "value" => value}) do
2027 name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255)
2028 value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255)
2030 is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
2031 String.length(value) <= value_limit
2034 defp valid_field?(_), do: false
2036 defp truncate_field(%{"name" => name, "value" => value}) do
2038 String.split_at(name, Pleroma.Config.get([:instance, :account_field_name_length], 255))
2041 String.split_at(value, Pleroma.Config.get([:instance, :account_field_value_length], 255))
2043 %{"name" => name, "value" => value}
2046 def admin_api_update(user, params) do
2053 |> update_and_set_cache()
2056 @doc "Signs user out of all applications"
2057 def global_sign_out(user) do
2058 OAuth.Authorization.delete_user_authorizations(user)
2059 OAuth.Token.delete_user_tokens(user)
2062 def mascot_update(user, url) do
2064 |> cast(%{mascot: url}, [:mascot])
2065 |> validate_required([:mascot])
2066 |> update_and_set_cache()
2069 def mastodon_settings_update(user, settings) do
2071 |> cast(%{settings: settings}, [:settings])
2072 |> validate_required([:settings])
2073 |> update_and_set_cache()
2076 @spec confirmation_changeset(User.t(), keyword()) :: Changeset.t()
2077 def confirmation_changeset(user, need_confirmation: need_confirmation?) do
2079 if need_confirmation? do
2081 confirmation_pending: true,
2082 confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
2086 confirmation_pending: false,
2087 confirmation_token: nil
2091 cast(user, params, [:confirmation_pending, :confirmation_token])
2094 def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do
2095 if id not in user.pinned_activities do
2096 max_pinned_statuses = Pleroma.Config.get([:instance, :max_pinned_statuses], 0)
2097 params = %{pinned_activities: user.pinned_activities ++ [id]}
2100 |> cast(params, [:pinned_activities])
2101 |> validate_length(:pinned_activities,
2102 max: max_pinned_statuses,
2103 message: "You have already pinned the maximum number of statuses"
2108 |> update_and_set_cache()
2111 def remove_pinnned_activity(user, %Pleroma.Activity{id: id}) do
2112 params = %{pinned_activities: List.delete(user.pinned_activities, id)}
2115 |> cast(params, [:pinned_activities])
2116 |> update_and_set_cache()
2119 def update_email_notifications(user, settings) do
2120 email_notifications =
2121 user.email_notifications
2122 |> Map.merge(settings)
2123 |> Map.take(["digest"])
2125 params = %{email_notifications: email_notifications}
2126 fields = [:email_notifications]
2129 |> cast(params, fields)
2130 |> validate_required(fields)
2131 |> update_and_set_cache()
2134 defp set_domain_blocks(user, domain_blocks) do
2135 params = %{domain_blocks: domain_blocks}
2138 |> cast(params, [:domain_blocks])
2139 |> validate_required([:domain_blocks])
2140 |> update_and_set_cache()
2143 def block_domain(user, domain_blocked) do
2144 set_domain_blocks(user, Enum.uniq([domain_blocked | user.domain_blocks]))
2147 def unblock_domain(user, domain_blocked) do
2148 set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked))
2151 @spec add_to_block(User.t(), User.t()) ::
2152 {:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()}
2153 defp add_to_block(%User{} = user, %User{} = blocked) do
2154 UserRelationship.create_block(user, blocked)
2157 @spec add_to_block(User.t(), User.t()) ::
2158 {:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()}
2159 defp remove_from_block(%User{} = user, %User{} = blocked) do
2160 UserRelationship.delete_block(user, blocked)
2163 defp add_to_mutes(%User{} = user, %User{} = muted_user, notifications?) do
2164 with {:ok, user_mute} <- UserRelationship.create_mute(user, muted_user),
2165 {:ok, user_notification_mute} <-
2166 (notifications? && UserRelationship.create_notification_mute(user, muted_user)) ||
2168 {:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
2172 defp remove_from_mutes(user, %User{} = muted_user) do
2173 with {:ok, user_mute} <- UserRelationship.delete_mute(user, muted_user),
2174 {:ok, user_notification_mute} <-
2175 UserRelationship.delete_notification_mute(user, muted_user) do
2176 {:ok, [user_mute, user_notification_mute]}
2180 def set_invisible(user, invisible) do
2181 params = %{invisible: invisible}
2184 |> cast(params, [:invisible])
2185 |> validate_required([:invisible])
2186 |> update_and_set_cache()
2189 def sanitize_html(%User{} = user) do
2190 sanitize_html(user, nil)
2193 # User data that mastodon isn't filtering (treated as plaintext):
2196 def sanitize_html(%User{} = user, filter) do
2200 |> Enum.map(fn %{"name" => name, "value" => value} ->
2203 "value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
2208 |> Map.put(:bio, HTML.filter_tags(user.bio, filter))
2209 |> Map.put(:fields, fields)