1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.User do
10 import Ecto, only: [assoc: 2]
14 alias Pleroma.Activity
16 alias Pleroma.Conversation.Participation
17 alias Pleroma.Delivery
18 alias Pleroma.FollowingRelationship
21 alias Pleroma.Notification
23 alias Pleroma.Registration
25 alias Pleroma.RepoStreamer
27 alias Pleroma.UserRelationship
29 alias Pleroma.Web.ActivityPub.ActivityPub
30 alias Pleroma.Web.ActivityPub.Utils
31 alias Pleroma.Web.CommonAPI
32 alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
33 alias Pleroma.Web.OAuth
34 alias Pleroma.Web.RelMe
35 alias Pleroma.Workers.BackgroundWorker
39 @type t :: %__MODULE__{}
40 @type account_status :: :active | :deactivated | :password_reset_pending | :confirmation_pending
41 @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
43 # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
44 @email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
46 @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
47 @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
49 # AP ID user relationships (blocks, mutes etc.)
50 # Format: [rel_type: [outgoing_rel: :outgoing_rel_target, incoming_rel: :incoming_rel_source]]
51 @user_relationships_config [
53 blocker_blocks: :blocked_users,
54 blockee_blocks: :blocker_users
57 muter_mutes: :muted_users,
58 mutee_mutes: :muter_users
61 reblog_muter_mutes: :reblog_muted_users,
62 reblog_mutee_mutes: :reblog_muter_users
65 notification_muter_mutes: :notification_muted_users,
66 notification_mutee_mutes: :notification_muter_users
68 # Note: `inverse_subscription` relationship is inverse: subscriber acts as relationship target
69 inverse_subscription: [
70 subscribee_subscriptions: :subscriber_users,
71 subscriber_subscriptions: :subscribee_users
77 field(:email, :string)
79 field(:nickname, :string)
80 field(:password_hash, :string)
81 field(:password, :string, virtual: true)
82 field(:password_confirmation, :string, virtual: true)
84 field(:ap_id, :string)
86 field(:local, :boolean, default: true)
87 field(:follower_address, :string)
88 field(:following_address, :string)
89 field(:search_rank, :float, virtual: true)
90 field(:search_type, :integer, virtual: true)
91 field(:tags, {:array, :string}, default: [])
92 field(:last_refreshed_at, :naive_datetime_usec)
93 field(:last_digest_emailed_at, :naive_datetime)
94 field(:banner, :map, default: %{})
95 field(:background, :map, default: %{})
96 field(:source_data, :map, default: %{})
97 field(:note_count, :integer, default: 0)
98 field(:follower_count, :integer, default: 0)
99 field(:following_count, :integer, default: 0)
100 field(:locked, :boolean, default: false)
101 field(:confirmation_pending, :boolean, default: false)
102 field(:password_reset_pending, :boolean, default: false)
103 field(:confirmation_token, :string, default: nil)
104 field(:default_scope, :string, default: "public")
105 field(:domain_blocks, {:array, :string}, default: [])
106 field(:deactivated, :boolean, default: false)
107 field(:no_rich_text, :boolean, default: false)
108 field(:ap_enabled, :boolean, default: false)
109 field(:is_moderator, :boolean, default: false)
110 field(:is_admin, :boolean, default: false)
111 field(:show_role, :boolean, default: true)
112 field(:settings, :map, default: nil)
113 field(:magic_key, :string, default: nil)
114 field(:uri, :string, default: nil)
115 field(:hide_followers_count, :boolean, default: false)
116 field(:hide_follows_count, :boolean, default: false)
117 field(:hide_followers, :boolean, default: false)
118 field(:hide_follows, :boolean, default: false)
119 field(:hide_favorites, :boolean, default: true)
120 field(:unread_conversation_count, :integer, default: 0)
121 field(:pinned_activities, {:array, :string}, default: [])
122 field(:email_notifications, :map, default: %{"digest" => false})
123 field(:mascot, :map, default: nil)
124 field(:emoji, {:array, :map}, default: [])
125 field(:pleroma_settings_store, :map, default: %{})
126 field(:fields, {:array, :map}, default: [])
127 field(:raw_fields, {:array, :map}, default: [])
128 field(:discoverable, :boolean, default: false)
129 field(:invisible, :boolean, default: false)
130 field(:allow_following_move, :boolean, default: true)
131 field(:skip_thread_containment, :boolean, default: false)
132 field(:actor_type, :string, default: "Person")
133 field(:also_known_as, {:array, :string}, default: [])
136 :notification_settings,
137 Pleroma.User.NotificationSetting,
141 has_many(:notifications, Notification)
142 has_many(:registrations, Registration)
143 has_many(:deliveries, Delivery)
145 has_many(:outgoing_relationships, UserRelationship, foreign_key: :source_id)
146 has_many(:incoming_relationships, UserRelationship, foreign_key: :target_id)
148 for {relationship_type,
150 {outgoing_relation, outgoing_relation_target},
151 {incoming_relation, incoming_relation_source}
152 ]} <- @user_relationships_config do
153 # Definitions of `has_many` relations: :blocker_blocks, :muter_mutes, :reblog_muter_mutes,
154 # :notification_muter_mutes, :subscribee_subscriptions
155 has_many(outgoing_relation, UserRelationship,
156 foreign_key: :source_id,
157 where: [relationship_type: relationship_type]
160 # Definitions of `has_many` relations: :blockee_blocks, :mutee_mutes, :reblog_mutee_mutes,
161 # :notification_mutee_mutes, :subscriber_subscriptions
162 has_many(incoming_relation, UserRelationship,
163 foreign_key: :target_id,
164 where: [relationship_type: relationship_type]
167 # Definitions of `has_many` relations: :blocked_users, :muted_users, :reblog_muted_users,
168 # :notification_muted_users, :subscriber_users
169 has_many(outgoing_relation_target, through: [outgoing_relation, :target])
171 # Definitions of `has_many` relations: :blocker_users, :muter_users, :reblog_muter_users,
172 # :notification_muter_users, :subscribee_users
173 has_many(incoming_relation_source, through: [incoming_relation, :source])
176 # `:blocks` is deprecated (replaced with `blocked_users` relation)
177 field(:blocks, {:array, :string}, default: [])
178 # `:mutes` is deprecated (replaced with `muted_users` relation)
179 field(:mutes, {:array, :string}, default: [])
180 # `:muted_reblogs` is deprecated (replaced with `reblog_muted_users` relation)
181 field(:muted_reblogs, {:array, :string}, default: [])
182 # `:muted_notifications` is deprecated (replaced with `notification_muted_users` relation)
183 field(:muted_notifications, {:array, :string}, default: [])
184 # `:subscribers` is deprecated (replaced with `subscriber_users` relation)
185 field(:subscribers, {:array, :string}, default: [])
190 for {_relationship_type, [{_outgoing_relation, outgoing_relation_target}, _]} <-
191 @user_relationships_config do
192 # `def blocked_users_relation/2`, `def muted_users_relation/2`,
193 # `def reblog_muted_users_relation/2`, `def notification_muted_users/2`,
194 # `def subscriber_users/2`
195 def unquote(:"#{outgoing_relation_target}_relation")(user, restrict_deactivated? \\ false) do
196 target_users_query = assoc(user, unquote(outgoing_relation_target))
198 if restrict_deactivated? do
199 restrict_deactivated(target_users_query)
205 # `def blocked_users/2`, `def muted_users/2`, `def reblog_muted_users/2`,
206 # `def notification_muted_users/2`, `def subscriber_users/2`
207 def unquote(outgoing_relation_target)(user, restrict_deactivated? \\ false) do
209 |> apply(unquote(:"#{outgoing_relation_target}_relation"), [
211 restrict_deactivated?
216 # `def blocked_users_ap_ids/2`, `def muted_users_ap_ids/2`, `def reblog_muted_users_ap_ids/2`,
217 # `def notification_muted_users_ap_ids/2`, `def subscriber_users_ap_ids/2`
218 def unquote(:"#{outgoing_relation_target}_ap_ids")(user, restrict_deactivated? \\ false) do
220 |> apply(unquote(:"#{outgoing_relation_target}_relation"), [
222 restrict_deactivated?
224 |> select([u], u.ap_id)
230 Dumps Flake Id to SQL-compatible format (16-byte UUID).
231 E.g. "9pQtDGXuq4p3VlcJEm" -> <<0, 0, 1, 110, 179, 218, 42, 92, 213, 41, 44, 227, 95, 213, 0, 0>>
233 def binary_id(source_id) when is_binary(source_id) do
234 with {:ok, dumped_id} <- FlakeId.Ecto.CompatType.dump(source_id) do
241 def binary_id(source_ids) when is_list(source_ids) do
242 Enum.map(source_ids, &binary_id/1)
245 def binary_id(%User{} = user), do: binary_id(user.id)
247 @doc "Returns status account"
248 @spec account_status(User.t()) :: account_status()
249 def account_status(%User{deactivated: true}), do: :deactivated
250 def account_status(%User{password_reset_pending: true}), do: :password_reset_pending
252 def account_status(%User{confirmation_pending: true}) do
253 case Config.get([:instance, :account_activation_required]) do
254 true -> :confirmation_pending
259 def account_status(%User{}), do: :active
261 @spec visible_for?(User.t(), User.t() | nil) :: boolean()
262 def visible_for?(user, for_user \\ nil)
264 def visible_for?(%User{invisible: true}, _), do: false
266 def visible_for?(%User{id: user_id}, %User{id: user_id}), do: true
268 def visible_for?(%User{local: local} = user, nil) do
274 if Config.get([:restrict_unauthenticated, :profiles, cfg_key]),
276 else: account_status(user) == :active
279 def visible_for?(%User{} = user, for_user) do
280 account_status(user) == :active || superuser?(for_user)
283 def visible_for?(_, _), do: false
285 @spec superuser?(User.t()) :: boolean()
286 def superuser?(%User{local: true, is_admin: true}), do: true
287 def superuser?(%User{local: true, is_moderator: true}), do: true
288 def superuser?(_), do: false
290 @spec invisible?(User.t()) :: boolean()
291 def invisible?(%User{invisible: true}), do: true
292 def invisible?(_), do: false
294 def avatar_url(user, options \\ []) do
296 %{"url" => [%{"href" => href} | _]} -> href
297 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
301 def banner_url(user, options \\ []) do
303 %{"url" => [%{"href" => href} | _]} -> href
304 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
308 def ap_id(%User{nickname: nickname}), do: "#{Web.base_url()}/users/#{nickname}"
310 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
311 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
313 @spec ap_following(User.t()) :: String.t()
314 def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
315 def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
317 @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
318 def restrict_deactivated(query) do
319 from(u in query, where: u.deactivated != ^true)
322 defdelegate following_count(user), to: FollowingRelationship
324 defp truncate_fields_param(params) do
325 if Map.has_key?(params, :fields) do
326 Map.put(params, :fields, Enum.map(params[:fields], &truncate_field/1))
332 defp truncate_if_exists(params, key, max_length) do
333 if Map.has_key?(params, key) and is_binary(params[key]) do
334 {value, _chopped} = String.split_at(params[key], max_length)
335 Map.put(params, key, value)
341 def remote_user_creation(params) do
342 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
343 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
347 |> truncate_if_exists(:name, name_limit)
348 |> truncate_if_exists(:bio, bio_limit)
349 |> truncate_fields_param()
369 :hide_followers_count,
380 |> validate_required([:name, :ap_id])
381 |> unique_constraint(:nickname)
382 |> validate_format(:nickname, @email_regex)
383 |> validate_length(:bio, max: bio_limit)
384 |> validate_length(:name, max: name_limit)
385 |> validate_fields(true)
387 case params[:source_data] do
388 %{"followers" => followers, "following" => following} ->
390 |> put_change(:follower_address, followers)
391 |> put_change(:following_address, following)
394 followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
395 put_change(changeset, :follower_address, followers)
399 def update_changeset(struct, params \\ %{}) do
400 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
401 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
416 :hide_followers_count,
419 :allow_following_move,
422 :skip_thread_containment,
425 :pleroma_settings_store,
431 |> unique_constraint(:nickname)
432 |> validate_format(:nickname, local_nickname_regex())
433 |> validate_length(:bio, max: bio_limit)
434 |> validate_length(:name, min: 1, max: name_limit)
436 |> put_change_if_present(:bio, &{:ok, parse_bio(&1, struct)})
437 |> put_change_if_present(:avatar, &put_upload(&1, :avatar))
438 |> put_change_if_present(:banner, &put_upload(&1, :banner))
439 |> put_change_if_present(:background, &put_upload(&1, :background))
440 |> put_change_if_present(
441 :pleroma_settings_store,
442 &{:ok, Map.merge(struct.pleroma_settings_store, &1)}
444 |> validate_fields(false)
447 defp put_fields(changeset) do
448 if raw_fields = get_change(changeset, :raw_fields) do
451 |> Enum.filter(fn %{"name" => n} -> n != "" end)
455 |> Enum.map(fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
458 |> put_change(:raw_fields, raw_fields)
459 |> put_change(:fields, fields)
465 defp put_change_if_present(changeset, map_field, value_function) do
466 if value = get_change(changeset, map_field) do
467 with {:ok, new_value} <- value_function.(value) do
468 put_change(changeset, map_field, new_value)
477 defp put_upload(value, type) do
478 with %Plug.Upload{} <- value,
479 {:ok, object} <- ActivityPub.upload(value, type: type) do
484 def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
485 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
486 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
488 params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
490 params = if remote?, do: truncate_fields_param(params), else: params
512 :allow_following_move,
514 :hide_followers_count,
520 |> unique_constraint(:nickname)
521 |> validate_format(:nickname, local_nickname_regex())
522 |> validate_length(:bio, max: bio_limit)
523 |> validate_length(:name, max: name_limit)
524 |> validate_fields(remote?)
527 def update_as_admin_changeset(struct, params) do
529 |> update_changeset(params)
530 |> cast(params, [:email])
531 |> delete_change(:also_known_as)
532 |> unique_constraint(:email)
533 |> validate_format(:email, @email_regex)
536 @spec update_as_admin(%User{}, map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
537 def update_as_admin(user, params) do
538 params = Map.put(params, "password_confirmation", params["password"])
539 changeset = update_as_admin_changeset(user, params)
541 if params["password"] do
542 reset_password(user, changeset, params)
544 User.update_and_set_cache(changeset)
548 def password_update_changeset(struct, params) do
550 |> cast(params, [:password, :password_confirmation])
551 |> validate_required([:password, :password_confirmation])
552 |> validate_confirmation(:password)
553 |> put_password_hash()
554 |> put_change(:password_reset_pending, false)
557 @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
558 def reset_password(%User{} = user, params) do
559 reset_password(user, user, params)
562 def reset_password(%User{id: user_id} = user, struct, params) do
565 |> Multi.update(:user, password_update_changeset(struct, params))
566 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
567 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
569 case Repo.transaction(multi) do
570 {:ok, %{user: user} = _} -> set_cache(user)
571 {:error, _, changeset, _} -> {:error, changeset}
575 def update_password_reset_pending(user, value) do
578 |> put_change(:password_reset_pending, value)
579 |> update_and_set_cache()
582 def force_password_reset_async(user) do
583 BackgroundWorker.enqueue("force_password_reset", %{"user_id" => user.id})
586 @spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
587 def force_password_reset(user), do: update_password_reset_pending(user, true)
589 def register_changeset(struct, params \\ %{}, opts \\ []) do
590 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
591 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
594 if is_nil(opts[:need_confirmation]) do
595 Pleroma.Config.get([:instance, :account_activation_required])
597 opts[:need_confirmation]
601 |> confirmation_changeset(need_confirmation: need_confirmation?)
602 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
603 |> validate_required([:name, :nickname, :password, :password_confirmation])
604 |> validate_confirmation(:password)
605 |> unique_constraint(:email)
606 |> unique_constraint(:nickname)
607 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
608 |> validate_format(:nickname, local_nickname_regex())
609 |> validate_format(:email, @email_regex)
610 |> validate_length(:bio, max: bio_limit)
611 |> validate_length(:name, min: 1, max: name_limit)
612 |> maybe_validate_required_email(opts[:external])
615 |> unique_constraint(:ap_id)
616 |> put_following_and_follower_address()
619 def maybe_validate_required_email(changeset, true), do: changeset
621 def maybe_validate_required_email(changeset, _) do
622 if Pleroma.Config.get([:instance, :account_activation_required]) do
623 validate_required(changeset, [:email])
629 defp put_ap_id(changeset) do
630 ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
631 put_change(changeset, :ap_id, ap_id)
634 defp put_following_and_follower_address(changeset) do
635 followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
638 |> put_change(:follower_address, followers)
641 defp autofollow_users(user) do
642 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
645 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
648 follow_all(user, autofollowed_users)
651 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
652 def register(%Ecto.Changeset{} = changeset) do
653 with {:ok, user} <- Repo.insert(changeset) do
654 post_register_action(user)
658 def post_register_action(%User{} = user) do
659 with {:ok, user} <- autofollow_users(user),
660 {:ok, user} <- set_cache(user),
661 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
662 {:ok, _} <- try_send_confirmation_email(user) do
667 def try_send_confirmation_email(%User{} = user) do
668 if user.confirmation_pending &&
669 Pleroma.Config.get([:instance, :account_activation_required]) do
671 |> Pleroma.Emails.UserEmail.account_confirmation_email()
672 |> Pleroma.Emails.Mailer.deliver_async()
680 def try_send_confirmation_email(users) do
681 Enum.each(users, &try_send_confirmation_email/1)
684 def needs_update?(%User{local: true}), do: false
686 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
688 def needs_update?(%User{local: false} = user) do
689 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
692 def needs_update?(_), do: true
694 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
695 def maybe_direct_follow(%User{} = follower, %User{local: true, locked: true} = followed) do
696 follow(follower, followed, "pending")
699 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
700 follow(follower, followed)
703 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
704 if not ap_enabled?(followed) do
705 follow(follower, followed)
711 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
712 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
713 def follow_all(follower, followeds) do
715 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
716 |> Enum.each(&follow(follower, &1, "accept"))
721 defdelegate following(user), to: FollowingRelationship
723 def follow(%User{} = follower, %User{} = followed, state \\ "accept") do
724 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
727 followed.deactivated ->
728 {:error, "Could not follow user: #{followed.nickname} is deactivated."}
730 deny_follow_blocked and blocks?(followed, follower) ->
731 {:error, "Could not follow user: #{followed.nickname} blocked you."}
734 FollowingRelationship.follow(follower, followed, state)
736 {:ok, _} = update_follower_count(followed)
739 |> update_following_count()
744 def unfollow(%User{ap_id: ap_id}, %User{ap_id: ap_id}) do
745 {:error, "Not subscribed!"}
748 def unfollow(%User{} = follower, %User{} = followed) do
749 case get_follow_state(follower, followed) do
750 state when state in ["accept", "pending"] ->
751 FollowingRelationship.unfollow(follower, followed)
752 {:ok, followed} = update_follower_count(followed)
756 |> update_following_count()
759 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
762 {:error, "Not subscribed!"}
766 defdelegate following?(follower, followed), to: FollowingRelationship
768 def get_follow_state(%User{} = follower, %User{} = following) do
769 following_relationship = FollowingRelationship.get(follower, following)
770 get_follow_state(follower, following, following_relationship)
773 def get_follow_state(
776 following_relationship
778 case {following_relationship, following.local} do
780 case Utils.fetch_latest_follow(follower, following) do
781 %{data: %{"state" => state}} when state in ["pending", "accept"] -> state
785 {%{state: state}, _} ->
793 def locked?(%User{} = user) do
798 Repo.get_by(User, id: id)
801 def get_by_ap_id(ap_id) do
802 Repo.get_by(User, ap_id: ap_id)
805 def get_all_by_ap_id(ap_ids) do
806 from(u in __MODULE__,
807 where: u.ap_id in ^ap_ids
812 def get_all_by_ids(ids) do
813 from(u in __MODULE__, where: u.id in ^ids)
817 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
818 # of the ap_id and the domain and tries to get that user
819 def get_by_guessed_nickname(ap_id) do
820 domain = URI.parse(ap_id).host
821 name = List.last(String.split(ap_id, "/"))
822 nickname = "#{name}@#{domain}"
824 get_cached_by_nickname(nickname)
827 def set_cache({:ok, user}), do: set_cache(user)
828 def set_cache({:error, err}), do: {:error, err}
830 def set_cache(%User{} = user) do
831 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
832 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
836 def update_and_set_cache(struct, params) do
838 |> update_changeset(params)
839 |> update_and_set_cache()
842 def update_and_set_cache(changeset) do
843 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
848 def invalidate_cache(user) do
849 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
850 Cachex.del(:user_cache, "nickname:#{user.nickname}")
853 @spec get_cached_by_ap_id(String.t()) :: User.t() | nil
854 def get_cached_by_ap_id(ap_id) do
855 key = "ap_id:#{ap_id}"
857 with {:ok, nil} <- Cachex.get(:user_cache, key),
858 user when not is_nil(user) <- get_by_ap_id(ap_id),
859 {:ok, true} <- Cachex.put(:user_cache, key, user) do
867 def get_cached_by_id(id) do
871 Cachex.fetch!(:user_cache, key, fn _ ->
875 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
876 {:commit, user.ap_id}
882 get_cached_by_ap_id(ap_id)
885 def get_cached_by_nickname(nickname) do
886 key = "nickname:#{nickname}"
888 Cachex.fetch!(:user_cache, key, fn ->
889 case get_or_fetch_by_nickname(nickname) do
890 {:ok, user} -> {:commit, user}
891 {:error, _error} -> {:ignore, nil}
896 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
897 restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
900 is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
901 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
903 restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
904 get_cached_by_nickname(nickname_or_id)
906 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
907 get_cached_by_nickname(nickname_or_id)
914 def get_by_nickname(nickname) do
915 Repo.get_by(User, nickname: nickname) ||
916 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
917 Repo.get_by(User, nickname: local_nickname(nickname))
921 def get_by_email(email), do: Repo.get_by(User, email: email)
923 def get_by_nickname_or_email(nickname_or_email) do
924 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
927 def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname)
929 def get_or_fetch_by_nickname(nickname) do
930 with %User{} = user <- get_by_nickname(nickname) do
934 with [_nick, _domain] <- String.split(nickname, "@"),
935 {:ok, user} <- fetch_by_nickname(nickname) do
938 _e -> {:error, "not found " <> nickname}
943 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
944 def get_followers_query(%User{} = user, nil) do
945 User.Query.build(%{followers: user, deactivated: false})
948 def get_followers_query(user, page) do
950 |> get_followers_query(nil)
951 |> User.Query.paginate(page, 20)
954 @spec get_followers_query(User.t()) :: Ecto.Query.t()
955 def get_followers_query(user), do: get_followers_query(user, nil)
957 @spec get_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
958 def get_followers(user, page \\ nil) do
960 |> get_followers_query(page)
964 @spec get_external_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
965 def get_external_followers(user, page \\ nil) do
967 |> get_followers_query(page)
968 |> User.Query.build(%{external: true})
972 def get_followers_ids(user, page \\ nil) do
974 |> get_followers_query(page)
979 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
980 def get_friends_query(%User{} = user, nil) do
981 User.Query.build(%{friends: user, deactivated: false})
984 def get_friends_query(user, page) do
986 |> get_friends_query(nil)
987 |> User.Query.paginate(page, 20)
990 @spec get_friends_query(User.t()) :: Ecto.Query.t()
991 def get_friends_query(user), do: get_friends_query(user, nil)
993 def get_friends(user, page \\ nil) do
995 |> get_friends_query(page)
999 def get_friends_ap_ids(user) do
1001 |> get_friends_query(nil)
1002 |> select([u], u.ap_id)
1006 def get_friends_ids(user, page \\ nil) do
1008 |> get_friends_query(page)
1009 |> select([u], u.id)
1013 defdelegate get_follow_requests(user), to: FollowingRelationship
1015 def increase_note_count(%User{} = user) do
1017 |> where(id: ^user.id)
1018 |> update([u], inc: [note_count: 1])
1020 |> Repo.update_all([])
1022 {1, [user]} -> set_cache(user)
1027 def decrease_note_count(%User{} = user) do
1029 |> where(id: ^user.id)
1032 note_count: fragment("greatest(0, note_count - 1)")
1036 |> Repo.update_all([])
1038 {1, [user]} -> set_cache(user)
1043 def update_note_count(%User{} = user, note_count \\ nil) do
1048 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
1054 |> cast(%{note_count: note_count}, [:note_count])
1055 |> update_and_set_cache()
1058 @spec maybe_fetch_follow_information(User.t()) :: User.t()
1059 def maybe_fetch_follow_information(user) do
1060 with {:ok, user} <- fetch_follow_information(user) do
1064 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
1070 def fetch_follow_information(user) do
1071 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
1073 |> follow_information_changeset(info)
1074 |> update_and_set_cache()
1078 defp follow_information_changeset(user, params) do
1085 :hide_followers_count,
1090 def update_follower_count(%User{} = user) do
1091 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
1092 follower_count_query =
1093 User.Query.build(%{followers: user, deactivated: false})
1094 |> select([u], %{count: count(u.id)})
1097 |> where(id: ^user.id)
1098 |> join(:inner, [u], s in subquery(follower_count_query))
1100 set: [follower_count: s.count]
1103 |> Repo.update_all([])
1105 {1, [user]} -> set_cache(user)
1109 {:ok, maybe_fetch_follow_information(user)}
1113 @spec update_following_count(User.t()) :: User.t()
1114 def update_following_count(%User{local: false} = user) do
1115 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
1116 maybe_fetch_follow_information(user)
1122 def update_following_count(%User{local: true} = user) do
1123 following_count = FollowingRelationship.following_count(user)
1126 |> follow_information_changeset(%{following_count: following_count})
1130 def set_unread_conversation_count(%User{local: true} = user) do
1131 unread_query = Participation.unread_conversation_count_for_user(user)
1134 |> join(:inner, [u], p in subquery(unread_query))
1136 set: [unread_conversation_count: p.count]
1138 |> where([u], u.id == ^user.id)
1140 |> Repo.update_all([])
1142 {1, [user]} -> set_cache(user)
1147 def set_unread_conversation_count(user), do: {:ok, user}
1149 def increment_unread_conversation_count(conversation, %User{local: true} = user) do
1151 Participation.unread_conversation_count_for_user(user)
1152 |> where([p], p.conversation_id == ^conversation.id)
1155 |> join(:inner, [u], p in subquery(unread_query))
1157 inc: [unread_conversation_count: 1]
1159 |> where([u], u.id == ^user.id)
1160 |> where([u, p], p.count == 0)
1162 |> Repo.update_all([])
1164 {1, [user]} -> set_cache(user)
1169 def increment_unread_conversation_count(_, user), do: {:ok, user}
1171 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
1172 def get_users_from_set(ap_ids, local_only \\ true) do
1173 criteria = %{ap_id: ap_ids, deactivated: false}
1174 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
1176 User.Query.build(criteria)
1180 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
1181 def get_recipients_from_activity(%Activity{recipients: to}) do
1182 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
1186 @spec mute(User.t(), User.t(), boolean()) ::
1187 {:ok, list(UserRelationship.t())} | {:error, String.t()}
1188 def mute(%User{} = muter, %User{} = mutee, notifications? \\ true) do
1189 add_to_mutes(muter, mutee, notifications?)
1192 def unmute(%User{} = muter, %User{} = mutee) do
1193 remove_from_mutes(muter, mutee)
1196 def subscribe(%User{} = subscriber, %User{} = target) do
1197 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
1199 if blocks?(target, subscriber) and deny_follow_blocked do
1200 {:error, "Could not subscribe: #{target.nickname} is blocking you"}
1202 # Note: the relationship is inverse: subscriber acts as relationship target
1203 UserRelationship.create_inverse_subscription(target, subscriber)
1207 def subscribe(%User{} = subscriber, %{ap_id: ap_id}) do
1208 with %User{} = subscribee <- get_cached_by_ap_id(ap_id) do
1209 subscribe(subscriber, subscribee)
1213 def unsubscribe(%User{} = unsubscriber, %User{} = target) do
1214 # Note: the relationship is inverse: subscriber acts as relationship target
1215 UserRelationship.delete_inverse_subscription(target, unsubscriber)
1218 def unsubscribe(%User{} = unsubscriber, %{ap_id: ap_id}) do
1219 with %User{} = user <- get_cached_by_ap_id(ap_id) do
1220 unsubscribe(unsubscriber, user)
1224 def block(%User{} = blocker, %User{} = blocked) do
1225 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
1227 if following?(blocker, blocked) do
1228 {:ok, blocker, _} = unfollow(blocker, blocked)
1234 # clear any requested follows as well
1236 case CommonAPI.reject_follow_request(blocked, blocker) do
1237 {:ok, %User{} = updated_blocked} -> updated_blocked
1241 unsubscribe(blocked, blocker)
1243 if following?(blocked, blocker), do: unfollow(blocked, blocker)
1245 {:ok, blocker} = update_follower_count(blocker)
1246 {:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
1247 add_to_block(blocker, blocked)
1250 # helper to handle the block given only an actor's AP id
1251 def block(%User{} = blocker, %{ap_id: ap_id}) do
1252 block(blocker, get_cached_by_ap_id(ap_id))
1255 def unblock(%User{} = blocker, %User{} = blocked) do
1256 remove_from_block(blocker, blocked)
1259 # helper to handle the block given only an actor's AP id
1260 def unblock(%User{} = blocker, %{ap_id: ap_id}) do
1261 unblock(blocker, get_cached_by_ap_id(ap_id))
1264 def mutes?(nil, _), do: false
1265 def mutes?(%User{} = user, %User{} = target), do: mutes_user?(user, target)
1267 def mutes_user?(%User{} = user, %User{} = target) do
1268 UserRelationship.mute_exists?(user, target)
1271 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
1272 def muted_notifications?(nil, _), do: false
1274 def muted_notifications?(%User{} = user, %User{} = target),
1275 do: UserRelationship.notification_mute_exists?(user, target)
1277 def blocks?(nil, _), do: false
1279 def blocks?(%User{} = user, %User{} = target) do
1280 blocks_user?(user, target) ||
1281 (!User.following?(user, target) && blocks_domain?(user, target))
1284 def blocks_user?(%User{} = user, %User{} = target) do
1285 UserRelationship.block_exists?(user, target)
1288 def blocks_user?(_, _), do: false
1290 def blocks_domain?(%User{} = user, %User{} = target) do
1291 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
1292 %{host: host} = URI.parse(target.ap_id)
1293 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1296 def blocks_domain?(_, _), do: false
1298 def subscribed_to?(%User{} = user, %User{} = target) do
1299 # Note: the relationship is inverse: subscriber acts as relationship target
1300 UserRelationship.inverse_subscription_exists?(target, user)
1303 def subscribed_to?(%User{} = user, %{ap_id: ap_id}) do
1304 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1305 subscribed_to?(user, target)
1310 Returns map of outgoing (blocked, muted etc.) relationships' user AP IDs by relation type.
1311 E.g. `outgoing_relationships_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}`
1313 @spec outgoing_relationships_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())}
1314 def outgoing_relationships_ap_ids(_user, []), do: %{}
1316 def outgoing_relationships_ap_ids(nil, _relationship_types), do: %{}
1318 def outgoing_relationships_ap_ids(%User{} = user, relationship_types)
1319 when is_list(relationship_types) do
1322 |> assoc(:outgoing_relationships)
1323 |> join(:inner, [user_rel], u in assoc(user_rel, :target))
1324 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1325 |> select([user_rel, u], [user_rel.relationship_type, fragment("array_agg(?)", u.ap_id)])
1326 |> group_by([user_rel, u], user_rel.relationship_type)
1328 |> Enum.into(%{}, fn [k, v] -> {k, v} end)
1333 fn rel_type -> {rel_type, db_result[rel_type] || []} end
1337 def incoming_relationships_ungrouped_ap_ids(user, relationship_types, ap_ids \\ nil)
1339 def incoming_relationships_ungrouped_ap_ids(_user, [], _ap_ids), do: []
1341 def incoming_relationships_ungrouped_ap_ids(nil, _relationship_types, _ap_ids), do: []
1343 def incoming_relationships_ungrouped_ap_ids(%User{} = user, relationship_types, ap_ids)
1344 when is_list(relationship_types) do
1346 |> assoc(:incoming_relationships)
1347 |> join(:inner, [user_rel], u in assoc(user_rel, :source))
1348 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1349 |> maybe_filter_on_ap_id(ap_ids)
1350 |> select([user_rel, u], u.ap_id)
1355 defp maybe_filter_on_ap_id(query, ap_ids) when is_list(ap_ids) do
1356 where(query, [user_rel, u], u.ap_id in ^ap_ids)
1359 defp maybe_filter_on_ap_id(query, _ap_ids), do: query
1361 def deactivate_async(user, status \\ true) do
1362 BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
1365 def deactivate(user, status \\ true)
1367 def deactivate(users, status) when is_list(users) do
1368 Repo.transaction(fn ->
1369 for user <- users, do: deactivate(user, status)
1373 def deactivate(%User{} = user, status) do
1374 with {:ok, user} <- set_activation_status(user, status) do
1377 |> Enum.filter(& &1.local)
1378 |> Enum.each(fn follower ->
1379 follower |> update_following_count() |> set_cache()
1382 # Only update local user counts, remote will be update during the next pull.
1385 |> Enum.filter(& &1.local)
1386 |> Enum.each(&update_follower_count/1)
1392 def update_notification_settings(%User{} = user, settings) do
1394 |> cast(%{notification_settings: settings}, [])
1395 |> cast_embed(:notification_settings)
1396 |> validate_required([:notification_settings])
1397 |> update_and_set_cache()
1400 def delete(users) when is_list(users) do
1401 for user <- users, do: delete(user)
1404 def delete(%User{} = user) do
1405 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1408 def perform(:force_password_reset, user), do: force_password_reset(user)
1410 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1411 def perform(:delete, %User{} = user) do
1412 {:ok, _user} = ActivityPub.delete(user)
1414 # Remove all relationships
1417 |> Enum.each(fn follower ->
1418 ActivityPub.unfollow(follower, user)
1419 unfollow(follower, user)
1424 |> Enum.each(fn followed ->
1425 ActivityPub.unfollow(user, followed)
1426 unfollow(user, followed)
1429 delete_user_activities(user)
1430 invalidate_cache(user)
1434 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1436 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1437 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1438 when is_list(blocked_identifiers) do
1440 blocked_identifiers,
1441 fn blocked_identifier ->
1442 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1443 {:ok, _user_block} <- block(blocker, blocked),
1444 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1448 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1455 def perform(:follow_import, %User{} = follower, followed_identifiers)
1456 when is_list(followed_identifiers) do
1458 followed_identifiers,
1459 fn followed_identifier ->
1460 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1461 {:ok, follower} <- maybe_direct_follow(follower, followed),
1462 {:ok, _} <- ActivityPub.follow(follower, followed) do
1466 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1473 @spec external_users_query() :: Ecto.Query.t()
1474 def external_users_query do
1482 @spec external_users(keyword()) :: [User.t()]
1483 def external_users(opts \\ []) do
1485 external_users_query()
1486 |> select([u], struct(u, [:id, :ap_id]))
1490 do: where(query, [u], u.id > ^opts[:max_id]),
1495 do: limit(query, ^opts[:limit]),
1501 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1502 BackgroundWorker.enqueue("blocks_import", %{
1503 "blocker_id" => blocker.id,
1504 "blocked_identifiers" => blocked_identifiers
1508 def follow_import(%User{} = follower, followed_identifiers)
1509 when is_list(followed_identifiers) do
1510 BackgroundWorker.enqueue("follow_import", %{
1511 "follower_id" => follower.id,
1512 "followed_identifiers" => followed_identifiers
1516 def delete_user_activities(%User{ap_id: ap_id}) do
1518 |> Activity.Queries.by_actor()
1519 |> RepoStreamer.chunk_stream(50)
1520 |> Stream.each(fn activities -> Enum.each(activities, &delete_activity/1) end)
1524 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1526 |> Object.normalize()
1527 |> ActivityPub.delete()
1530 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1531 object = Object.normalize(activity)
1534 |> get_cached_by_ap_id()
1535 |> ActivityPub.unlike(object)
1538 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1539 object = Object.normalize(activity)
1542 |> get_cached_by_ap_id()
1543 |> ActivityPub.unannounce(object)
1546 defp delete_activity(_activity), do: "Doing nothing"
1548 def html_filter_policy(%User{no_rich_text: true}) do
1549 Pleroma.HTML.Scrubber.TwitterText
1552 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1554 def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
1556 def get_or_fetch_by_ap_id(ap_id) do
1557 user = get_cached_by_ap_id(ap_id)
1559 if !is_nil(user) and !needs_update?(user) do
1562 fetch_by_ap_id(ap_id)
1567 Creates an internal service actor by URI if missing.
1568 Optionally takes nickname for addressing.
1570 @spec get_or_create_service_actor_by_ap_id(String.t(), String.t()) :: User.t() | nil
1571 def get_or_create_service_actor_by_ap_id(uri, nickname) do
1573 case get_cached_by_ap_id(uri) do
1575 with {:error, %{errors: errors}} <- create_service_actor(uri, nickname) do
1576 Logger.error("Cannot create service actor: #{uri}/.\n#{inspect(errors)}")
1580 %User{invisible: false} = user ->
1590 @spec set_invisible(User.t()) :: {:ok, User.t()}
1591 defp set_invisible(user) do
1593 |> change(%{invisible: true})
1594 |> update_and_set_cache()
1597 @spec create_service_actor(String.t(), String.t()) ::
1598 {:ok, User.t()} | {:error, Ecto.Changeset.t()}
1599 defp create_service_actor(uri, nickname) do
1605 follower_address: uri <> "/followers"
1608 |> unique_constraint(:nickname)
1614 def public_key(%{source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}}) do
1617 |> :public_key.pem_decode()
1619 |> :public_key.pem_entry_decode()
1624 def public_key(_), do: {:error, "not found key"}
1626 def get_public_key_for_ap_id(ap_id) do
1627 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1628 {:ok, public_key} <- public_key(user) do
1635 defp blank?(""), do: nil
1636 defp blank?(n), do: n
1638 def insert_or_update_user(data) do
1640 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1641 |> remote_user_creation()
1642 |> Repo.insert(on_conflict: {:replace_all_except, [:id]}, conflict_target: :nickname)
1646 def ap_enabled?(%User{local: true}), do: true
1647 def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled
1648 def ap_enabled?(_), do: false
1650 @doc "Gets or fetch a user by uri or nickname."
1651 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1652 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1653 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1655 # wait a period of time and return newest version of the User structs
1656 # this is because we have synchronous follow APIs and need to simulate them
1657 # with an async handshake
1658 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1659 with %User{} = a <- get_cached_by_id(a.id),
1660 %User{} = b <- get_cached_by_id(b.id) do
1667 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1668 with :ok <- :timer.sleep(timeout),
1669 %User{} = a <- get_cached_by_id(a.id),
1670 %User{} = b <- get_cached_by_id(b.id) do
1677 def parse_bio(bio) when is_binary(bio) and bio != "" do
1679 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1683 def parse_bio(_), do: ""
1685 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1686 # TODO: get profile URLs other than user.ap_id
1687 profile_urls = [user.ap_id]
1690 |> CommonUtils.format_input("text/plain",
1691 mentions_format: :full,
1692 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1697 def parse_bio(_, _), do: ""
1699 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1700 Repo.transaction(fn ->
1701 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1705 def tag(nickname, tags) when is_binary(nickname),
1706 do: tag(get_by_nickname(nickname), tags)
1708 def tag(%User{} = user, tags),
1709 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1711 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1712 Repo.transaction(fn ->
1713 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1717 def untag(nickname, tags) when is_binary(nickname),
1718 do: untag(get_by_nickname(nickname), tags)
1720 def untag(%User{} = user, tags),
1721 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1723 defp update_tags(%User{} = user, new_tags) do
1724 {:ok, updated_user} =
1726 |> change(%{tags: new_tags})
1727 |> update_and_set_cache()
1732 defp normalize_tags(tags) do
1735 |> Enum.map(&String.downcase/1)
1738 defp local_nickname_regex do
1739 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1740 @extended_local_nickname_regex
1742 @strict_local_nickname_regex
1746 def local_nickname(nickname_or_mention) do
1749 |> String.split("@")
1753 def full_nickname(nickname_or_mention),
1754 do: String.trim_leading(nickname_or_mention, "@")
1756 def error_user(ap_id) do
1760 nickname: "erroruser@example.com",
1761 inserted_at: NaiveDateTime.utc_now()
1765 @spec all_superusers() :: [User.t()]
1766 def all_superusers do
1767 User.Query.build(%{super_users: true, local: true, deactivated: false})
1771 def muting_reblogs?(%User{} = user, %User{} = target) do
1772 UserRelationship.reblog_mute_exists?(user, target)
1775 def showing_reblogs?(%User{} = user, %User{} = target) do
1776 not muting_reblogs?(user, target)
1780 The function returns a query to get users with no activity for given interval of days.
1781 Inactive users are those who didn't read any notification, or had any activity where
1782 the user is the activity's actor, during `inactivity_threshold` days.
1783 Deactivated users will not appear in this list.
1787 iex> Pleroma.User.list_inactive_users()
1790 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1791 def list_inactive_users_query(inactivity_threshold \\ 7) do
1792 negative_inactivity_threshold = -inactivity_threshold
1793 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1794 # Subqueries are not supported in `where` clauses, join gets too complicated.
1795 has_read_notifications =
1796 from(n in Pleroma.Notification,
1797 where: n.seen == true,
1799 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1802 |> Pleroma.Repo.all()
1804 from(u in Pleroma.User,
1805 left_join: a in Pleroma.Activity,
1806 on: u.ap_id == a.actor,
1807 where: not is_nil(u.nickname),
1808 where: u.deactivated != ^true,
1809 where: u.id not in ^has_read_notifications,
1812 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1813 is_nil(max(a.inserted_at))
1818 Enable or disable email notifications for user
1822 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => false}}, "digest", true)
1823 Pleroma.User{email_notifications: %{"digest" => true}}
1825 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => true}}, "digest", false)
1826 Pleroma.User{email_notifications: %{"digest" => false}}
1828 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1829 {:ok, t()} | {:error, Ecto.Changeset.t()}
1830 def switch_email_notifications(user, type, status) do
1831 User.update_email_notifications(user, %{type => status})
1835 Set `last_digest_emailed_at` value for the user to current time
1837 @spec touch_last_digest_emailed_at(t()) :: t()
1838 def touch_last_digest_emailed_at(user) do
1839 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1841 {:ok, updated_user} =
1843 |> change(%{last_digest_emailed_at: now})
1844 |> update_and_set_cache()
1849 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1850 def toggle_confirmation(%User{} = user) do
1852 |> confirmation_changeset(need_confirmation: !user.confirmation_pending)
1853 |> update_and_set_cache()
1856 @spec toggle_confirmation([User.t()]) :: [{:ok, User.t()} | {:error, Changeset.t()}]
1857 def toggle_confirmation(users) do
1858 Enum.map(users, &toggle_confirmation/1)
1861 def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
1865 def get_mascot(%{mascot: mascot}) when is_nil(mascot) do
1866 # use instance-default
1867 config = Pleroma.Config.get([:assets, :mascots])
1868 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1869 mascot = Keyword.get(config, default_mascot)
1872 "id" => "default-mascot",
1873 "url" => mascot[:url],
1874 "preview_url" => mascot[:url],
1876 "mime_type" => mascot[:mime_type]
1881 def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
1883 def ensure_keys_present(%User{} = user) do
1884 with {:ok, pem} <- Keys.generate_rsa_pem() do
1886 |> cast(%{keys: pem}, [:keys])
1887 |> validate_required([:keys])
1888 |> update_and_set_cache()
1892 def get_ap_ids_by_nicknames(nicknames) do
1894 where: u.nickname in ^nicknames,
1900 defdelegate search(query, opts \\ []), to: User.Search
1902 defp put_password_hash(
1903 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1905 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1908 defp put_password_hash(changeset), do: changeset
1910 def is_internal_user?(%User{nickname: nil}), do: true
1911 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1912 def is_internal_user?(_), do: false
1914 # A hack because user delete activities have a fake id for whatever reason
1915 # TODO: Get rid of this
1916 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
1918 def get_delivered_users_by_object_id(object_id) do
1920 inner_join: delivery in assoc(u, :deliveries),
1921 where: delivery.object_id == ^object_id
1926 def change_email(user, email) do
1928 |> cast(%{email: email}, [:email])
1929 |> validate_required([:email])
1930 |> unique_constraint(:email)
1931 |> validate_format(:email, @email_regex)
1932 |> update_and_set_cache()
1935 # Internal function; public one is `deactivate/2`
1936 defp set_activation_status(user, deactivated) do
1938 |> cast(%{deactivated: deactivated}, [:deactivated])
1939 |> update_and_set_cache()
1942 def update_banner(user, banner) do
1944 |> cast(%{banner: banner}, [:banner])
1945 |> update_and_set_cache()
1948 def update_background(user, background) do
1950 |> cast(%{background: background}, [:background])
1951 |> update_and_set_cache()
1954 def update_source_data(user, source_data) do
1956 |> cast(%{source_data: source_data}, [:source_data])
1957 |> update_and_set_cache()
1960 def roles(%{is_moderator: is_moderator, is_admin: is_admin}) do
1963 moderator: is_moderator
1967 # ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``.
1968 # For example: [{"name": "Pronoun", "value": "she/her"}, …]
1969 def fields(%{fields: nil, source_data: %{"attachment" => attachment}}) do
1970 limit = Pleroma.Config.get([:instance, :max_remote_account_fields], 0)
1973 |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
1974 |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
1978 def fields(%{fields: nil}), do: []
1980 def fields(%{fields: fields}), do: fields
1982 def sanitized_fields(%User{} = user) do
1985 |> Enum.map(fn %{"name" => name, "value" => value} ->
1988 "value" => Pleroma.HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
1993 def validate_fields(changeset, remote? \\ false) do
1994 limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
1995 limit = Pleroma.Config.get([:instance, limit_name], 0)
1998 |> validate_length(:fields, max: limit)
1999 |> validate_change(:fields, fn :fields, fields ->
2000 if Enum.all?(fields, &valid_field?/1) do
2008 defp valid_field?(%{"name" => name, "value" => value}) do
2009 name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255)
2010 value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255)
2012 is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
2013 String.length(value) <= value_limit
2016 defp valid_field?(_), do: false
2018 defp truncate_field(%{"name" => name, "value" => value}) do
2020 String.split_at(name, Pleroma.Config.get([:instance, :account_field_name_length], 255))
2023 String.split_at(value, Pleroma.Config.get([:instance, :account_field_value_length], 255))
2025 %{"name" => name, "value" => value}
2028 def admin_api_update(user, params) do
2035 |> update_and_set_cache()
2038 @doc "Signs user out of all applications"
2039 def global_sign_out(user) do
2040 OAuth.Authorization.delete_user_authorizations(user)
2041 OAuth.Token.delete_user_tokens(user)
2044 def mascot_update(user, url) do
2046 |> cast(%{mascot: url}, [:mascot])
2047 |> validate_required([:mascot])
2048 |> update_and_set_cache()
2051 def mastodon_settings_update(user, settings) do
2053 |> cast(%{settings: settings}, [:settings])
2054 |> validate_required([:settings])
2055 |> update_and_set_cache()
2058 @spec confirmation_changeset(User.t(), keyword()) :: Changeset.t()
2059 def confirmation_changeset(user, need_confirmation: need_confirmation?) do
2061 if need_confirmation? do
2063 confirmation_pending: true,
2064 confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
2068 confirmation_pending: false,
2069 confirmation_token: nil
2073 cast(user, params, [:confirmation_pending, :confirmation_token])
2076 def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do
2077 if id not in user.pinned_activities do
2078 max_pinned_statuses = Pleroma.Config.get([:instance, :max_pinned_statuses], 0)
2079 params = %{pinned_activities: user.pinned_activities ++ [id]}
2082 |> cast(params, [:pinned_activities])
2083 |> validate_length(:pinned_activities,
2084 max: max_pinned_statuses,
2085 message: "You have already pinned the maximum number of statuses"
2090 |> update_and_set_cache()
2093 def remove_pinnned_activity(user, %Pleroma.Activity{id: id}) do
2094 params = %{pinned_activities: List.delete(user.pinned_activities, id)}
2097 |> cast(params, [:pinned_activities])
2098 |> update_and_set_cache()
2101 def update_email_notifications(user, settings) do
2102 email_notifications =
2103 user.email_notifications
2104 |> Map.merge(settings)
2105 |> Map.take(["digest"])
2107 params = %{email_notifications: email_notifications}
2108 fields = [:email_notifications]
2111 |> cast(params, fields)
2112 |> validate_required(fields)
2113 |> update_and_set_cache()
2116 defp set_domain_blocks(user, domain_blocks) do
2117 params = %{domain_blocks: domain_blocks}
2120 |> cast(params, [:domain_blocks])
2121 |> validate_required([:domain_blocks])
2122 |> update_and_set_cache()
2125 def block_domain(user, domain_blocked) do
2126 set_domain_blocks(user, Enum.uniq([domain_blocked | user.domain_blocks]))
2129 def unblock_domain(user, domain_blocked) do
2130 set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked))
2133 @spec add_to_block(User.t(), User.t()) ::
2134 {:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()}
2135 defp add_to_block(%User{} = user, %User{} = blocked) do
2136 UserRelationship.create_block(user, blocked)
2139 @spec add_to_block(User.t(), User.t()) ::
2140 {:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()}
2141 defp remove_from_block(%User{} = user, %User{} = blocked) do
2142 UserRelationship.delete_block(user, blocked)
2145 defp add_to_mutes(%User{} = user, %User{} = muted_user, notifications?) do
2146 with {:ok, user_mute} <- UserRelationship.create_mute(user, muted_user),
2147 {:ok, user_notification_mute} <-
2148 (notifications? && UserRelationship.create_notification_mute(user, muted_user)) ||
2150 {:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
2154 defp remove_from_mutes(user, %User{} = muted_user) do
2155 with {:ok, user_mute} <- UserRelationship.delete_mute(user, muted_user),
2156 {:ok, user_notification_mute} <-
2157 UserRelationship.delete_notification_mute(user, muted_user) do
2158 {:ok, [user_mute, user_notification_mute]}
2162 def set_invisible(user, invisible) do
2163 params = %{invisible: invisible}
2166 |> cast(params, [:invisible])
2167 |> validate_required([:invisible])
2168 |> update_and_set_cache()
2171 def sanitize_html(%User{} = user) do
2172 sanitize_html(user, nil)
2175 # User data that mastodon isn't filtering (treated as plaintext):
2178 def sanitize_html(%User{} = user, filter) do
2182 |> Enum.map(fn %{"name" => name, "value" => value} ->
2185 "value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
2190 |> Map.put(:bio, HTML.filter_tags(user.bio, filter))
2191 |> Map.put(:fields, fields)