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(:note_count, :integer, default: 0)
101 field(:follower_count, :integer, default: 0)
102 field(:following_count, :integer, default: 0)
103 field(:locked, :boolean, default: false)
104 field(:confirmation_pending, :boolean, default: false)
105 field(:password_reset_pending, :boolean, default: false)
106 field(:confirmation_token, :string, default: nil)
107 field(:default_scope, :string, default: "public")
108 field(:domain_blocks, {:array, :string}, default: [])
109 field(:deactivated, :boolean, default: false)
110 field(:no_rich_text, :boolean, default: false)
111 field(:ap_enabled, :boolean, default: false)
112 field(:is_moderator, :boolean, default: false)
113 field(:is_admin, :boolean, default: false)
114 field(:show_role, :boolean, default: true)
115 field(:settings, :map, default: nil)
116 field(:magic_key, :string, default: nil)
117 field(:uri, Types.Uri, default: nil)
118 field(:hide_followers_count, :boolean, default: false)
119 field(:hide_follows_count, :boolean, default: false)
120 field(:hide_followers, :boolean, default: false)
121 field(:hide_follows, :boolean, default: false)
122 field(:hide_favorites, :boolean, default: true)
123 field(:unread_conversation_count, :integer, default: 0)
124 field(:pinned_activities, {:array, :string}, default: [])
125 field(:email_notifications, :map, default: %{"digest" => false})
126 field(:mascot, :map, default: nil)
127 field(:emoji, :map, default: %{})
128 field(:pleroma_settings_store, :map, default: %{})
129 field(:fields, {:array, :map}, default: [])
130 field(:raw_fields, {:array, :map}, default: [])
131 field(:discoverable, :boolean, default: false)
132 field(:invisible, :boolean, default: false)
133 field(:allow_following_move, :boolean, default: true)
134 field(:skip_thread_containment, :boolean, default: false)
135 field(:actor_type, :string, default: "Person")
136 field(:also_known_as, {:array, :string}, default: [])
137 field(:inbox, :string)
138 field(:shared_inbox, :string)
141 :notification_settings,
142 Pleroma.User.NotificationSetting,
146 has_many(:notifications, Notification)
147 has_many(:registrations, Registration)
148 has_many(:deliveries, Delivery)
150 has_many(:outgoing_relationships, UserRelationship, foreign_key: :source_id)
151 has_many(:incoming_relationships, UserRelationship, foreign_key: :target_id)
153 for {relationship_type,
155 {outgoing_relation, outgoing_relation_target},
156 {incoming_relation, incoming_relation_source}
157 ]} <- @user_relationships_config do
158 # Definitions of `has_many` relations: :blocker_blocks, :muter_mutes, :reblog_muter_mutes,
159 # :notification_muter_mutes, :subscribee_subscriptions
160 has_many(outgoing_relation, UserRelationship,
161 foreign_key: :source_id,
162 where: [relationship_type: relationship_type]
165 # Definitions of `has_many` relations: :blockee_blocks, :mutee_mutes, :reblog_mutee_mutes,
166 # :notification_mutee_mutes, :subscriber_subscriptions
167 has_many(incoming_relation, UserRelationship,
168 foreign_key: :target_id,
169 where: [relationship_type: relationship_type]
172 # Definitions of `has_many` relations: :blocked_users, :muted_users, :reblog_muted_users,
173 # :notification_muted_users, :subscriber_users
174 has_many(outgoing_relation_target, through: [outgoing_relation, :target])
176 # Definitions of `has_many` relations: :blocker_users, :muter_users, :reblog_muter_users,
177 # :notification_muter_users, :subscribee_users
178 has_many(incoming_relation_source, through: [incoming_relation, :source])
181 # `:blocks` is deprecated (replaced with `blocked_users` relation)
182 field(:blocks, {:array, :string}, default: [])
183 # `:mutes` is deprecated (replaced with `muted_users` relation)
184 field(:mutes, {:array, :string}, default: [])
185 # `:muted_reblogs` is deprecated (replaced with `reblog_muted_users` relation)
186 field(:muted_reblogs, {:array, :string}, default: [])
187 # `:muted_notifications` is deprecated (replaced with `notification_muted_users` relation)
188 field(:muted_notifications, {:array, :string}, default: [])
189 # `:subscribers` is deprecated (replaced with `subscriber_users` relation)
190 field(:subscribers, {:array, :string}, default: [])
195 for {_relationship_type, [{_outgoing_relation, outgoing_relation_target}, _]} <-
196 @user_relationships_config do
197 # `def blocked_users_relation/2`, `def muted_users_relation/2`,
198 # `def reblog_muted_users_relation/2`, `def notification_muted_users/2`,
199 # `def subscriber_users/2`
200 def unquote(:"#{outgoing_relation_target}_relation")(user, restrict_deactivated? \\ false) do
201 target_users_query = assoc(user, unquote(outgoing_relation_target))
203 if restrict_deactivated? do
204 restrict_deactivated(target_users_query)
210 # `def blocked_users/2`, `def muted_users/2`, `def reblog_muted_users/2`,
211 # `def notification_muted_users/2`, `def subscriber_users/2`
212 def unquote(outgoing_relation_target)(user, restrict_deactivated? \\ false) do
214 |> apply(unquote(:"#{outgoing_relation_target}_relation"), [
216 restrict_deactivated?
221 # `def blocked_users_ap_ids/2`, `def muted_users_ap_ids/2`, `def reblog_muted_users_ap_ids/2`,
222 # `def notification_muted_users_ap_ids/2`, `def subscriber_users_ap_ids/2`
223 def unquote(:"#{outgoing_relation_target}_ap_ids")(user, restrict_deactivated? \\ false) do
225 |> apply(unquote(:"#{outgoing_relation_target}_relation"), [
227 restrict_deactivated?
229 |> select([u], u.ap_id)
235 Dumps Flake Id to SQL-compatible format (16-byte UUID).
236 E.g. "9pQtDGXuq4p3VlcJEm" -> <<0, 0, 1, 110, 179, 218, 42, 92, 213, 41, 44, 227, 95, 213, 0, 0>>
238 def binary_id(source_id) when is_binary(source_id) do
239 with {:ok, dumped_id} <- FlakeId.Ecto.CompatType.dump(source_id) do
246 def binary_id(source_ids) when is_list(source_ids) do
247 Enum.map(source_ids, &binary_id/1)
250 def binary_id(%User{} = user), do: binary_id(user.id)
252 @doc "Returns status account"
253 @spec account_status(User.t()) :: account_status()
254 def account_status(%User{deactivated: true}), do: :deactivated
255 def account_status(%User{password_reset_pending: true}), do: :password_reset_pending
257 def account_status(%User{confirmation_pending: true}) do
258 case Config.get([:instance, :account_activation_required]) do
259 true -> :confirmation_pending
264 def account_status(%User{}), do: :active
266 @spec visible_for?(User.t(), User.t() | nil) :: boolean()
267 def visible_for?(user, for_user \\ nil)
269 def visible_for?(%User{invisible: true}, _), do: false
271 def visible_for?(%User{id: user_id}, %User{id: user_id}), do: true
273 def visible_for?(%User{local: local} = user, nil) do
279 if Config.get([:restrict_unauthenticated, :profiles, cfg_key]),
281 else: account_status(user) == :active
284 def visible_for?(%User{} = user, for_user) do
285 account_status(user) == :active || superuser?(for_user)
288 def visible_for?(_, _), do: false
290 @spec superuser?(User.t()) :: boolean()
291 def superuser?(%User{local: true, is_admin: true}), do: true
292 def superuser?(%User{local: true, is_moderator: true}), do: true
293 def superuser?(_), do: false
295 @spec invisible?(User.t()) :: boolean()
296 def invisible?(%User{invisible: true}), do: true
297 def invisible?(_), do: false
299 def avatar_url(user, options \\ []) do
301 %{"url" => [%{"href" => href} | _]} -> href
302 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
306 def banner_url(user, options \\ []) do
308 %{"url" => [%{"href" => href} | _]} -> href
309 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
313 # Should probably be renamed or removed
314 def ap_id(%User{nickname: nickname}), do: "#{Web.base_url()}/users/#{nickname}"
316 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
317 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
319 @spec ap_following(User.t()) :: String.t()
320 def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
321 def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
323 @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
324 def restrict_deactivated(query) do
325 from(u in query, where: u.deactivated != ^true)
328 defdelegate following_count(user), to: FollowingRelationship
330 defp truncate_fields_param(params) do
331 if Map.has_key?(params, :fields) do
332 Map.put(params, :fields, Enum.map(params[:fields], &truncate_field/1))
338 defp truncate_if_exists(params, key, max_length) do
339 if Map.has_key?(params, key) and is_binary(params[key]) do
340 {value, _chopped} = String.split_at(params[key], max_length)
341 Map.put(params, key, value)
347 defp fix_follower_address(%{follower_address: _, following_address: _} = params), do: params
349 defp fix_follower_address(%{nickname: nickname} = params),
350 do: Map.put(params, :follower_address, ap_followers(%User{nickname: nickname}))
352 defp fix_follower_address(params), do: params
354 def remote_user_changeset(struct \\ %User{local: false}, params) do
355 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
356 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
359 case params[:name] do
360 name when is_binary(name) and byte_size(name) > 0 -> name
361 _ -> params[:nickname]
366 |> Map.put(:name, name)
367 |> Map.put_new(:last_refreshed_at, NaiveDateTime.utc_now())
368 |> truncate_if_exists(:name, name_limit)
369 |> truncate_if_exists(:bio, bio_limit)
370 |> truncate_fields_param()
371 |> fix_follower_address()
396 :hide_followers_count,
407 |> validate_required([:name, :ap_id])
408 |> unique_constraint(:nickname)
409 |> validate_format(:nickname, @email_regex)
410 |> validate_length(:bio, max: bio_limit)
411 |> validate_length(:name, max: name_limit)
412 |> validate_fields(true)
415 def update_changeset(struct, params \\ %{}) do
416 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
417 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
436 :hide_followers_count,
439 :allow_following_move,
442 :skip_thread_containment,
445 :pleroma_settings_store,
451 |> unique_constraint(:nickname)
452 |> validate_format(:nickname, local_nickname_regex())
453 |> validate_length(:bio, max: bio_limit)
454 |> validate_length(:name, min: 1, max: name_limit)
457 |> put_change_if_present(:bio, &{:ok, parse_bio(&1, struct)})
458 |> put_change_if_present(:avatar, &put_upload(&1, :avatar))
459 |> put_change_if_present(:banner, &put_upload(&1, :banner))
460 |> put_change_if_present(:background, &put_upload(&1, :background))
461 |> put_change_if_present(
462 :pleroma_settings_store,
463 &{:ok, Map.merge(struct.pleroma_settings_store, &1)}
465 |> validate_fields(false)
468 defp put_fields(changeset) do
469 if raw_fields = get_change(changeset, :raw_fields) do
472 |> Enum.filter(fn %{"name" => n} -> n != "" end)
476 |> Enum.map(fn f -> Map.update!(f, "value", &parse_fields(&1)) end)
479 |> put_change(:raw_fields, raw_fields)
480 |> put_change(:fields, fields)
486 defp parse_fields(value) do
488 |> Formatter.linkify(mentions_format: :full)
492 defp put_emoji(changeset) do
493 bio = get_change(changeset, :bio)
494 name = get_change(changeset, :name)
497 emoji = Map.merge(Emoji.Formatter.get_emoji_map(bio), Emoji.Formatter.get_emoji_map(name))
498 put_change(changeset, :emoji, emoji)
504 defp put_change_if_present(changeset, map_field, value_function) do
505 if value = get_change(changeset, map_field) do
506 with {:ok, new_value} <- value_function.(value) do
507 put_change(changeset, map_field, new_value)
516 defp put_upload(value, type) do
517 with %Plug.Upload{} <- value,
518 {:ok, object} <- ActivityPub.upload(value, type: type) do
523 def update_as_admin_changeset(struct, params) do
525 |> update_changeset(params)
526 |> cast(params, [:email])
527 |> delete_change(:also_known_as)
528 |> unique_constraint(:email)
529 |> validate_format(:email, @email_regex)
532 @spec update_as_admin(%User{}, map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
533 def update_as_admin(user, params) do
534 params = Map.put(params, "password_confirmation", params["password"])
535 changeset = update_as_admin_changeset(user, params)
537 if params["password"] do
538 reset_password(user, changeset, params)
540 User.update_and_set_cache(changeset)
544 def password_update_changeset(struct, params) do
546 |> cast(params, [:password, :password_confirmation])
547 |> validate_required([:password, :password_confirmation])
548 |> validate_confirmation(:password)
549 |> put_password_hash()
550 |> put_change(:password_reset_pending, false)
553 @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
554 def reset_password(%User{} = user, params) do
555 reset_password(user, user, params)
558 def reset_password(%User{id: user_id} = user, struct, params) do
561 |> Multi.update(:user, password_update_changeset(struct, params))
562 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
563 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
565 case Repo.transaction(multi) do
566 {:ok, %{user: user} = _} -> set_cache(user)
567 {:error, _, changeset, _} -> {:error, changeset}
571 def update_password_reset_pending(user, value) do
574 |> put_change(:password_reset_pending, value)
575 |> update_and_set_cache()
578 def force_password_reset_async(user) do
579 BackgroundWorker.enqueue("force_password_reset", %{"user_id" => user.id})
582 @spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
583 def force_password_reset(user), do: update_password_reset_pending(user, true)
585 def register_changeset(struct, params \\ %{}, opts \\ []) do
586 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
587 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
590 if is_nil(opts[:need_confirmation]) do
591 Pleroma.Config.get([:instance, :account_activation_required])
593 opts[:need_confirmation]
597 |> confirmation_changeset(need_confirmation: need_confirmation?)
598 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation, :emoji])
599 |> validate_required([:name, :nickname, :password, :password_confirmation])
600 |> validate_confirmation(:password)
601 |> unique_constraint(:email)
602 |> unique_constraint(:nickname)
603 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
604 |> validate_format(:nickname, local_nickname_regex())
605 |> validate_format(:email, @email_regex)
606 |> validate_length(:bio, max: bio_limit)
607 |> validate_length(:name, min: 1, max: name_limit)
608 |> maybe_validate_required_email(opts[:external])
611 |> unique_constraint(:ap_id)
612 |> put_following_and_follower_address()
615 def maybe_validate_required_email(changeset, true), do: changeset
617 def maybe_validate_required_email(changeset, _) do
618 if Pleroma.Config.get([:instance, :account_activation_required]) do
619 validate_required(changeset, [:email])
625 defp put_ap_id(changeset) do
626 ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
627 put_change(changeset, :ap_id, ap_id)
630 defp put_following_and_follower_address(changeset) do
631 followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
634 |> put_change(:follower_address, followers)
637 defp autofollow_users(user) do
638 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
641 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
644 follow_all(user, autofollowed_users)
647 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
648 def register(%Ecto.Changeset{} = changeset) do
649 with {:ok, user} <- Repo.insert(changeset) do
650 post_register_action(user)
654 def post_register_action(%User{} = user) do
655 with {:ok, user} <- autofollow_users(user),
656 {:ok, user} <- set_cache(user),
657 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
658 {:ok, _} <- try_send_confirmation_email(user) do
663 def try_send_confirmation_email(%User{} = user) do
664 if user.confirmation_pending &&
665 Pleroma.Config.get([:instance, :account_activation_required]) do
667 |> Pleroma.Emails.UserEmail.account_confirmation_email()
668 |> Pleroma.Emails.Mailer.deliver_async()
676 def try_send_confirmation_email(users) do
677 Enum.each(users, &try_send_confirmation_email/1)
680 def needs_update?(%User{local: true}), do: false
682 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
684 def needs_update?(%User{local: false} = user) do
685 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
688 def needs_update?(_), do: true
690 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
692 # "Locked" (self-locked) users demand explicit authorization of follow requests
693 def maybe_direct_follow(%User{} = follower, %User{local: true, locked: true} = followed) do
694 follow(follower, followed, :follow_pending)
697 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
698 follow(follower, followed)
701 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
702 if not ap_enabled?(followed) do
703 follow(follower, followed)
709 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
710 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
711 def follow_all(follower, followeds) do
713 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
714 |> Enum.each(&follow(follower, &1, :follow_accept))
719 defdelegate following(user), to: FollowingRelationship
721 def follow(%User{} = follower, %User{} = followed, state \\ :follow_accept) do
722 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
725 followed.deactivated ->
726 {:error, "Could not follow user: #{followed.nickname} is deactivated."}
728 deny_follow_blocked and blocks?(followed, follower) ->
729 {:error, "Could not follow user: #{followed.nickname} blocked you."}
732 FollowingRelationship.follow(follower, followed, state)
734 {:ok, _} = update_follower_count(followed)
737 |> update_following_count()
742 def unfollow(%User{ap_id: ap_id}, %User{ap_id: ap_id}) do
743 {:error, "Not subscribed!"}
746 def unfollow(%User{} = follower, %User{} = followed) do
747 case get_follow_state(follower, followed) do
748 state when state in [:follow_pending, :follow_accept] ->
749 FollowingRelationship.unfollow(follower, followed)
750 {:ok, followed} = update_follower_count(followed)
754 |> update_following_count()
757 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
760 {:error, "Not subscribed!"}
764 defdelegate following?(follower, followed), to: FollowingRelationship
766 @doc "Returns follow state as Pleroma.FollowingRelationship.State value"
767 def get_follow_state(%User{} = follower, %User{} = following) do
768 following_relationship = FollowingRelationship.get(follower, following)
769 get_follow_state(follower, following, following_relationship)
772 def get_follow_state(
775 following_relationship
777 case {following_relationship, following.local} do
779 case Utils.fetch_latest_follow(follower, following) do
780 %Activity{data: %{"state" => state}} when state in ["pending", "accept"] ->
781 FollowingRelationship.state_to_enum(state)
787 {%{state: state}, _} ->
795 def locked?(%User{} = user) do
800 Repo.get_by(User, id: id)
803 def get_by_ap_id(ap_id) do
804 Repo.get_by(User, ap_id: ap_id)
807 def get_all_by_ap_id(ap_ids) do
808 from(u in __MODULE__,
809 where: u.ap_id in ^ap_ids
814 def get_all_by_ids(ids) do
815 from(u in __MODULE__, where: u.id in ^ids)
819 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
820 # of the ap_id and the domain and tries to get that user
821 def get_by_guessed_nickname(ap_id) do
822 domain = URI.parse(ap_id).host
823 name = List.last(String.split(ap_id, "/"))
824 nickname = "#{name}@#{domain}"
826 get_cached_by_nickname(nickname)
829 def set_cache({:ok, user}), do: set_cache(user)
830 def set_cache({:error, err}), do: {:error, err}
832 def set_cache(%User{} = user) do
833 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
834 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
835 Cachex.put(:user_cache, "friends_ap_ids:#{user.nickname}", get_user_friends_ap_ids(user))
839 def update_and_set_cache(struct, params) do
841 |> update_changeset(params)
842 |> update_and_set_cache()
845 def update_and_set_cache(changeset) do
846 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
851 def get_user_friends_ap_ids(user) do
852 from(u in User.get_friends_query(user), select: u.ap_id)
856 @spec get_cached_user_friends_ap_ids(User.t()) :: [String.t()]
857 def get_cached_user_friends_ap_ids(user) do
858 Cachex.fetch!(:user_cache, "friends_ap_ids:#{user.ap_id}", fn _ ->
859 get_user_friends_ap_ids(user)
863 def invalidate_cache(user) do
864 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
865 Cachex.del(:user_cache, "nickname:#{user.nickname}")
866 Cachex.del(:user_cache, "friends_ap_ids:#{user.ap_id}")
869 @spec get_cached_by_ap_id(String.t()) :: User.t() | nil
870 def get_cached_by_ap_id(ap_id) do
871 key = "ap_id:#{ap_id}"
873 with {:ok, nil} <- Cachex.get(:user_cache, key),
874 user when not is_nil(user) <- get_by_ap_id(ap_id),
875 {:ok, true} <- Cachex.put(:user_cache, key, user) do
883 def get_cached_by_id(id) do
887 Cachex.fetch!(:user_cache, key, fn _ ->
891 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
892 {:commit, user.ap_id}
898 get_cached_by_ap_id(ap_id)
901 def get_cached_by_nickname(nickname) do
902 key = "nickname:#{nickname}"
904 Cachex.fetch!(:user_cache, key, fn ->
905 case get_or_fetch_by_nickname(nickname) do
906 {:ok, user} -> {:commit, user}
907 {:error, _error} -> {:ignore, nil}
912 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
913 restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
916 is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
917 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
919 restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
920 get_cached_by_nickname(nickname_or_id)
922 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
923 get_cached_by_nickname(nickname_or_id)
930 def get_by_nickname(nickname) do
931 Repo.get_by(User, nickname: nickname) ||
932 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
933 Repo.get_by(User, nickname: local_nickname(nickname))
937 def get_by_email(email), do: Repo.get_by(User, email: email)
939 def get_by_nickname_or_email(nickname_or_email) do
940 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
943 def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname)
945 def get_or_fetch_by_nickname(nickname) do
946 with %User{} = user <- get_by_nickname(nickname) do
950 with [_nick, _domain] <- String.split(nickname, "@"),
951 {:ok, user} <- fetch_by_nickname(nickname) do
954 _e -> {:error, "not found " <> nickname}
959 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
960 def get_followers_query(%User{} = user, nil) do
961 User.Query.build(%{followers: user, deactivated: false})
964 def get_followers_query(user, page) do
966 |> get_followers_query(nil)
967 |> User.Query.paginate(page, 20)
970 @spec get_followers_query(User.t()) :: Ecto.Query.t()
971 def get_followers_query(user), do: get_followers_query(user, nil)
973 @spec get_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
974 def get_followers(user, page \\ nil) do
976 |> get_followers_query(page)
980 @spec get_external_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
981 def get_external_followers(user, page \\ nil) do
983 |> get_followers_query(page)
984 |> User.Query.build(%{external: true})
988 def get_followers_ids(user, page \\ nil) do
990 |> get_followers_query(page)
995 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
996 def get_friends_query(%User{} = user, nil) do
997 User.Query.build(%{friends: user, deactivated: false})
1000 def get_friends_query(user, page) do
1002 |> get_friends_query(nil)
1003 |> User.Query.paginate(page, 20)
1006 @spec get_friends_query(User.t()) :: Ecto.Query.t()
1007 def get_friends_query(user), do: get_friends_query(user, nil)
1009 def get_friends(user, page \\ nil) do
1011 |> get_friends_query(page)
1015 def get_friends_ap_ids(user) do
1017 |> get_friends_query(nil)
1018 |> select([u], u.ap_id)
1022 def get_friends_ids(user, page \\ nil) do
1024 |> get_friends_query(page)
1025 |> select([u], u.id)
1029 defdelegate get_follow_requests(user), to: FollowingRelationship
1031 def increase_note_count(%User{} = user) do
1033 |> where(id: ^user.id)
1034 |> update([u], inc: [note_count: 1])
1036 |> Repo.update_all([])
1038 {1, [user]} -> set_cache(user)
1043 def decrease_note_count(%User{} = user) do
1045 |> where(id: ^user.id)
1048 note_count: fragment("greatest(0, note_count - 1)")
1052 |> Repo.update_all([])
1054 {1, [user]} -> set_cache(user)
1059 def update_note_count(%User{} = user, note_count \\ nil) do
1064 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
1070 |> cast(%{note_count: note_count}, [:note_count])
1071 |> update_and_set_cache()
1074 @spec maybe_fetch_follow_information(User.t()) :: User.t()
1075 def maybe_fetch_follow_information(user) do
1076 with {:ok, user} <- fetch_follow_information(user) do
1080 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
1086 def fetch_follow_information(user) do
1087 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
1089 |> follow_information_changeset(info)
1090 |> update_and_set_cache()
1094 defp follow_information_changeset(user, params) do
1101 :hide_followers_count,
1106 def update_follower_count(%User{} = user) do
1107 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
1108 follower_count_query =
1109 User.Query.build(%{followers: user, deactivated: false})
1110 |> select([u], %{count: count(u.id)})
1113 |> where(id: ^user.id)
1114 |> join(:inner, [u], s in subquery(follower_count_query))
1116 set: [follower_count: s.count]
1119 |> Repo.update_all([])
1121 {1, [user]} -> set_cache(user)
1125 {:ok, maybe_fetch_follow_information(user)}
1129 @spec update_following_count(User.t()) :: User.t()
1130 def update_following_count(%User{local: false} = user) do
1131 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
1132 maybe_fetch_follow_information(user)
1138 def update_following_count(%User{local: true} = user) do
1139 following_count = FollowingRelationship.following_count(user)
1142 |> follow_information_changeset(%{following_count: following_count})
1146 def set_unread_conversation_count(%User{local: true} = user) do
1147 unread_query = Participation.unread_conversation_count_for_user(user)
1150 |> join(:inner, [u], p in subquery(unread_query))
1152 set: [unread_conversation_count: p.count]
1154 |> where([u], u.id == ^user.id)
1156 |> Repo.update_all([])
1158 {1, [user]} -> set_cache(user)
1163 def set_unread_conversation_count(user), do: {:ok, user}
1165 def increment_unread_conversation_count(conversation, %User{local: true} = user) do
1167 Participation.unread_conversation_count_for_user(user)
1168 |> where([p], p.conversation_id == ^conversation.id)
1171 |> join(:inner, [u], p in subquery(unread_query))
1173 inc: [unread_conversation_count: 1]
1175 |> where([u], u.id == ^user.id)
1176 |> where([u, p], p.count == 0)
1178 |> Repo.update_all([])
1180 {1, [user]} -> set_cache(user)
1185 def increment_unread_conversation_count(_, user), do: {:ok, user}
1187 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
1188 def get_users_from_set(ap_ids, local_only \\ true) do
1189 criteria = %{ap_id: ap_ids, deactivated: false}
1190 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
1192 User.Query.build(criteria)
1196 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
1197 def get_recipients_from_activity(%Activity{recipients: to, actor: actor}) do
1200 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
1204 @spec mute(User.t(), User.t(), boolean()) ::
1205 {:ok, list(UserRelationship.t())} | {:error, String.t()}
1206 def mute(%User{} = muter, %User{} = mutee, notifications? \\ true) do
1207 add_to_mutes(muter, mutee, notifications?)
1210 def unmute(%User{} = muter, %User{} = mutee) do
1211 remove_from_mutes(muter, mutee)
1214 def subscribe(%User{} = subscriber, %User{} = target) do
1215 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
1217 if blocks?(target, subscriber) and deny_follow_blocked do
1218 {:error, "Could not subscribe: #{target.nickname} is blocking you"}
1220 # Note: the relationship is inverse: subscriber acts as relationship target
1221 UserRelationship.create_inverse_subscription(target, subscriber)
1225 def subscribe(%User{} = subscriber, %{ap_id: ap_id}) do
1226 with %User{} = subscribee <- get_cached_by_ap_id(ap_id) do
1227 subscribe(subscriber, subscribee)
1231 def unsubscribe(%User{} = unsubscriber, %User{} = target) do
1232 # Note: the relationship is inverse: subscriber acts as relationship target
1233 UserRelationship.delete_inverse_subscription(target, unsubscriber)
1236 def unsubscribe(%User{} = unsubscriber, %{ap_id: ap_id}) do
1237 with %User{} = user <- get_cached_by_ap_id(ap_id) do
1238 unsubscribe(unsubscriber, user)
1242 def block(%User{} = blocker, %User{} = blocked) do
1243 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
1245 if following?(blocker, blocked) do
1246 {:ok, blocker, _} = unfollow(blocker, blocked)
1252 # clear any requested follows as well
1254 case CommonAPI.reject_follow_request(blocked, blocker) do
1255 {:ok, %User{} = updated_blocked} -> updated_blocked
1259 unsubscribe(blocked, blocker)
1261 if following?(blocked, blocker), do: unfollow(blocked, blocker)
1263 {:ok, blocker} = update_follower_count(blocker)
1264 {:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
1265 add_to_block(blocker, blocked)
1268 # helper to handle the block given only an actor's AP id
1269 def block(%User{} = blocker, %{ap_id: ap_id}) do
1270 block(blocker, get_cached_by_ap_id(ap_id))
1273 def unblock(%User{} = blocker, %User{} = blocked) do
1274 remove_from_block(blocker, blocked)
1277 # helper to handle the block given only an actor's AP id
1278 def unblock(%User{} = blocker, %{ap_id: ap_id}) do
1279 unblock(blocker, get_cached_by_ap_id(ap_id))
1282 def mutes?(nil, _), do: false
1283 def mutes?(%User{} = user, %User{} = target), do: mutes_user?(user, target)
1285 def mutes_user?(%User{} = user, %User{} = target) do
1286 UserRelationship.mute_exists?(user, target)
1289 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
1290 def muted_notifications?(nil, _), do: false
1292 def muted_notifications?(%User{} = user, %User{} = target),
1293 do: UserRelationship.notification_mute_exists?(user, target)
1295 def blocks?(nil, _), do: false
1297 def blocks?(%User{} = user, %User{} = target) do
1298 blocks_user?(user, target) ||
1299 (blocks_domain?(user, target) and not User.following?(user, target))
1302 def blocks_user?(%User{} = user, %User{} = target) do
1303 UserRelationship.block_exists?(user, target)
1306 def blocks_user?(_, _), do: false
1308 def blocks_domain?(%User{} = user, %User{} = target) do
1309 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
1310 %{host: host} = URI.parse(target.ap_id)
1311 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1314 def blocks_domain?(_, _), do: false
1316 def subscribed_to?(%User{} = user, %User{} = target) do
1317 # Note: the relationship is inverse: subscriber acts as relationship target
1318 UserRelationship.inverse_subscription_exists?(target, user)
1321 def subscribed_to?(%User{} = user, %{ap_id: ap_id}) do
1322 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1323 subscribed_to?(user, target)
1328 Returns map of outgoing (blocked, muted etc.) relationships' user AP IDs by relation type.
1329 E.g. `outgoing_relationships_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}`
1331 @spec outgoing_relationships_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())}
1332 def outgoing_relationships_ap_ids(_user, []), do: %{}
1334 def outgoing_relationships_ap_ids(nil, _relationship_types), do: %{}
1336 def outgoing_relationships_ap_ids(%User{} = user, relationship_types)
1337 when is_list(relationship_types) do
1340 |> assoc(:outgoing_relationships)
1341 |> join(:inner, [user_rel], u in assoc(user_rel, :target))
1342 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1343 |> select([user_rel, u], [user_rel.relationship_type, fragment("array_agg(?)", u.ap_id)])
1344 |> group_by([user_rel, u], user_rel.relationship_type)
1346 |> Enum.into(%{}, fn [k, v] -> {k, v} end)
1351 fn rel_type -> {rel_type, db_result[rel_type] || []} end
1355 def incoming_relationships_ungrouped_ap_ids(user, relationship_types, ap_ids \\ nil)
1357 def incoming_relationships_ungrouped_ap_ids(_user, [], _ap_ids), do: []
1359 def incoming_relationships_ungrouped_ap_ids(nil, _relationship_types, _ap_ids), do: []
1361 def incoming_relationships_ungrouped_ap_ids(%User{} = user, relationship_types, ap_ids)
1362 when is_list(relationship_types) do
1364 |> assoc(:incoming_relationships)
1365 |> join(:inner, [user_rel], u in assoc(user_rel, :source))
1366 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1367 |> maybe_filter_on_ap_id(ap_ids)
1368 |> select([user_rel, u], u.ap_id)
1373 defp maybe_filter_on_ap_id(query, ap_ids) when is_list(ap_ids) do
1374 where(query, [user_rel, u], u.ap_id in ^ap_ids)
1377 defp maybe_filter_on_ap_id(query, _ap_ids), do: query
1379 def deactivate_async(user, status \\ true) do
1380 BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
1383 def deactivate(user, status \\ true)
1385 def deactivate(users, status) when is_list(users) do
1386 Repo.transaction(fn ->
1387 for user <- users, do: deactivate(user, status)
1391 def deactivate(%User{} = user, status) do
1392 with {:ok, user} <- set_activation_status(user, status) do
1395 |> Enum.filter(& &1.local)
1396 |> Enum.each(fn follower ->
1397 follower |> update_following_count() |> set_cache()
1400 # Only update local user counts, remote will be update during the next pull.
1403 |> Enum.filter(& &1.local)
1404 |> Enum.each(&update_follower_count/1)
1410 def update_notification_settings(%User{} = user, settings) do
1412 |> cast(%{notification_settings: settings}, [])
1413 |> cast_embed(:notification_settings)
1414 |> validate_required([:notification_settings])
1415 |> update_and_set_cache()
1418 def delete(users) when is_list(users) do
1419 for user <- users, do: delete(user)
1422 def delete(%User{} = user) do
1423 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1426 def perform(:force_password_reset, user), do: force_password_reset(user)
1428 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1429 def perform(:delete, %User{} = user) do
1430 {:ok, _user} = ActivityPub.delete(user)
1432 # Remove all relationships
1435 |> Enum.each(fn follower ->
1436 ActivityPub.unfollow(follower, user)
1437 unfollow(follower, user)
1442 |> Enum.each(fn followed ->
1443 ActivityPub.unfollow(user, followed)
1444 unfollow(user, followed)
1447 delete_user_activities(user)
1451 |> change(%{deactivated: true, email: nil})
1452 |> update_and_set_cache()
1454 invalidate_cache(user)
1459 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1461 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1462 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1463 when is_list(blocked_identifiers) do
1465 blocked_identifiers,
1466 fn blocked_identifier ->
1467 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1468 {:ok, _user_block} <- block(blocker, blocked),
1469 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1473 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1480 def perform(:follow_import, %User{} = follower, followed_identifiers)
1481 when is_list(followed_identifiers) do
1483 followed_identifiers,
1484 fn followed_identifier ->
1485 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1486 {:ok, follower} <- maybe_direct_follow(follower, followed),
1487 {:ok, _} <- ActivityPub.follow(follower, followed) do
1491 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1498 @spec external_users_query() :: Ecto.Query.t()
1499 def external_users_query do
1507 @spec external_users(keyword()) :: [User.t()]
1508 def external_users(opts \\ []) do
1510 external_users_query()
1511 |> select([u], struct(u, [:id, :ap_id]))
1515 do: where(query, [u], u.id > ^opts[:max_id]),
1520 do: limit(query, ^opts[:limit]),
1526 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1527 BackgroundWorker.enqueue("blocks_import", %{
1528 "blocker_id" => blocker.id,
1529 "blocked_identifiers" => blocked_identifiers
1533 def follow_import(%User{} = follower, followed_identifiers)
1534 when is_list(followed_identifiers) do
1535 BackgroundWorker.enqueue("follow_import", %{
1536 "follower_id" => follower.id,
1537 "followed_identifiers" => followed_identifiers
1541 def delete_user_activities(%User{ap_id: ap_id}) do
1543 |> Activity.Queries.by_actor()
1544 |> RepoStreamer.chunk_stream(50)
1545 |> Stream.each(fn activities -> Enum.each(activities, &delete_activity/1) end)
1549 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1551 |> Object.normalize()
1552 |> ActivityPub.delete()
1555 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1556 object = Object.normalize(activity)
1559 |> get_cached_by_ap_id()
1560 |> ActivityPub.unlike(object)
1563 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1564 object = Object.normalize(activity)
1567 |> get_cached_by_ap_id()
1568 |> ActivityPub.unannounce(object)
1571 defp delete_activity(_activity), do: "Doing nothing"
1573 def html_filter_policy(%User{no_rich_text: true}) do
1574 Pleroma.HTML.Scrubber.TwitterText
1577 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1579 def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
1581 def get_or_fetch_by_ap_id(ap_id) do
1582 user = get_cached_by_ap_id(ap_id)
1584 if !is_nil(user) and !needs_update?(user) do
1587 fetch_by_ap_id(ap_id)
1592 Creates an internal service actor by URI if missing.
1593 Optionally takes nickname for addressing.
1595 @spec get_or_create_service_actor_by_ap_id(String.t(), String.t()) :: User.t() | nil
1596 def get_or_create_service_actor_by_ap_id(uri, nickname) do
1598 case get_cached_by_ap_id(uri) do
1600 with {:error, %{errors: errors}} <- create_service_actor(uri, nickname) do
1601 Logger.error("Cannot create service actor: #{uri}/.\n#{inspect(errors)}")
1605 %User{invisible: false} = user ->
1615 @spec set_invisible(User.t()) :: {:ok, User.t()}
1616 defp set_invisible(user) do
1618 |> change(%{invisible: true})
1619 |> update_and_set_cache()
1622 @spec create_service_actor(String.t(), String.t()) ::
1623 {:ok, User.t()} | {:error, Ecto.Changeset.t()}
1624 defp create_service_actor(uri, nickname) do
1630 follower_address: uri <> "/followers"
1633 |> unique_constraint(:nickname)
1638 def public_key(%{public_key: public_key_pem}) when is_binary(public_key_pem) do
1641 |> :public_key.pem_decode()
1643 |> :public_key.pem_entry_decode()
1648 def public_key(_), do: {:error, "key not found"}
1650 def get_public_key_for_ap_id(ap_id) do
1651 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1652 {:ok, public_key} <- public_key(user) do
1659 def ap_enabled?(%User{local: true}), do: true
1660 def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled
1661 def ap_enabled?(_), do: false
1663 @doc "Gets or fetch a user by uri or nickname."
1664 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1665 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1666 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1668 # wait a period of time and return newest version of the User structs
1669 # this is because we have synchronous follow APIs and need to simulate them
1670 # with an async handshake
1671 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1672 with %User{} = a <- get_cached_by_id(a.id),
1673 %User{} = b <- get_cached_by_id(b.id) do
1680 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1681 with :ok <- :timer.sleep(timeout),
1682 %User{} = a <- get_cached_by_id(a.id),
1683 %User{} = b <- get_cached_by_id(b.id) do
1690 def parse_bio(bio) when is_binary(bio) and bio != "" do
1692 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1696 def parse_bio(_), do: ""
1698 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1699 # TODO: get profile URLs other than user.ap_id
1700 profile_urls = [user.ap_id]
1703 |> CommonUtils.format_input("text/plain",
1704 mentions_format: :full,
1705 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1710 def parse_bio(_, _), do: ""
1712 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1713 Repo.transaction(fn ->
1714 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1718 def tag(nickname, tags) when is_binary(nickname),
1719 do: tag(get_by_nickname(nickname), tags)
1721 def tag(%User{} = user, tags),
1722 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1724 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1725 Repo.transaction(fn ->
1726 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1730 def untag(nickname, tags) when is_binary(nickname),
1731 do: untag(get_by_nickname(nickname), tags)
1733 def untag(%User{} = user, tags),
1734 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1736 defp update_tags(%User{} = user, new_tags) do
1737 {:ok, updated_user} =
1739 |> change(%{tags: new_tags})
1740 |> update_and_set_cache()
1745 defp normalize_tags(tags) do
1748 |> Enum.map(&String.downcase/1)
1751 defp local_nickname_regex do
1752 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1753 @extended_local_nickname_regex
1755 @strict_local_nickname_regex
1759 def local_nickname(nickname_or_mention) do
1762 |> String.split("@")
1766 def full_nickname(nickname_or_mention),
1767 do: String.trim_leading(nickname_or_mention, "@")
1769 def error_user(ap_id) do
1773 nickname: "erroruser@example.com",
1774 inserted_at: NaiveDateTime.utc_now()
1778 @spec all_superusers() :: [User.t()]
1779 def all_superusers do
1780 User.Query.build(%{super_users: true, local: true, deactivated: false})
1784 def muting_reblogs?(%User{} = user, %User{} = target) do
1785 UserRelationship.reblog_mute_exists?(user, target)
1788 def showing_reblogs?(%User{} = user, %User{} = target) do
1789 not muting_reblogs?(user, target)
1793 The function returns a query to get users with no activity for given interval of days.
1794 Inactive users are those who didn't read any notification, or had any activity where
1795 the user is the activity's actor, during `inactivity_threshold` days.
1796 Deactivated users will not appear in this list.
1800 iex> Pleroma.User.list_inactive_users()
1803 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1804 def list_inactive_users_query(inactivity_threshold \\ 7) do
1805 negative_inactivity_threshold = -inactivity_threshold
1806 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1807 # Subqueries are not supported in `where` clauses, join gets too complicated.
1808 has_read_notifications =
1809 from(n in Pleroma.Notification,
1810 where: n.seen == true,
1812 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1815 |> Pleroma.Repo.all()
1817 from(u in Pleroma.User,
1818 left_join: a in Pleroma.Activity,
1819 on: u.ap_id == a.actor,
1820 where: not is_nil(u.nickname),
1821 where: u.deactivated != ^true,
1822 where: u.id not in ^has_read_notifications,
1825 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1826 is_nil(max(a.inserted_at))
1831 Enable or disable email notifications for user
1835 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => false}}, "digest", true)
1836 Pleroma.User{email_notifications: %{"digest" => true}}
1838 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => true}}, "digest", false)
1839 Pleroma.User{email_notifications: %{"digest" => false}}
1841 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1842 {:ok, t()} | {:error, Ecto.Changeset.t()}
1843 def switch_email_notifications(user, type, status) do
1844 User.update_email_notifications(user, %{type => status})
1848 Set `last_digest_emailed_at` value for the user to current time
1850 @spec touch_last_digest_emailed_at(t()) :: t()
1851 def touch_last_digest_emailed_at(user) do
1852 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1854 {:ok, updated_user} =
1856 |> change(%{last_digest_emailed_at: now})
1857 |> update_and_set_cache()
1862 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1863 def toggle_confirmation(%User{} = user) do
1865 |> confirmation_changeset(need_confirmation: !user.confirmation_pending)
1866 |> update_and_set_cache()
1869 @spec toggle_confirmation([User.t()]) :: [{:ok, User.t()} | {:error, Changeset.t()}]
1870 def toggle_confirmation(users) do
1871 Enum.map(users, &toggle_confirmation/1)
1874 def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
1878 def get_mascot(%{mascot: mascot}) when is_nil(mascot) do
1879 # use instance-default
1880 config = Pleroma.Config.get([:assets, :mascots])
1881 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1882 mascot = Keyword.get(config, default_mascot)
1885 "id" => "default-mascot",
1886 "url" => mascot[:url],
1887 "preview_url" => mascot[:url],
1889 "mime_type" => mascot[:mime_type]
1894 def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
1896 def ensure_keys_present(%User{} = user) do
1897 with {:ok, pem} <- Keys.generate_rsa_pem() do
1899 |> cast(%{keys: pem}, [:keys])
1900 |> validate_required([:keys])
1901 |> update_and_set_cache()
1905 def get_ap_ids_by_nicknames(nicknames) do
1907 where: u.nickname in ^nicknames,
1913 defdelegate search(query, opts \\ []), to: User.Search
1915 defp put_password_hash(
1916 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1918 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1921 defp put_password_hash(changeset), do: changeset
1923 def is_internal_user?(%User{nickname: nil}), do: true
1924 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1925 def is_internal_user?(_), do: false
1927 # A hack because user delete activities have a fake id for whatever reason
1928 # TODO: Get rid of this
1929 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
1931 def get_delivered_users_by_object_id(object_id) do
1933 inner_join: delivery in assoc(u, :deliveries),
1934 where: delivery.object_id == ^object_id
1939 def change_email(user, email) do
1941 |> cast(%{email: email}, [:email])
1942 |> validate_required([:email])
1943 |> unique_constraint(:email)
1944 |> validate_format(:email, @email_regex)
1945 |> update_and_set_cache()
1948 # Internal function; public one is `deactivate/2`
1949 defp set_activation_status(user, deactivated) do
1951 |> cast(%{deactivated: deactivated}, [:deactivated])
1952 |> update_and_set_cache()
1955 def update_banner(user, banner) do
1957 |> cast(%{banner: banner}, [:banner])
1958 |> update_and_set_cache()
1961 def update_background(user, background) do
1963 |> cast(%{background: background}, [:background])
1964 |> update_and_set_cache()
1967 def roles(%{is_moderator: is_moderator, is_admin: is_admin}) do
1970 moderator: is_moderator
1974 def validate_fields(changeset, remote? \\ false) do
1975 limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
1976 limit = Pleroma.Config.get([:instance, limit_name], 0)
1979 |> validate_length(:fields, max: limit)
1980 |> validate_change(:fields, fn :fields, fields ->
1981 if Enum.all?(fields, &valid_field?/1) do
1989 defp valid_field?(%{"name" => name, "value" => value}) do
1990 name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255)
1991 value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255)
1993 is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
1994 String.length(value) <= value_limit
1997 defp valid_field?(_), do: false
1999 defp truncate_field(%{"name" => name, "value" => value}) do
2001 String.split_at(name, Pleroma.Config.get([:instance, :account_field_name_length], 255))
2004 String.split_at(value, Pleroma.Config.get([:instance, :account_field_value_length], 255))
2006 %{"name" => name, "value" => value}
2009 def admin_api_update(user, params) do
2016 |> update_and_set_cache()
2019 @doc "Signs user out of all applications"
2020 def global_sign_out(user) do
2021 OAuth.Authorization.delete_user_authorizations(user)
2022 OAuth.Token.delete_user_tokens(user)
2025 def mascot_update(user, url) do
2027 |> cast(%{mascot: url}, [:mascot])
2028 |> validate_required([:mascot])
2029 |> update_and_set_cache()
2032 def mastodon_settings_update(user, settings) do
2034 |> cast(%{settings: settings}, [:settings])
2035 |> validate_required([:settings])
2036 |> update_and_set_cache()
2039 @spec confirmation_changeset(User.t(), keyword()) :: Changeset.t()
2040 def confirmation_changeset(user, need_confirmation: need_confirmation?) do
2042 if need_confirmation? do
2044 confirmation_pending: true,
2045 confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
2049 confirmation_pending: false,
2050 confirmation_token: nil
2054 cast(user, params, [:confirmation_pending, :confirmation_token])
2057 def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do
2058 if id not in user.pinned_activities do
2059 max_pinned_statuses = Pleroma.Config.get([:instance, :max_pinned_statuses], 0)
2060 params = %{pinned_activities: user.pinned_activities ++ [id]}
2063 |> cast(params, [:pinned_activities])
2064 |> validate_length(:pinned_activities,
2065 max: max_pinned_statuses,
2066 message: "You have already pinned the maximum number of statuses"
2071 |> update_and_set_cache()
2074 def remove_pinnned_activity(user, %Pleroma.Activity{id: id}) do
2075 params = %{pinned_activities: List.delete(user.pinned_activities, id)}
2078 |> cast(params, [:pinned_activities])
2079 |> update_and_set_cache()
2082 def update_email_notifications(user, settings) do
2083 email_notifications =
2084 user.email_notifications
2085 |> Map.merge(settings)
2086 |> Map.take(["digest"])
2088 params = %{email_notifications: email_notifications}
2089 fields = [:email_notifications]
2092 |> cast(params, fields)
2093 |> validate_required(fields)
2094 |> update_and_set_cache()
2097 defp set_domain_blocks(user, domain_blocks) do
2098 params = %{domain_blocks: domain_blocks}
2101 |> cast(params, [:domain_blocks])
2102 |> validate_required([:domain_blocks])
2103 |> update_and_set_cache()
2106 def block_domain(user, domain_blocked) do
2107 set_domain_blocks(user, Enum.uniq([domain_blocked | user.domain_blocks]))
2110 def unblock_domain(user, domain_blocked) do
2111 set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked))
2114 @spec add_to_block(User.t(), User.t()) ::
2115 {:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()}
2116 defp add_to_block(%User{} = user, %User{} = blocked) do
2117 UserRelationship.create_block(user, blocked)
2120 @spec add_to_block(User.t(), User.t()) ::
2121 {:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()}
2122 defp remove_from_block(%User{} = user, %User{} = blocked) do
2123 UserRelationship.delete_block(user, blocked)
2126 defp add_to_mutes(%User{} = user, %User{} = muted_user, notifications?) do
2127 with {:ok, user_mute} <- UserRelationship.create_mute(user, muted_user),
2128 {:ok, user_notification_mute} <-
2129 (notifications? && UserRelationship.create_notification_mute(user, muted_user)) ||
2131 {:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
2135 defp remove_from_mutes(user, %User{} = muted_user) do
2136 with {:ok, user_mute} <- UserRelationship.delete_mute(user, muted_user),
2137 {:ok, user_notification_mute} <-
2138 UserRelationship.delete_notification_mute(user, muted_user) do
2139 {:ok, [user_mute, user_notification_mute]}
2143 def set_invisible(user, invisible) do
2144 params = %{invisible: invisible}
2147 |> cast(params, [:invisible])
2148 |> validate_required([:invisible])
2149 |> update_and_set_cache()
2152 def sanitize_html(%User{} = user) do
2153 sanitize_html(user, nil)
2156 # User data that mastodon isn't filtering (treated as plaintext):
2159 def sanitize_html(%User{} = user, filter) do
2161 Enum.map(user.fields, fn %{"name" => name, "value" => value} ->
2164 "value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
2169 |> Map.put(:bio, HTML.filter_tags(user.bio, filter))
2170 |> Map.put(:fields, fields)