1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.User do
10 import Ecto, only: [assoc: 2]
14 alias Pleroma.Activity
16 alias Pleroma.Conversation.Participation
17 alias Pleroma.Delivery
18 alias Pleroma.FollowingRelationship
19 alias Pleroma.Formatter
22 alias Pleroma.Notification
24 alias Pleroma.Registration
26 alias Pleroma.RepoStreamer
28 alias Pleroma.UserRelationship
30 alias Pleroma.Web.ActivityPub.ActivityPub
31 alias Pleroma.Web.ActivityPub.Utils
32 alias Pleroma.Web.CommonAPI
33 alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
34 alias Pleroma.Web.OAuth
35 alias Pleroma.Web.RelMe
36 alias Pleroma.Workers.BackgroundWorker
40 @type t :: %__MODULE__{}
41 @type account_status :: :active | :deactivated | :password_reset_pending | :confirmation_pending
42 @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
44 # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
45 @email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
47 @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
48 @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
50 # AP ID user relationships (blocks, mutes etc.)
51 # Format: [rel_type: [outgoing_rel: :outgoing_rel_target, incoming_rel: :incoming_rel_source]]
52 @user_relationships_config [
54 blocker_blocks: :blocked_users,
55 blockee_blocks: :blocker_users
58 muter_mutes: :muted_users,
59 mutee_mutes: :muter_users
62 reblog_muter_mutes: :reblog_muted_users,
63 reblog_mutee_mutes: :reblog_muter_users
66 notification_muter_mutes: :notification_muted_users,
67 notification_mutee_mutes: :notification_muter_users
69 # Note: `inverse_subscription` relationship is inverse: subscriber acts as relationship target
70 inverse_subscription: [
71 subscribee_subscriptions: :subscriber_users,
72 subscriber_subscriptions: :subscribee_users
78 field(:email, :string)
80 field(:nickname, :string)
81 field(:password_hash, :string)
82 field(:password, :string, virtual: true)
83 field(:password_confirmation, :string, virtual: true)
85 field(:ap_id, :string)
87 field(:local, :boolean, default: true)
88 field(:follower_address, :string)
89 field(:following_address, :string)
90 field(:search_rank, :float, virtual: true)
91 field(:search_type, :integer, virtual: true)
92 field(:tags, {:array, :string}, default: [])
93 field(:last_refreshed_at, :naive_datetime_usec)
94 field(:last_digest_emailed_at, :naive_datetime)
95 field(:banner, :map, default: %{})
96 field(:background, :map, default: %{})
97 field(:source_data, :map, default: %{})
98 field(:note_count, :integer, default: 0)
99 field(:follower_count, :integer, default: 0)
100 field(:following_count, :integer, default: 0)
101 field(:locked, :boolean, default: false)
102 field(:confirmation_pending, :boolean, default: false)
103 field(:password_reset_pending, :boolean, default: false)
104 field(:confirmation_token, :string, default: nil)
105 field(:default_scope, :string, default: "public")
106 field(:domain_blocks, {:array, :string}, default: [])
107 field(:deactivated, :boolean, default: false)
108 field(:no_rich_text, :boolean, default: false)
109 field(:ap_enabled, :boolean, default: false)
110 field(:is_moderator, :boolean, default: false)
111 field(:is_admin, :boolean, default: false)
112 field(:show_role, :boolean, default: true)
113 field(:settings, :map, default: nil)
114 field(:magic_key, :string, default: nil)
115 field(:uri, :string, default: nil)
116 field(:hide_followers_count, :boolean, default: false)
117 field(:hide_follows_count, :boolean, default: false)
118 field(:hide_followers, :boolean, default: false)
119 field(:hide_follows, :boolean, default: false)
120 field(:hide_favorites, :boolean, default: true)
121 field(:unread_conversation_count, :integer, default: 0)
122 field(:pinned_activities, {:array, :string}, default: [])
123 field(:email_notifications, :map, default: %{"digest" => false})
124 field(:mascot, :map, default: nil)
125 field(:emoji, {:array, :map}, default: [])
126 field(:pleroma_settings_store, :map, default: %{})
127 field(:fields, {:array, :map}, default: [])
128 field(:raw_fields, {:array, :map}, default: [])
129 field(:discoverable, :boolean, default: false)
130 field(:invisible, :boolean, default: false)
131 field(:allow_following_move, :boolean, default: true)
132 field(:skip_thread_containment, :boolean, default: false)
133 field(:actor_type, :string, default: "Person")
134 field(:also_known_as, {:array, :string}, default: [])
137 :notification_settings,
138 Pleroma.User.NotificationSetting,
142 has_many(:notifications, Notification)
143 has_many(:registrations, Registration)
144 has_many(:deliveries, Delivery)
146 has_many(:outgoing_relationships, UserRelationship, foreign_key: :source_id)
147 has_many(:incoming_relationships, UserRelationship, foreign_key: :target_id)
149 for {relationship_type,
151 {outgoing_relation, outgoing_relation_target},
152 {incoming_relation, incoming_relation_source}
153 ]} <- @user_relationships_config do
154 # Definitions of `has_many` relations: :blocker_blocks, :muter_mutes, :reblog_muter_mutes,
155 # :notification_muter_mutes, :subscribee_subscriptions
156 has_many(outgoing_relation, UserRelationship,
157 foreign_key: :source_id,
158 where: [relationship_type: relationship_type]
161 # Definitions of `has_many` relations: :blockee_blocks, :mutee_mutes, :reblog_mutee_mutes,
162 # :notification_mutee_mutes, :subscriber_subscriptions
163 has_many(incoming_relation, UserRelationship,
164 foreign_key: :target_id,
165 where: [relationship_type: relationship_type]
168 # Definitions of `has_many` relations: :blocked_users, :muted_users, :reblog_muted_users,
169 # :notification_muted_users, :subscriber_users
170 has_many(outgoing_relation_target, through: [outgoing_relation, :target])
172 # Definitions of `has_many` relations: :blocker_users, :muter_users, :reblog_muter_users,
173 # :notification_muter_users, :subscribee_users
174 has_many(incoming_relation_source, through: [incoming_relation, :source])
177 # `:blocks` is deprecated (replaced with `blocked_users` relation)
178 field(:blocks, {:array, :string}, default: [])
179 # `:mutes` is deprecated (replaced with `muted_users` relation)
180 field(:mutes, {:array, :string}, default: [])
181 # `:muted_reblogs` is deprecated (replaced with `reblog_muted_users` relation)
182 field(:muted_reblogs, {:array, :string}, default: [])
183 # `:muted_notifications` is deprecated (replaced with `notification_muted_users` relation)
184 field(:muted_notifications, {:array, :string}, default: [])
185 # `:subscribers` is deprecated (replaced with `subscriber_users` relation)
186 field(:subscribers, {:array, :string}, default: [])
191 for {_relationship_type, [{_outgoing_relation, outgoing_relation_target}, _]} <-
192 @user_relationships_config do
193 # `def blocked_users_relation/2`, `def muted_users_relation/2`,
194 # `def reblog_muted_users_relation/2`, `def notification_muted_users/2`,
195 # `def subscriber_users/2`
196 def unquote(:"#{outgoing_relation_target}_relation")(user, restrict_deactivated? \\ false) do
197 target_users_query = assoc(user, unquote(outgoing_relation_target))
199 if restrict_deactivated? do
200 restrict_deactivated(target_users_query)
206 # `def blocked_users/2`, `def muted_users/2`, `def reblog_muted_users/2`,
207 # `def notification_muted_users/2`, `def subscriber_users/2`
208 def unquote(outgoing_relation_target)(user, restrict_deactivated? \\ false) do
210 |> apply(unquote(:"#{outgoing_relation_target}_relation"), [
212 restrict_deactivated?
217 # `def blocked_users_ap_ids/2`, `def muted_users_ap_ids/2`, `def reblog_muted_users_ap_ids/2`,
218 # `def notification_muted_users_ap_ids/2`, `def subscriber_users_ap_ids/2`
219 def unquote(:"#{outgoing_relation_target}_ap_ids")(user, restrict_deactivated? \\ false) do
221 |> apply(unquote(:"#{outgoing_relation_target}_relation"), [
223 restrict_deactivated?
225 |> select([u], u.ap_id)
230 @doc "Returns status account"
231 @spec account_status(User.t()) :: account_status()
232 def account_status(%User{deactivated: true}), do: :deactivated
233 def account_status(%User{password_reset_pending: true}), do: :password_reset_pending
235 def account_status(%User{confirmation_pending: true}) do
236 case Config.get([:instance, :account_activation_required]) do
237 true -> :confirmation_pending
242 def account_status(%User{}), do: :active
244 @spec visible_for?(User.t(), User.t() | nil) :: boolean()
245 def visible_for?(user, for_user \\ nil)
247 def visible_for?(%User{invisible: true}, _), do: false
249 def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
251 def visible_for?(%User{} = user, for_user) do
252 account_status(user) == :active || superuser?(for_user)
255 def visible_for?(_, _), do: false
257 @spec superuser?(User.t()) :: boolean()
258 def superuser?(%User{local: true, is_admin: true}), do: true
259 def superuser?(%User{local: true, is_moderator: true}), do: true
260 def superuser?(_), do: false
262 @spec invisible?(User.t()) :: boolean()
263 def invisible?(%User{invisible: true}), do: true
264 def invisible?(_), do: false
266 def avatar_url(user, options \\ []) do
268 %{"url" => [%{"href" => href} | _]} -> href
269 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
273 def banner_url(user, options \\ []) do
275 %{"url" => [%{"href" => href} | _]} -> href
276 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
280 def ap_id(%User{nickname: nickname}), do: "#{Web.base_url()}/users/#{nickname}"
282 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
283 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
285 @spec ap_following(User.t()) :: String.t()
286 def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
287 def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
289 def follow_state(%User{} = user, %User{} = target) do
290 case Utils.fetch_latest_follow(user, target) do
291 %{data: %{"state" => state}} -> state
292 # Ideally this would be nil, but then Cachex does not commit the value
297 def get_cached_follow_state(user, target) do
298 key = "follow_state:#{user.ap_id}|#{target.ap_id}"
299 Cachex.fetch!(:user_cache, key, fn _ -> {:commit, follow_state(user, target)} end)
302 @spec set_follow_state_cache(String.t(), String.t(), String.t()) :: {:ok | :error, boolean()}
303 def set_follow_state_cache(user_ap_id, target_ap_id, state) do
304 Cachex.put(:user_cache, "follow_state:#{user_ap_id}|#{target_ap_id}", state)
307 @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
308 def restrict_deactivated(query) do
309 from(u in query, where: u.deactivated != ^true)
312 defdelegate following_count(user), to: FollowingRelationship
314 defp truncate_fields_param(params) do
315 if Map.has_key?(params, :fields) do
316 Map.put(params, :fields, Enum.map(params[:fields], &truncate_field/1))
322 defp truncate_if_exists(params, key, max_length) do
323 if Map.has_key?(params, key) and is_binary(params[key]) do
324 {value, _chopped} = String.split_at(params[key], max_length)
325 Map.put(params, key, value)
331 def remote_user_creation(params) do
332 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
333 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
337 |> truncate_if_exists(:name, name_limit)
338 |> truncate_if_exists(:bio, bio_limit)
339 |> truncate_fields_param()
359 :hide_followers_count,
370 |> validate_required([:name, :ap_id])
371 |> unique_constraint(:nickname)
372 |> validate_format(:nickname, @email_regex)
373 |> validate_length(:bio, max: bio_limit)
374 |> validate_length(:name, max: name_limit)
375 |> validate_fields(true)
377 case params[:source_data] do
378 %{"followers" => followers, "following" => following} ->
380 |> put_change(:follower_address, followers)
381 |> put_change(:following_address, following)
384 followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
385 put_change(changeset, :follower_address, followers)
389 def update_changeset(struct, params \\ %{}) do
390 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
391 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
406 :hide_followers_count,
409 :allow_following_move,
412 :skip_thread_containment,
415 :pleroma_settings_store,
421 |> unique_constraint(:nickname)
422 |> validate_format(:nickname, local_nickname_regex())
423 |> validate_length(:bio, max: bio_limit)
424 |> validate_length(:name, min: 1, max: name_limit)
426 |> put_change_if_present(:bio, &{:ok, parse_bio(&1, struct)})
427 |> put_change_if_present(:avatar, &put_upload(&1, :avatar))
428 |> put_change_if_present(:banner, &put_upload(&1, :banner))
429 |> put_change_if_present(:background, &put_upload(&1, :background))
430 |> put_change_if_present(
431 :pleroma_settings_store,
432 &{:ok, Map.merge(struct.pleroma_settings_store, &1)}
434 |> validate_fields(false)
437 defp put_fields(changeset) do
438 if raw_fields = get_change(changeset, :raw_fields) do
441 |> Enum.filter(fn %{"name" => n} -> n != "" end)
445 |> Enum.map(fn f -> Map.update!(f, "value", &parse_fields(&1)) end)
448 |> put_change(:raw_fields, raw_fields)
449 |> put_change(:fields, fields)
455 defp parse_fields(value) do
457 |> Formatter.linkify(mentions_format: :full)
461 defp put_change_if_present(changeset, map_field, value_function) do
462 if value = get_change(changeset, map_field) do
463 with {:ok, new_value} <- value_function.(value) do
464 put_change(changeset, map_field, new_value)
473 defp put_upload(value, type) do
474 with %Plug.Upload{} <- value,
475 {:ok, object} <- ActivityPub.upload(value, type: type) do
480 def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
481 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
482 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
484 params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
486 params = if remote?, do: truncate_fields_param(params), else: params
508 :allow_following_move,
510 :hide_followers_count,
516 |> unique_constraint(:nickname)
517 |> validate_format(:nickname, local_nickname_regex())
518 |> validate_length(:bio, max: bio_limit)
519 |> validate_length(:name, max: name_limit)
520 |> validate_fields(remote?)
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])
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()}
691 def maybe_direct_follow(%User{} = follower, %User{local: true, locked: true} = followed) do
692 follow(follower, followed, "pending")
695 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
696 follow(follower, followed)
699 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
700 if not ap_enabled?(followed) do
701 follow(follower, followed)
707 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
708 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
709 def follow_all(follower, followeds) do
711 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
712 |> Enum.each(&follow(follower, &1, "accept"))
717 defdelegate following(user), to: FollowingRelationship
719 def follow(%User{} = follower, %User{} = followed, state \\ "accept") do
720 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
723 followed.deactivated ->
724 {:error, "Could not follow user: #{followed.nickname} is deactivated."}
726 deny_follow_blocked and blocks?(followed, follower) ->
727 {:error, "Could not follow user: #{followed.nickname} blocked you."}
730 FollowingRelationship.follow(follower, followed, state)
732 {:ok, _} = update_follower_count(followed)
735 |> update_following_count()
740 def unfollow(%User{ap_id: ap_id}, %User{ap_id: ap_id}) do
741 {:error, "Not subscribed!"}
744 def unfollow(%User{} = follower, %User{} = followed) do
745 case get_follow_state(follower, followed) do
746 state when state in ["accept", "pending"] ->
747 FollowingRelationship.unfollow(follower, followed)
748 {:ok, followed} = update_follower_count(followed)
752 |> update_following_count()
755 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
758 {:error, "Not subscribed!"}
762 defdelegate following?(follower, followed), to: FollowingRelationship
764 def get_follow_state(%User{} = follower, %User{} = following) do
765 following_relationship = FollowingRelationship.get(follower, following)
767 case {following_relationship, following.local} do
769 case Utils.fetch_latest_follow(follower, following) do
770 %{data: %{"state" => state}} when state in ["pending", "accept"] -> state
774 {%{state: state}, _} ->
782 def locked?(%User{} = user) do
787 Repo.get_by(User, id: id)
790 def get_by_ap_id(ap_id) do
791 Repo.get_by(User, ap_id: ap_id)
794 def get_all_by_ap_id(ap_ids) do
795 from(u in __MODULE__,
796 where: u.ap_id in ^ap_ids
801 def get_all_by_ids(ids) do
802 from(u in __MODULE__, where: u.id in ^ids)
806 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
807 # of the ap_id and the domain and tries to get that user
808 def get_by_guessed_nickname(ap_id) do
809 domain = URI.parse(ap_id).host
810 name = List.last(String.split(ap_id, "/"))
811 nickname = "#{name}@#{domain}"
813 get_cached_by_nickname(nickname)
816 def set_cache({:ok, user}), do: set_cache(user)
817 def set_cache({:error, err}), do: {:error, err}
819 def set_cache(%User{} = user) do
820 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
821 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
825 def update_and_set_cache(struct, params) do
827 |> update_changeset(params)
828 |> update_and_set_cache()
831 def update_and_set_cache(changeset) do
832 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
837 def invalidate_cache(user) do
838 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
839 Cachex.del(:user_cache, "nickname:#{user.nickname}")
842 @spec get_cached_by_ap_id(String.t()) :: User.t() | nil
843 def get_cached_by_ap_id(ap_id) do
844 key = "ap_id:#{ap_id}"
846 with {:ok, nil} <- Cachex.get(:user_cache, key),
847 user when not is_nil(user) <- get_by_ap_id(ap_id),
848 {:ok, true} <- Cachex.put(:user_cache, key, user) do
856 def get_cached_by_id(id) do
860 Cachex.fetch!(:user_cache, key, fn _ ->
864 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
865 {:commit, user.ap_id}
871 get_cached_by_ap_id(ap_id)
874 def get_cached_by_nickname(nickname) do
875 key = "nickname:#{nickname}"
877 Cachex.fetch!(:user_cache, key, fn ->
878 case get_or_fetch_by_nickname(nickname) do
879 {:ok, user} -> {:commit, user}
880 {:error, _error} -> {:ignore, nil}
885 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
886 restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
889 is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
890 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
892 restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
893 get_cached_by_nickname(nickname_or_id)
895 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
896 get_cached_by_nickname(nickname_or_id)
903 def get_by_nickname(nickname) do
904 Repo.get_by(User, nickname: nickname) ||
905 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
906 Repo.get_by(User, nickname: local_nickname(nickname))
910 def get_by_email(email), do: Repo.get_by(User, email: email)
912 def get_by_nickname_or_email(nickname_or_email) do
913 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
916 def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname)
918 def get_or_fetch_by_nickname(nickname) do
919 with %User{} = user <- get_by_nickname(nickname) do
923 with [_nick, _domain] <- String.split(nickname, "@"),
924 {:ok, user} <- fetch_by_nickname(nickname) do
927 _e -> {:error, "not found " <> nickname}
932 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
933 def get_followers_query(%User{} = user, nil) do
934 User.Query.build(%{followers: user, deactivated: false})
937 def get_followers_query(user, page) do
939 |> get_followers_query(nil)
940 |> User.Query.paginate(page, 20)
943 @spec get_followers_query(User.t()) :: Ecto.Query.t()
944 def get_followers_query(user), do: get_followers_query(user, nil)
946 @spec get_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
947 def get_followers(user, page \\ nil) do
949 |> get_followers_query(page)
953 @spec get_external_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
954 def get_external_followers(user, page \\ nil) do
956 |> get_followers_query(page)
957 |> User.Query.build(%{external: true})
961 def get_followers_ids(user, page \\ nil) do
963 |> get_followers_query(page)
968 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
969 def get_friends_query(%User{} = user, nil) do
970 User.Query.build(%{friends: user, deactivated: false})
973 def get_friends_query(user, page) do
975 |> get_friends_query(nil)
976 |> User.Query.paginate(page, 20)
979 @spec get_friends_query(User.t()) :: Ecto.Query.t()
980 def get_friends_query(user), do: get_friends_query(user, nil)
982 def get_friends(user, page \\ nil) do
984 |> get_friends_query(page)
988 def get_friends_ap_ids(user) do
990 |> get_friends_query(nil)
991 |> select([u], u.ap_id)
995 def get_friends_ids(user, page \\ nil) do
997 |> get_friends_query(page)
1002 defdelegate get_follow_requests(user), to: FollowingRelationship
1004 def increase_note_count(%User{} = user) do
1006 |> where(id: ^user.id)
1007 |> update([u], inc: [note_count: 1])
1009 |> Repo.update_all([])
1011 {1, [user]} -> set_cache(user)
1016 def decrease_note_count(%User{} = user) do
1018 |> where(id: ^user.id)
1021 note_count: fragment("greatest(0, note_count - 1)")
1025 |> Repo.update_all([])
1027 {1, [user]} -> set_cache(user)
1032 def update_note_count(%User{} = user, note_count \\ nil) do
1037 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
1043 |> cast(%{note_count: note_count}, [:note_count])
1044 |> update_and_set_cache()
1047 @spec maybe_fetch_follow_information(User.t()) :: User.t()
1048 def maybe_fetch_follow_information(user) do
1049 with {:ok, user} <- fetch_follow_information(user) do
1053 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
1059 def fetch_follow_information(user) do
1060 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
1062 |> follow_information_changeset(info)
1063 |> update_and_set_cache()
1067 defp follow_information_changeset(user, params) do
1074 :hide_followers_count,
1079 def update_follower_count(%User{} = user) do
1080 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
1081 follower_count_query =
1082 User.Query.build(%{followers: user, deactivated: false})
1083 |> select([u], %{count: count(u.id)})
1086 |> where(id: ^user.id)
1087 |> join(:inner, [u], s in subquery(follower_count_query))
1089 set: [follower_count: s.count]
1092 |> Repo.update_all([])
1094 {1, [user]} -> set_cache(user)
1098 {:ok, maybe_fetch_follow_information(user)}
1102 @spec update_following_count(User.t()) :: User.t()
1103 def update_following_count(%User{local: false} = user) do
1104 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
1105 maybe_fetch_follow_information(user)
1111 def update_following_count(%User{local: true} = user) do
1112 following_count = FollowingRelationship.following_count(user)
1115 |> follow_information_changeset(%{following_count: following_count})
1119 def set_unread_conversation_count(%User{local: true} = user) do
1120 unread_query = Participation.unread_conversation_count_for_user(user)
1123 |> join(:inner, [u], p in subquery(unread_query))
1125 set: [unread_conversation_count: p.count]
1127 |> where([u], u.id == ^user.id)
1129 |> Repo.update_all([])
1131 {1, [user]} -> set_cache(user)
1136 def set_unread_conversation_count(user), do: {:ok, user}
1138 def increment_unread_conversation_count(conversation, %User{local: true} = user) do
1140 Participation.unread_conversation_count_for_user(user)
1141 |> where([p], p.conversation_id == ^conversation.id)
1144 |> join(:inner, [u], p in subquery(unread_query))
1146 inc: [unread_conversation_count: 1]
1148 |> where([u], u.id == ^user.id)
1149 |> where([u, p], p.count == 0)
1151 |> Repo.update_all([])
1153 {1, [user]} -> set_cache(user)
1158 def increment_unread_conversation_count(_, user), do: {:ok, user}
1160 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
1161 def get_users_from_set(ap_ids, local_only \\ true) do
1162 criteria = %{ap_id: ap_ids, deactivated: false}
1163 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
1165 User.Query.build(criteria)
1169 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
1170 def get_recipients_from_activity(%Activity{recipients: to}) do
1171 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
1175 @spec mute(User.t(), User.t(), boolean()) ::
1176 {:ok, list(UserRelationship.t())} | {:error, String.t()}
1177 def mute(%User{} = muter, %User{} = mutee, notifications? \\ true) do
1178 add_to_mutes(muter, mutee, notifications?)
1181 def unmute(%User{} = muter, %User{} = mutee) do
1182 remove_from_mutes(muter, mutee)
1185 def subscribe(%User{} = subscriber, %User{} = target) do
1186 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
1188 if blocks?(target, subscriber) and deny_follow_blocked do
1189 {:error, "Could not subscribe: #{target.nickname} is blocking you"}
1191 # Note: the relationship is inverse: subscriber acts as relationship target
1192 UserRelationship.create_inverse_subscription(target, subscriber)
1196 def subscribe(%User{} = subscriber, %{ap_id: ap_id}) do
1197 with %User{} = subscribee <- get_cached_by_ap_id(ap_id) do
1198 subscribe(subscriber, subscribee)
1202 def unsubscribe(%User{} = unsubscriber, %User{} = target) do
1203 # Note: the relationship is inverse: subscriber acts as relationship target
1204 UserRelationship.delete_inverse_subscription(target, unsubscriber)
1207 def unsubscribe(%User{} = unsubscriber, %{ap_id: ap_id}) do
1208 with %User{} = user <- get_cached_by_ap_id(ap_id) do
1209 unsubscribe(unsubscriber, user)
1213 def block(%User{} = blocker, %User{} = blocked) do
1214 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
1216 if following?(blocker, blocked) do
1217 {:ok, blocker, _} = unfollow(blocker, blocked)
1223 # clear any requested follows as well
1225 case CommonAPI.reject_follow_request(blocked, blocker) do
1226 {:ok, %User{} = updated_blocked} -> updated_blocked
1230 unsubscribe(blocked, blocker)
1232 if following?(blocked, blocker), do: unfollow(blocked, blocker)
1234 {:ok, blocker} = update_follower_count(blocker)
1235 {:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
1236 add_to_block(blocker, blocked)
1239 # helper to handle the block given only an actor's AP id
1240 def block(%User{} = blocker, %{ap_id: ap_id}) do
1241 block(blocker, get_cached_by_ap_id(ap_id))
1244 def unblock(%User{} = blocker, %User{} = blocked) do
1245 remove_from_block(blocker, blocked)
1248 # helper to handle the block given only an actor's AP id
1249 def unblock(%User{} = blocker, %{ap_id: ap_id}) do
1250 unblock(blocker, get_cached_by_ap_id(ap_id))
1253 def mutes?(nil, _), do: false
1254 def mutes?(%User{} = user, %User{} = target), do: mutes_user?(user, target)
1256 def mutes_user?(%User{} = user, %User{} = target) do
1257 UserRelationship.mute_exists?(user, target)
1260 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
1261 def muted_notifications?(nil, _), do: false
1263 def muted_notifications?(%User{} = user, %User{} = target),
1264 do: UserRelationship.notification_mute_exists?(user, target)
1266 def blocks?(nil, _), do: false
1268 def blocks?(%User{} = user, %User{} = target) do
1269 blocks_user?(user, target) ||
1270 (!User.following?(user, target) && blocks_domain?(user, target))
1273 def blocks_user?(%User{} = user, %User{} = target) do
1274 UserRelationship.block_exists?(user, target)
1277 def blocks_user?(_, _), do: false
1279 def blocks_domain?(%User{} = user, %User{} = target) do
1280 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
1281 %{host: host} = URI.parse(target.ap_id)
1282 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1285 def blocks_domain?(_, _), do: false
1287 def subscribed_to?(%User{} = user, %User{} = target) do
1288 # Note: the relationship is inverse: subscriber acts as relationship target
1289 UserRelationship.inverse_subscription_exists?(target, user)
1292 def subscribed_to?(%User{} = user, %{ap_id: ap_id}) do
1293 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1294 subscribed_to?(user, target)
1299 Returns map of outgoing (blocked, muted etc.) relationships' user AP IDs by relation type.
1300 E.g. `outgoing_relationships_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}`
1302 @spec outgoing_relationships_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())}
1303 def outgoing_relationships_ap_ids(_user, []), do: %{}
1305 def outgoing_relationships_ap_ids(nil, _relationship_types), do: %{}
1307 def outgoing_relationships_ap_ids(%User{} = user, relationship_types)
1308 when is_list(relationship_types) do
1311 |> assoc(:outgoing_relationships)
1312 |> join(:inner, [user_rel], u in assoc(user_rel, :target))
1313 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1314 |> select([user_rel, u], [user_rel.relationship_type, fragment("array_agg(?)", u.ap_id)])
1315 |> group_by([user_rel, u], user_rel.relationship_type)
1317 |> Enum.into(%{}, fn [k, v] -> {k, v} end)
1322 fn rel_type -> {rel_type, db_result[rel_type] || []} end
1326 def incoming_relationships_ungrouped_ap_ids(user, relationship_types, ap_ids \\ nil)
1328 def incoming_relationships_ungrouped_ap_ids(_user, [], _ap_ids), do: []
1330 def incoming_relationships_ungrouped_ap_ids(nil, _relationship_types, _ap_ids), do: []
1332 def incoming_relationships_ungrouped_ap_ids(%User{} = user, relationship_types, ap_ids)
1333 when is_list(relationship_types) do
1335 |> assoc(:incoming_relationships)
1336 |> join(:inner, [user_rel], u in assoc(user_rel, :source))
1337 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1338 |> maybe_filter_on_ap_id(ap_ids)
1339 |> select([user_rel, u], u.ap_id)
1344 defp maybe_filter_on_ap_id(query, ap_ids) when is_list(ap_ids) do
1345 where(query, [user_rel, u], u.ap_id in ^ap_ids)
1348 defp maybe_filter_on_ap_id(query, _ap_ids), do: query
1350 def deactivate_async(user, status \\ true) do
1351 BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
1354 def deactivate(user, status \\ true)
1356 def deactivate(users, status) when is_list(users) do
1357 Repo.transaction(fn ->
1358 for user <- users, do: deactivate(user, status)
1362 def deactivate(%User{} = user, status) do
1363 with {:ok, user} <- set_activation_status(user, status) do
1366 |> Enum.filter(& &1.local)
1367 |> Enum.each(fn follower ->
1368 follower |> update_following_count() |> set_cache()
1371 # Only update local user counts, remote will be update during the next pull.
1374 |> Enum.filter(& &1.local)
1375 |> Enum.each(&update_follower_count/1)
1381 def update_notification_settings(%User{} = user, settings) do
1383 |> cast(%{notification_settings: settings}, [])
1384 |> cast_embed(:notification_settings)
1385 |> validate_required([:notification_settings])
1386 |> update_and_set_cache()
1389 def delete(users) when is_list(users) do
1390 for user <- users, do: delete(user)
1393 def delete(%User{} = user) do
1394 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1397 def perform(:force_password_reset, user), do: force_password_reset(user)
1399 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1400 def perform(:delete, %User{} = user) do
1401 {:ok, _user} = ActivityPub.delete(user)
1403 # Remove all relationships
1406 |> Enum.each(fn follower ->
1407 ActivityPub.unfollow(follower, user)
1408 unfollow(follower, user)
1413 |> Enum.each(fn followed ->
1414 ActivityPub.unfollow(user, followed)
1415 unfollow(user, followed)
1418 delete_user_activities(user)
1422 |> change(%{deactivated: true, email: nil})
1423 |> update_and_set_cache()
1425 invalidate_cache(user)
1430 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1432 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1433 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1434 when is_list(blocked_identifiers) do
1436 blocked_identifiers,
1437 fn blocked_identifier ->
1438 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1439 {:ok, _user_block} <- block(blocker, blocked),
1440 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1444 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1451 def perform(:follow_import, %User{} = follower, followed_identifiers)
1452 when is_list(followed_identifiers) do
1454 followed_identifiers,
1455 fn followed_identifier ->
1456 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1457 {:ok, follower} <- maybe_direct_follow(follower, followed),
1458 {:ok, _} <- ActivityPub.follow(follower, followed) do
1462 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1469 @spec external_users_query() :: Ecto.Query.t()
1470 def external_users_query do
1478 @spec external_users(keyword()) :: [User.t()]
1479 def external_users(opts \\ []) do
1481 external_users_query()
1482 |> select([u], struct(u, [:id, :ap_id]))
1486 do: where(query, [u], u.id > ^opts[:max_id]),
1491 do: limit(query, ^opts[:limit]),
1497 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1498 BackgroundWorker.enqueue("blocks_import", %{
1499 "blocker_id" => blocker.id,
1500 "blocked_identifiers" => blocked_identifiers
1504 def follow_import(%User{} = follower, followed_identifiers)
1505 when is_list(followed_identifiers) do
1506 BackgroundWorker.enqueue("follow_import", %{
1507 "follower_id" => follower.id,
1508 "followed_identifiers" => followed_identifiers
1512 def delete_user_activities(%User{ap_id: ap_id}) do
1514 |> Activity.Queries.by_actor()
1515 |> RepoStreamer.chunk_stream(50)
1516 |> Stream.each(fn activities -> Enum.each(activities, &delete_activity/1) end)
1520 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1522 |> Object.normalize()
1523 |> ActivityPub.delete()
1526 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1527 object = Object.normalize(activity)
1530 |> get_cached_by_ap_id()
1531 |> ActivityPub.unlike(object)
1534 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1535 object = Object.normalize(activity)
1538 |> get_cached_by_ap_id()
1539 |> ActivityPub.unannounce(object)
1542 defp delete_activity(_activity), do: "Doing nothing"
1544 def html_filter_policy(%User{no_rich_text: true}) do
1545 Pleroma.HTML.Scrubber.TwitterText
1548 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1550 def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
1552 def get_or_fetch_by_ap_id(ap_id) do
1553 user = get_cached_by_ap_id(ap_id)
1555 if !is_nil(user) and !needs_update?(user) do
1558 fetch_by_ap_id(ap_id)
1563 Creates an internal service actor by URI if missing.
1564 Optionally takes nickname for addressing.
1566 @spec get_or_create_service_actor_by_ap_id(String.t(), String.t()) :: User.t() | nil
1567 def get_or_create_service_actor_by_ap_id(uri, nickname) do
1569 case get_cached_by_ap_id(uri) do
1571 with {:error, %{errors: errors}} <- create_service_actor(uri, nickname) do
1572 Logger.error("Cannot create service actor: #{uri}/.\n#{inspect(errors)}")
1576 %User{invisible: false} = user ->
1586 @spec set_invisible(User.t()) :: {:ok, User.t()}
1587 defp set_invisible(user) do
1589 |> change(%{invisible: true})
1590 |> update_and_set_cache()
1593 @spec create_service_actor(String.t(), String.t()) ::
1594 {:ok, User.t()} | {:error, Ecto.Changeset.t()}
1595 defp create_service_actor(uri, nickname) do
1601 follower_address: uri <> "/followers"
1604 |> unique_constraint(:nickname)
1610 def public_key(%{source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}}) do
1613 |> :public_key.pem_decode()
1615 |> :public_key.pem_entry_decode()
1620 def public_key(_), do: {:error, "not found key"}
1622 def get_public_key_for_ap_id(ap_id) do
1623 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1624 {:ok, public_key} <- public_key(user) do
1631 defp blank?(""), do: nil
1632 defp blank?(n), do: n
1634 def insert_or_update_user(data) do
1636 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1637 |> remote_user_creation()
1638 |> Repo.insert(on_conflict: {:replace_all_except, [:id]}, conflict_target: :nickname)
1642 def ap_enabled?(%User{local: true}), do: true
1643 def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled
1644 def ap_enabled?(_), do: false
1646 @doc "Gets or fetch a user by uri or nickname."
1647 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1648 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1649 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1651 # wait a period of time and return newest version of the User structs
1652 # this is because we have synchronous follow APIs and need to simulate them
1653 # with an async handshake
1654 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1655 with %User{} = a <- get_cached_by_id(a.id),
1656 %User{} = b <- get_cached_by_id(b.id) do
1663 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1664 with :ok <- :timer.sleep(timeout),
1665 %User{} = a <- get_cached_by_id(a.id),
1666 %User{} = b <- get_cached_by_id(b.id) do
1673 def parse_bio(bio) when is_binary(bio) and bio != "" do
1675 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1679 def parse_bio(_), do: ""
1681 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1682 # TODO: get profile URLs other than user.ap_id
1683 profile_urls = [user.ap_id]
1686 |> CommonUtils.format_input("text/plain",
1687 mentions_format: :full,
1688 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1693 def parse_bio(_, _), do: ""
1695 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1696 Repo.transaction(fn ->
1697 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1701 def tag(nickname, tags) when is_binary(nickname),
1702 do: tag(get_by_nickname(nickname), tags)
1704 def tag(%User{} = user, tags),
1705 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1707 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1708 Repo.transaction(fn ->
1709 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1713 def untag(nickname, tags) when is_binary(nickname),
1714 do: untag(get_by_nickname(nickname), tags)
1716 def untag(%User{} = user, tags),
1717 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1719 defp update_tags(%User{} = user, new_tags) do
1720 {:ok, updated_user} =
1722 |> change(%{tags: new_tags})
1723 |> update_and_set_cache()
1728 defp normalize_tags(tags) do
1731 |> Enum.map(&String.downcase/1)
1734 defp local_nickname_regex do
1735 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1736 @extended_local_nickname_regex
1738 @strict_local_nickname_regex
1742 def local_nickname(nickname_or_mention) do
1745 |> String.split("@")
1749 def full_nickname(nickname_or_mention),
1750 do: String.trim_leading(nickname_or_mention, "@")
1752 def error_user(ap_id) do
1756 nickname: "erroruser@example.com",
1757 inserted_at: NaiveDateTime.utc_now()
1761 @spec all_superusers() :: [User.t()]
1762 def all_superusers do
1763 User.Query.build(%{super_users: true, local: true, deactivated: false})
1767 def showing_reblogs?(%User{} = user, %User{} = target) do
1768 not UserRelationship.reblog_mute_exists?(user, target)
1772 The function returns a query to get users with no activity for given interval of days.
1773 Inactive users are those who didn't read any notification, or had any activity where
1774 the user is the activity's actor, during `inactivity_threshold` days.
1775 Deactivated users will not appear in this list.
1779 iex> Pleroma.User.list_inactive_users()
1782 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1783 def list_inactive_users_query(inactivity_threshold \\ 7) do
1784 negative_inactivity_threshold = -inactivity_threshold
1785 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1786 # Subqueries are not supported in `where` clauses, join gets too complicated.
1787 has_read_notifications =
1788 from(n in Pleroma.Notification,
1789 where: n.seen == true,
1791 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1794 |> Pleroma.Repo.all()
1796 from(u in Pleroma.User,
1797 left_join: a in Pleroma.Activity,
1798 on: u.ap_id == a.actor,
1799 where: not is_nil(u.nickname),
1800 where: u.deactivated != ^true,
1801 where: u.id not in ^has_read_notifications,
1804 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1805 is_nil(max(a.inserted_at))
1810 Enable or disable email notifications for user
1814 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => false}}, "digest", true)
1815 Pleroma.User{email_notifications: %{"digest" => true}}
1817 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => true}}, "digest", false)
1818 Pleroma.User{email_notifications: %{"digest" => false}}
1820 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1821 {:ok, t()} | {:error, Ecto.Changeset.t()}
1822 def switch_email_notifications(user, type, status) do
1823 User.update_email_notifications(user, %{type => status})
1827 Set `last_digest_emailed_at` value for the user to current time
1829 @spec touch_last_digest_emailed_at(t()) :: t()
1830 def touch_last_digest_emailed_at(user) do
1831 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1833 {:ok, updated_user} =
1835 |> change(%{last_digest_emailed_at: now})
1836 |> update_and_set_cache()
1841 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1842 def toggle_confirmation(%User{} = user) do
1844 |> confirmation_changeset(need_confirmation: !user.confirmation_pending)
1845 |> update_and_set_cache()
1848 @spec toggle_confirmation([User.t()]) :: [{:ok, User.t()} | {:error, Changeset.t()}]
1849 def toggle_confirmation(users) do
1850 Enum.map(users, &toggle_confirmation/1)
1853 def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
1857 def get_mascot(%{mascot: mascot}) when is_nil(mascot) do
1858 # use instance-default
1859 config = Pleroma.Config.get([:assets, :mascots])
1860 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1861 mascot = Keyword.get(config, default_mascot)
1864 "id" => "default-mascot",
1865 "url" => mascot[:url],
1866 "preview_url" => mascot[:url],
1868 "mime_type" => mascot[:mime_type]
1873 def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
1875 def ensure_keys_present(%User{} = user) do
1876 with {:ok, pem} <- Keys.generate_rsa_pem() do
1878 |> cast(%{keys: pem}, [:keys])
1879 |> validate_required([:keys])
1880 |> update_and_set_cache()
1884 def get_ap_ids_by_nicknames(nicknames) do
1886 where: u.nickname in ^nicknames,
1892 defdelegate search(query, opts \\ []), to: User.Search
1894 defp put_password_hash(
1895 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1897 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1900 defp put_password_hash(changeset), do: changeset
1902 def is_internal_user?(%User{nickname: nil}), do: true
1903 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1904 def is_internal_user?(_), do: false
1906 # A hack because user delete activities have a fake id for whatever reason
1907 # TODO: Get rid of this
1908 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
1910 def get_delivered_users_by_object_id(object_id) do
1912 inner_join: delivery in assoc(u, :deliveries),
1913 where: delivery.object_id == ^object_id
1918 def change_email(user, email) do
1920 |> cast(%{email: email}, [:email])
1921 |> validate_required([:email])
1922 |> unique_constraint(:email)
1923 |> validate_format(:email, @email_regex)
1924 |> update_and_set_cache()
1927 # Internal function; public one is `deactivate/2`
1928 defp set_activation_status(user, deactivated) do
1930 |> cast(%{deactivated: deactivated}, [:deactivated])
1931 |> update_and_set_cache()
1934 def update_banner(user, banner) do
1936 |> cast(%{banner: banner}, [:banner])
1937 |> update_and_set_cache()
1940 def update_background(user, background) do
1942 |> cast(%{background: background}, [:background])
1943 |> update_and_set_cache()
1946 def update_source_data(user, source_data) do
1948 |> cast(%{source_data: source_data}, [:source_data])
1949 |> update_and_set_cache()
1952 def roles(%{is_moderator: is_moderator, is_admin: is_admin}) do
1955 moderator: is_moderator
1959 # ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``.
1960 # For example: [{"name": "Pronoun", "value": "she/her"}, …]
1961 def fields(%{fields: nil, source_data: %{"attachment" => attachment}}) do
1962 limit = Pleroma.Config.get([:instance, :max_remote_account_fields], 0)
1965 |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
1966 |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
1970 def fields(%{fields: nil}), do: []
1972 def fields(%{fields: fields}), do: fields
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
2163 |> Enum.map(fn %{"name" => name, "value" => value} ->
2166 "value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
2171 |> Map.put(:bio, HTML.filter_tags(user.bio, filter))
2172 |> Map.put(:fields, fields)