1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.User do
7 A user, local or remote
14 import Ecto, only: [assoc: 2]
17 alias Pleroma.Activity
19 alias Pleroma.Conversation.Participation
20 alias Pleroma.Delivery
21 alias Pleroma.EctoType.ActivityPub.ObjectValidators
23 alias Pleroma.FollowingRelationship
24 alias Pleroma.Formatter
26 alias Pleroma.User.HashtagFollow
30 alias Pleroma.Notification
32 alias Pleroma.Registration
35 alias Pleroma.UserRelationship
36 alias Pleroma.Web.ActivityPub.ActivityPub
37 alias Pleroma.Web.ActivityPub.Builder
38 alias Pleroma.Web.ActivityPub.Pipeline
39 alias Pleroma.Web.ActivityPub.Utils
40 alias Pleroma.Web.CommonAPI
41 alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
42 alias Pleroma.Web.Endpoint
43 alias Pleroma.Web.OAuth
44 alias Pleroma.Web.RelMe
45 alias Pleroma.Workers.BackgroundWorker
49 @type t :: %__MODULE__{}
50 @type account_status ::
53 | :password_reset_pending
54 | :confirmation_pending
56 @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
58 # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
59 @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])?)*$/
61 @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
62 @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
64 # AP ID user relationships (blocks, mutes etc.)
65 # Format: [rel_type: [outgoing_rel: :outgoing_rel_target, incoming_rel: :incoming_rel_source]]
66 @user_relationships_config [
68 blocker_blocks: :blocked_users,
69 blockee_blocks: :blocker_users
72 muter_mutes: :muted_users,
73 mutee_mutes: :muter_users
76 reblog_muter_mutes: :reblog_muted_users,
77 reblog_mutee_mutes: :reblog_muter_users
80 notification_muter_mutes: :notification_muted_users,
81 notification_mutee_mutes: :notification_muter_users
83 # Note: `inverse_subscription` relationship is inverse: subscriber acts as relationship target
84 inverse_subscription: [
85 subscribee_subscriptions: :subscriber_users,
86 subscriber_subscriptions: :subscribee_users
90 @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
93 field(:bio, :string, default: "")
94 field(:raw_bio, :string)
95 field(:email, :string)
97 field(:nickname, :string)
98 field(:password_hash, :string)
99 field(:password, :string, virtual: true)
100 field(:password_confirmation, :string, virtual: true)
101 field(:keys, :string)
102 field(:public_key, :string)
103 field(:ap_id, :string)
104 field(:avatar, :map, default: %{})
105 field(:local, :boolean, default: true)
106 field(:follower_address, :string)
107 field(:following_address, :string)
108 field(:featured_address, :string)
109 field(:search_rank, :float, virtual: true)
110 field(:search_type, :integer, virtual: true)
111 field(:tags, {:array, :string}, default: [])
112 field(:last_refreshed_at, :naive_datetime_usec)
113 field(:last_digest_emailed_at, :naive_datetime)
114 field(:banner, :map, default: %{})
115 field(:background, :map, default: %{})
116 field(:note_count, :integer, default: 0)
117 field(:follower_count, :integer, default: 0)
118 field(:following_count, :integer, default: 0)
119 field(:is_locked, :boolean, default: false)
120 field(:is_confirmed, :boolean, default: true)
121 field(:password_reset_pending, :boolean, default: false)
122 field(:is_approved, :boolean, default: true)
123 field(:registration_reason, :string, default: nil)
124 field(:confirmation_token, :string, default: nil)
125 field(:default_scope, :string, default: "public")
126 field(:domain_blocks, {:array, :string}, default: [])
127 field(:is_active, :boolean, default: true)
128 field(:no_rich_text, :boolean, default: false)
129 field(:ap_enabled, :boolean, default: false)
130 field(:is_moderator, :boolean, default: false)
131 field(:is_admin, :boolean, default: false)
132 field(:show_role, :boolean, default: true)
133 field(:mastofe_settings, :map, default: nil)
134 field(:uri, ObjectValidators.Uri, default: nil)
135 field(:hide_followers_count, :boolean, default: false)
136 field(:hide_follows_count, :boolean, default: false)
137 field(:hide_followers, :boolean, default: false)
138 field(:hide_follows, :boolean, default: false)
139 field(:hide_favorites, :boolean, default: true)
140 field(:email_notifications, :map, default: %{"digest" => false})
141 field(:mascot, :map, default: nil)
142 field(:emoji, :map, default: %{})
143 field(:pleroma_settings_store, :map, default: %{})
144 field(:fields, {:array, :map}, default: [])
145 field(:raw_fields, {:array, :map}, default: [])
146 field(:is_discoverable, :boolean, default: false)
147 field(:invisible, :boolean, default: false)
148 field(:allow_following_move, :boolean, default: true)
149 field(:skip_thread_containment, :boolean, default: false)
150 field(:actor_type, :string, default: "Person")
151 field(:also_known_as, {:array, ObjectValidators.ObjectID}, default: [])
152 field(:inbox, :string)
153 field(:shared_inbox, :string)
154 field(:last_active_at, :naive_datetime)
155 field(:disclose_client, :boolean, default: true)
156 field(:pinned_objects, :map, default: %{})
157 field(:is_suggested, :boolean, default: false)
158 field(:last_status_at, :naive_datetime)
159 field(:language, :string)
160 field(:status_ttl_days, :integer, default: nil)
163 :notification_settings,
164 Pleroma.User.NotificationSetting,
168 has_many(:notifications, Notification)
169 has_many(:registrations, Registration)
170 has_many(:deliveries, Delivery)
172 has_many(:outgoing_relationships, UserRelationship, foreign_key: :source_id)
173 has_many(:incoming_relationships, UserRelationship, foreign_key: :target_id)
175 has_many(:frontend_profiles, Pleroma.Akkoma.FrontendSettingsProfile)
177 many_to_many(:followed_hashtags, Hashtag,
179 on_delete: :delete_all,
180 join_through: HashtagFollow
183 for {relationship_type,
185 {outgoing_relation, outgoing_relation_target},
186 {incoming_relation, incoming_relation_source}
187 ]} <- @user_relationships_config do
188 # Definitions of `has_many` relations: :blocker_blocks, :muter_mutes, :reblog_muter_mutes,
189 # :notification_muter_mutes, :subscribee_subscriptions
190 has_many(outgoing_relation, UserRelationship,
191 foreign_key: :source_id,
192 where: [relationship_type: relationship_type]
195 # Definitions of `has_many` relations: :blockee_blocks, :mutee_mutes, :reblog_mutee_mutes,
196 # :notification_mutee_mutes, :subscriber_subscriptions
197 has_many(incoming_relation, UserRelationship,
198 foreign_key: :target_id,
199 where: [relationship_type: relationship_type]
202 # Definitions of `has_many` relations: :blocked_users, :muted_users, :reblog_muted_users,
203 # :notification_muted_users, :subscriber_users
204 has_many(outgoing_relation_target, through: [outgoing_relation, :target])
206 # Definitions of `has_many` relations: :blocker_users, :muter_users, :reblog_muter_users,
207 # :notification_muter_users, :subscribee_users
208 has_many(incoming_relation_source, through: [incoming_relation, :source])
212 :multi_factor_authentication_settings,
220 for {_relationship_type, [{_outgoing_relation, outgoing_relation_target}, _]} <-
221 @user_relationships_config do
222 # `def blocked_users_relation/2`, `def muted_users_relation/2`,
223 # `def reblog_muted_users_relation/2`, `def notification_muted_users/2`,
224 # `def subscriber_users/2`
225 def unquote(:"#{outgoing_relation_target}_relation")(user, restrict_deactivated? \\ false) do
226 target_users_query = assoc(user, unquote(outgoing_relation_target))
228 if restrict_deactivated? do
230 |> User.Query.build(%{deactivated: false})
236 # `def blocked_users/2`, `def muted_users/2`, `def reblog_muted_users/2`,
237 # `def notification_muted_users/2`, `def subscriber_users/2`
238 def unquote(outgoing_relation_target)(user, restrict_deactivated? \\ false) do
240 |> apply(unquote(:"#{outgoing_relation_target}_relation"), [
242 restrict_deactivated?
247 # `def blocked_users_ap_ids/2`, `def muted_users_ap_ids/2`, `def reblog_muted_users_ap_ids/2`,
248 # `def notification_muted_users_ap_ids/2`, `def subscriber_users_ap_ids/2`
249 def unquote(:"#{outgoing_relation_target}_ap_ids")(user, restrict_deactivated? \\ false) do
251 |> apply(unquote(:"#{outgoing_relation_target}_relation"), [
253 restrict_deactivated?
255 |> select([u], u.ap_id)
260 def cached_blocked_users_ap_ids(user) do
261 @cachex.fetch!(:user_cache, "blocked_users_ap_ids:#{user.ap_id}", fn _ ->
262 blocked_users_ap_ids(user)
266 def cached_muted_users_ap_ids(user) do
267 @cachex.fetch!(:user_cache, "muted_users_ap_ids:#{user.ap_id}", fn _ ->
268 muted_users_ap_ids(user)
272 defdelegate following_count(user), to: FollowingRelationship
273 defdelegate following(user), to: FollowingRelationship
274 defdelegate following?(follower, followed), to: FollowingRelationship
275 defdelegate following_ap_ids(user), to: FollowingRelationship
276 defdelegate get_follow_requests_query(user), to: FollowingRelationship
278 def get_follow_requests(user) do
279 get_follow_requests_query(user)
283 defdelegate search(query, opts \\ []), to: User.Search
286 Dumps Flake Id to SQL-compatible format (16-byte UUID).
287 E.g. "9pQtDGXuq4p3VlcJEm" -> <<0, 0, 1, 110, 179, 218, 42, 92, 213, 41, 44, 227, 95, 213, 0, 0>>
289 def binary_id(source_id) when is_binary(source_id) do
290 with {:ok, dumped_id} <- FlakeId.Ecto.CompatType.dump(source_id) do
297 def binary_id(source_ids) when is_list(source_ids) do
298 Enum.map(source_ids, &binary_id/1)
301 def binary_id(%User{} = user), do: binary_id(user.id)
303 @doc "Returns status account"
304 @spec account_status(User.t()) :: account_status()
305 def account_status(%User{is_active: false}), do: :deactivated
306 def account_status(%User{password_reset_pending: true}), do: :password_reset_pending
307 def account_status(%User{local: true, is_approved: false}), do: :approval_pending
308 def account_status(%User{local: true, is_confirmed: false}), do: :confirmation_pending
309 def account_status(%User{}), do: :active
311 @spec visible_for(User.t(), User.t() | nil) ::
314 | :restricted_unauthenticated
316 | :confirmation_pending
317 def visible_for(user, for_user \\ nil)
319 def visible_for(%User{invisible: true}, _), do: :invisible
321 def visible_for(%User{id: user_id}, %User{id: user_id}), do: :visible
323 def visible_for(%User{} = user, nil) do
324 if restrict_unauthenticated?(user) do
325 :restrict_unauthenticated
327 visible_account_status(user)
331 def visible_for(%User{} = user, for_user) do
332 if superuser?(for_user) do
335 visible_account_status(user)
339 def visible_for(_, _), do: :invisible
341 defp restrict_unauthenticated?(%User{local: true}) do
342 Config.restrict_unauthenticated_access?(:profiles, :local)
345 defp restrict_unauthenticated?(%User{local: _}) do
346 Config.restrict_unauthenticated_access?(:profiles, :remote)
349 defp visible_account_status(user) do
350 status = account_status(user)
352 if status in [:active, :password_reset_pending] do
359 @spec superuser?(User.t()) :: boolean()
360 def superuser?(%User{local: true, is_admin: true}), do: true
361 def superuser?(%User{local: true, is_moderator: true}), do: true
362 def superuser?(_), do: false
364 @spec invisible?(User.t()) :: boolean()
365 def invisible?(%User{invisible: true}), do: true
366 def invisible?(_), do: false
368 def avatar_url(user, options \\ []) do
369 default = Config.get([:assets, :default_user_avatar], "#{Endpoint.url()}/images/avi.png")
370 do_optional_url(user.avatar, default, options)
373 def banner_url(user, options \\ []) do
374 do_optional_url(user.banner, "#{Endpoint.url()}/images/banner.png", options)
377 defp do_optional_url(field, default, options \\ []) do
379 %{"url" => [%{"href" => href} | _]} when is_binary(href) ->
383 unless options[:no_default], do: default
387 # Should probably be renamed or removed
388 @spec ap_id(User.t()) :: String.t()
389 def ap_id(%User{nickname: nickname}), do: "#{Endpoint.url()}/users/#{nickname}"
391 @spec ap_followers(User.t()) :: String.t()
392 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
393 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
395 @spec ap_following(User.t()) :: String.t()
396 def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
397 def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
399 @spec ap_featured_collection(User.t()) :: String.t()
400 def ap_featured_collection(%User{featured_address: fa}) when is_binary(fa), do: fa
402 def ap_featured_collection(%User{} = user), do: "#{ap_id(user)}/collections/featured"
404 defp truncate_fields_param(params) do
405 if Map.has_key?(params, :fields) do
406 Map.put(params, :fields, Enum.map(params[:fields], &truncate_field/1))
412 defp truncate_if_exists(params, key, max_length) do
413 if Map.has_key?(params, key) and is_binary(params[key]) do
414 {value, _chopped} = String.split_at(params[key], max_length)
415 Map.put(params, key, value)
421 defp fix_follower_address(%{follower_address: _, following_address: _} = params), do: params
423 defp fix_follower_address(%{nickname: nickname} = params),
424 do: Map.put(params, :follower_address, ap_followers(%User{nickname: nickname}))
426 defp fix_follower_address(params), do: params
428 def remote_user_changeset(struct \\ %User{local: false}, params) do
429 bio_limit = Config.get([:instance, :user_bio_length], 5000)
430 name_limit = Config.get([:instance, :user_name_length], 100)
433 case params[:name] do
434 name when is_binary(name) and byte_size(name) > 0 -> name
435 _ -> params[:nickname]
440 |> Map.put(:name, name)
441 |> Map.put_new(:last_refreshed_at, NaiveDateTime.utc_now())
442 |> truncate_if_exists(:name, name_limit)
443 |> truncate_if_exists(:bio, bio_limit)
444 |> truncate_fields_param()
445 |> fix_follower_address()
469 :hide_followers_count,
481 |> cast(params, [:name], empty_values: [])
482 |> validate_required([:ap_id])
483 |> validate_required([:name], trim: false)
484 |> unique_constraint(:nickname)
485 |> validate_format(:nickname, @email_regex)
486 |> validate_length(:bio, max: bio_limit)
487 |> validate_length(:name, max: name_limit)
488 |> validate_fields(true, struct)
489 |> validate_non_local()
492 defp validate_non_local(cng) do
493 local? = get_field(cng, :local)
497 |> add_error(:local, "User is local, can't update with this changeset.")
503 def update_changeset(struct, params \\ %{}) do
504 bio_limit = Config.get([:instance, :user_bio_length], 5000)
505 name_limit = Config.get([:instance, :user_name_length], 100)
525 :hide_followers_count,
528 :allow_following_move,
532 :skip_thread_containment,
535 :pleroma_settings_store,
542 |> unique_constraint(:nickname)
543 |> validate_format(:nickname, local_nickname_regex())
544 |> validate_length(:bio, max: bio_limit)
545 |> validate_length(:name, min: 1, max: name_limit)
546 |> validate_inclusion(:actor_type, ["Person", "Service"])
547 |> validate_number(:status_ttl_days, greater_than: 0)
550 |> put_change_if_present(:bio, &{:ok, parse_bio(&1, struct)})
551 |> put_change_if_present(:avatar, &put_upload(&1, :avatar))
552 |> put_change_if_present(:banner, &put_upload(&1, :banner))
553 |> put_change_if_present(:background, &put_upload(&1, :background))
554 |> put_change_if_present(
555 :pleroma_settings_store,
556 &{:ok, Map.merge(struct.pleroma_settings_store, &1)}
558 |> validate_fields(false, struct)
561 defp put_fields(changeset) do
562 # These fields are inconsistent in tests when it comes to binary/atom keys
563 if raw_fields = get_change(changeset, :raw_fields) do
567 %{name: name, value: value} ->
568 %{"name" => name, "value" => value}
570 %{"name" => _} = field ->
573 |> Enum.filter(fn %{"name" => n} -> n != "" end)
577 |> Enum.map(fn f -> Map.update!(f, "value", &parse_fields(&1)) end)
580 |> put_change(:raw_fields, raw_fields)
581 |> put_change(:fields, fields)
587 defp parse_fields(value) do
589 |> Formatter.linkify(mentions_format: :full)
593 defp put_emoji(changeset) do
594 emojified_fields = [:bio, :name, :raw_fields]
596 if Enum.any?(changeset.changes, fn {k, _} -> k in emojified_fields end) do
597 bio = Emoji.Formatter.get_emoji_map(get_field(changeset, :bio))
598 name = Emoji.Formatter.get_emoji_map(get_field(changeset, :name))
600 emoji = Map.merge(bio, name)
604 |> get_field(:raw_fields)
605 |> Enum.reduce(emoji, fn x, acc ->
606 Map.merge(acc, Emoji.Formatter.get_emoji_map(x["name"] <> x["value"]))
609 put_change(changeset, :emoji, emoji)
615 defp put_change_if_present(changeset, map_field, value_function) do
616 with {:ok, value} <- fetch_change(changeset, map_field),
617 {:ok, new_value} <- value_function.(value) do
618 put_change(changeset, map_field, new_value)
620 {:error, :file_too_large} ->
621 Ecto.Changeset.validate_change(changeset, map_field, fn map_field, _value ->
622 [{map_field, "file is too large"}]
630 defp put_upload(value, type) do
631 with %Plug.Upload{} <- value,
632 {:ok, object} <- ActivityPub.upload(value, type: type) do
637 def update_as_admin_changeset(struct, params) do
639 |> update_changeset(params)
640 |> cast(params, [:email])
641 |> delete_change(:also_known_as)
642 |> unique_constraint(:email)
643 |> validate_format(:email, @email_regex)
644 |> validate_inclusion(:actor_type, ["Person", "Service"])
647 @spec update_as_admin(User.t(), map()) :: {:ok, User.t()} | {:error, Changeset.t()}
648 def update_as_admin(user, params) do
649 params = Map.put(params, "password_confirmation", params["password"])
650 changeset = update_as_admin_changeset(user, params)
652 if params["password"] do
653 reset_password(user, changeset, params)
655 User.update_and_set_cache(changeset)
659 def password_update_changeset(struct, params) do
661 |> cast(params, [:password, :password_confirmation])
662 |> validate_required([:password, :password_confirmation])
663 |> validate_confirmation(:password)
664 |> put_password_hash()
665 |> put_change(:password_reset_pending, false)
668 @spec reset_password(User.t(), map()) :: {:ok, User.t()} | {:error, Changeset.t()}
669 def reset_password(%User{} = user, params) do
670 reset_password(user, user, params)
673 def reset_password(%User{id: user_id} = user, struct, params) do
676 |> Multi.update(:user, password_update_changeset(struct, params))
677 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
678 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
680 case Repo.transaction(multi) do
681 {:ok, %{user: user} = _} -> set_cache(user)
682 {:error, _, changeset, _} -> {:error, changeset}
686 def update_password_reset_pending(user, value) do
689 |> put_change(:password_reset_pending, value)
690 |> update_and_set_cache()
693 def force_password_reset_async(user) do
694 BackgroundWorker.enqueue("force_password_reset", %{"user_id" => user.id})
697 @spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
698 def force_password_reset(user), do: update_password_reset_pending(user, true)
700 # Used to auto-register LDAP accounts which won't have a password hash stored locally
701 def register_changeset_ldap(struct, params = %{password: password})
702 when is_nil(password) do
704 if Map.has_key?(params, :email) do
705 Map.put_new(params, :email, params[:email])
716 |> validate_required([:name, :nickname])
717 |> unique_constraint(:nickname)
718 |> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames]))
719 |> validate_format(:nickname, local_nickname_regex())
721 |> unique_constraint(:ap_id)
722 |> put_following_and_follower_and_featured_address()
726 @spec register_changeset(User.t(), map(), keyword()) :: Changeset.t()
727 def register_changeset(%User{} = struct, params \\ %{}, opts \\ []) do
728 bio_limit = Config.get([:instance, :user_bio_length], 5000)
729 name_limit = Config.get([:instance, :user_name_length], 100)
730 reason_limit = Config.get([:instance, :registration_reason_length], 500)
733 if is_nil(opts[:confirmed]) do
734 !Config.get([:instance, :account_activation_required])
740 if is_nil(opts[:approved]) do
741 !Config.get([:instance, :account_approval_required])
747 |> confirmation_changeset(set_confirmation: confirmed?)
748 |> approval_changeset(set_approval: approved?)
756 :password_confirmation,
758 :registration_reason,
761 |> validate_required([:name, :nickname, :password, :password_confirmation])
762 |> validate_confirmation(:password)
763 |> unique_constraint(:email)
764 |> validate_format(:email, @email_regex)
765 |> validate_change(:email, fn :email, email ->
767 Config.get([User, :email_blacklist])
768 |> Enum.all?(fn blacklisted_domain ->
769 !String.ends_with?(email, ["@" <> blacklisted_domain, "." <> blacklisted_domain])
772 if valid?, do: [], else: [email: "Invalid email"]
774 |> unique_constraint(:nickname)
775 |> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames]))
776 |> validate_format(:nickname, local_nickname_regex())
777 |> validate_length(:bio, max: bio_limit)
778 |> validate_length(:name, min: 1, max: name_limit)
779 |> validate_length(:registration_reason, max: reason_limit)
780 |> maybe_validate_required_email(opts[:external])
783 |> unique_constraint(:ap_id)
784 |> put_following_and_follower_and_featured_address()
788 def maybe_validate_required_email(changeset, true), do: changeset
790 def maybe_validate_required_email(changeset, _) do
791 if Config.get([:instance, :account_activation_required]) do
792 validate_required(changeset, [:email])
798 def put_ap_id(changeset) do
799 ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
800 put_change(changeset, :ap_id, ap_id)
803 def put_following_and_follower_and_featured_address(changeset) do
804 user = %User{nickname: get_field(changeset, :nickname)}
805 followers = ap_followers(user)
806 following = ap_following(user)
807 featured = ap_featured_collection(user)
810 |> put_change(:follower_address, followers)
811 |> put_change(:following_address, following)
812 |> put_change(:featured_address, featured)
815 defp put_private_key(changeset) do
816 {:ok, pem} = Keys.generate_rsa_pem()
817 put_change(changeset, :keys, pem)
820 defp autofollow_users(user) do
821 candidates = Config.get([:instance, :autofollowed_nicknames])
824 User.Query.build(%{nickname: candidates, local: true, is_active: true})
827 follow_all(user, autofollowed_users)
830 defp autofollowing_users(user) do
831 candidates = Config.get([:instance, :autofollowing_nicknames])
833 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
835 |> Enum.each(&follow(&1, user, :follow_accept))
840 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
841 @spec register(Changeset.t()) :: {:ok, User.t()} | {:error, any} | nil
842 def register(%Ecto.Changeset{} = changeset) do
843 with {:ok, user} <- Repo.insert(changeset) do
844 post_register_action(user)
848 @spec post_register_action(User.t()) :: {:error, any} | {:ok, User.t()}
849 def post_register_action(%User{is_confirmed: false} = user) do
850 with {:ok, _} <- maybe_send_confirmation_email(user) do
855 def post_register_action(%User{is_approved: false} = user) do
856 with {:ok, _} <- send_user_approval_email(user),
857 {:ok, _} <- send_admin_approval_emails(user) do
862 def post_register_action(%User{is_approved: true, is_confirmed: true} = user) do
863 with {:ok, user} <- autofollow_users(user),
864 {:ok, _} <- autofollowing_users(user),
865 {:ok, user} <- set_cache(user),
866 {:ok, _} <- maybe_send_registration_email(user),
867 {:ok, _} <- maybe_send_welcome_email(user),
868 {:ok, _} <- maybe_send_welcome_message(user) do
873 defp send_user_approval_email(user) do
875 |> Pleroma.Emails.UserEmail.approval_pending_email()
876 |> Pleroma.Emails.Mailer.deliver_async()
881 defp send_admin_approval_emails(user) do
883 |> Enum.filter(fn user -> not is_nil(user.email) end)
884 |> Enum.each(fn superuser ->
886 |> Pleroma.Emails.AdminEmail.new_unapproved_registration(user)
887 |> Pleroma.Emails.Mailer.deliver_async()
893 defp maybe_send_welcome_message(user) do
894 if User.WelcomeMessage.enabled?() do
895 User.WelcomeMessage.post_message(user)
902 defp maybe_send_welcome_email(%User{email: email} = user) when is_binary(email) do
903 if User.WelcomeEmail.enabled?() do
904 User.WelcomeEmail.send_email(user)
911 defp maybe_send_welcome_email(_), do: {:ok, :noop}
913 @spec maybe_send_confirmation_email(User.t()) :: {:ok, :enqueued | :noop}
914 def maybe_send_confirmation_email(%User{is_confirmed: false, email: email} = user)
915 when is_binary(email) do
916 if Config.get([:instance, :account_activation_required]) do
917 send_confirmation_email(user)
924 def maybe_send_confirmation_email(_), do: {:ok, :noop}
926 @spec send_confirmation_email(Uset.t()) :: User.t()
927 def send_confirmation_email(%User{} = user) do
929 |> Pleroma.Emails.UserEmail.account_confirmation_email()
930 |> Pleroma.Emails.Mailer.deliver_async()
935 @spec maybe_send_registration_email(User.t()) :: {:ok, :enqueued | :noop}
936 defp maybe_send_registration_email(%User{email: email} = user) when is_binary(email) do
937 with false <- User.WelcomeEmail.enabled?(),
938 false <- Config.get([:instance, :account_activation_required], false),
939 false <- Config.get([:instance, :account_approval_required], false) do
941 |> Pleroma.Emails.UserEmail.successful_registration_email()
942 |> Pleroma.Emails.Mailer.deliver_async()
951 defp maybe_send_registration_email(_), do: {:ok, :noop}
953 def needs_update?(%User{local: true}), do: false
955 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
957 def needs_update?(%User{local: false} = user) do
958 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
961 def needs_update?(_), do: true
963 @spec maybe_direct_follow(User.t(), User.t()) ::
964 {:ok, User.t(), User.t()} | {:error, String.t()}
966 # "Locked" (self-locked) users demand explicit authorization of follow requests
967 def maybe_direct_follow(%User{} = follower, %User{local: true, is_locked: true} = followed) do
968 follow(follower, followed, :follow_pending)
971 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
972 follow(follower, followed)
975 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
976 if not ap_enabled?(followed) do
977 follow(follower, followed)
979 {:ok, follower, followed}
983 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
984 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
985 def follow_all(follower, followeds) do
987 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
988 |> Enum.each(&follow(follower, &1, :follow_accept))
993 def follow(%User{} = follower, %User{} = followed, state \\ :follow_accept) do
994 deny_follow_blocked = Config.get([:user, :deny_follow_blocked])
997 not followed.is_active ->
998 {:error, "Could not follow user: #{followed.nickname} is deactivated."}
1000 deny_follow_blocked and blocks?(followed, follower) ->
1001 {:error, "Could not follow user: #{followed.nickname} blocked you."}
1004 FollowingRelationship.follow(follower, followed, state)
1008 def unfollow(%User{ap_id: ap_id}, %User{ap_id: ap_id}) do
1009 {:error, "Not subscribed!"}
1012 @spec unfollow(User.t(), User.t()) :: {:ok, User.t(), Activity.t()} | {:error, String.t()}
1013 def unfollow(%User{} = follower, %User{} = followed) do
1014 case do_unfollow(follower, followed) do
1015 {:ok, follower, followed} ->
1016 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
1023 @spec do_unfollow(User.t(), User.t()) :: {:ok, User.t(), User.t()} | {:error, String.t()}
1024 defp do_unfollow(%User{} = follower, %User{} = followed) do
1025 case get_follow_state(follower, followed) do
1026 state when state in [:follow_pending, :follow_accept] ->
1027 FollowingRelationship.unfollow(follower, followed)
1030 {:error, "Not subscribed!"}
1034 @doc "Returns follow state as Pleroma.FollowingRelationship.State value"
1035 def get_follow_state(%User{} = follower, %User{} = following) do
1036 following_relationship = FollowingRelationship.get(follower, following)
1037 get_follow_state(follower, following, following_relationship)
1040 def get_follow_state(
1042 %User{} = following,
1043 following_relationship
1045 case {following_relationship, following.local} do
1047 case Utils.fetch_latest_follow(follower, following) do
1048 %Activity{data: %{"state" => state}} when state in ["pending", "accept"] ->
1049 FollowingRelationship.state_to_enum(state)
1055 {%{state: state}, _} ->
1063 def locked?(%User{} = user) do
1064 user.is_locked || false
1067 def get_by_id(id) do
1068 Repo.get_by(User, id: id)
1071 def get_by_ap_id(ap_id) do
1072 Repo.get_by(User, ap_id: ap_id)
1075 def get_all_by_ap_id(ap_ids) do
1076 from(u in __MODULE__,
1077 where: u.ap_id in ^ap_ids
1082 def get_all_by_ids(ids) do
1083 from(u in __MODULE__, where: u.id in ^ids)
1087 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
1088 # of the ap_id and the domain and tries to get that user
1089 def get_by_guessed_nickname(ap_id) do
1090 domain = URI.parse(ap_id).host
1091 name = List.last(String.split(ap_id, "/"))
1092 nickname = "#{name}@#{domain}"
1094 get_cached_by_nickname(nickname)
1101 ) :: {:ok, User.t()} | {:error, any}
1102 def set_cache({:ok, user}), do: set_cache(user)
1103 def set_cache({:error, err}), do: {:error, err}
1105 def set_cache(%User{} = user) do
1106 @cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
1107 @cachex.put(:user_cache, "nickname:#{user.nickname}", user)
1108 @cachex.put(:user_cache, "friends_ap_ids:#{user.nickname}", get_user_friends_ap_ids(user))
1112 @spec update_and_set_cache(User.t(), map()) :: {:ok, User.t()} | {:error, any}
1113 def update_and_set_cache(struct, params) do
1115 |> update_changeset(params)
1116 |> update_and_set_cache()
1119 @spec update_and_set_cache(Changeset.t()) :: {:ok, User.t()} | {:error, any}
1120 def update_and_set_cache(%{data: %Pleroma.User{} = user} = changeset) do
1121 was_superuser_before_update = User.superuser?(user)
1123 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
1126 |> maybe_remove_report_notifications(was_superuser_before_update)
1129 defp maybe_remove_report_notifications({:ok, %Pleroma.User{} = user} = result, true) do
1130 if not User.superuser?(user),
1131 do: user |> Notification.destroy_multiple_from_types(["pleroma:report"])
1136 defp maybe_remove_report_notifications(result, _) do
1140 def get_user_friends_ap_ids(user) do
1141 from(u in User.get_friends_query(user), select: u.ap_id)
1145 @spec get_cached_user_friends_ap_ids(User.t()) :: [String.t()]
1146 def get_cached_user_friends_ap_ids(user) do
1147 @cachex.fetch!(:user_cache, "friends_ap_ids:#{user.ap_id}", fn _ ->
1148 get_user_friends_ap_ids(user)
1152 def invalidate_cache(user) do
1153 @cachex.del(:user_cache, "ap_id:#{user.ap_id}")
1154 @cachex.del(:user_cache, "nickname:#{user.nickname}")
1155 @cachex.del(:user_cache, "friends_ap_ids:#{user.ap_id}")
1156 @cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
1157 @cachex.del(:user_cache, "muted_users_ap_ids:#{user.ap_id}")
1160 @spec get_cached_by_ap_id(String.t()) :: User.t() | nil
1161 def get_cached_by_ap_id(ap_id) do
1162 key = "ap_id:#{ap_id}"
1164 with {:ok, nil} <- @cachex.get(:user_cache, key),
1165 user when not is_nil(user) <- get_by_ap_id(ap_id),
1166 {:ok, true} <- @cachex.put(:user_cache, key, user) do
1174 @spec get_cached_by_id(String.t()) :: nil | Pleroma.User.t()
1175 def get_cached_by_id(id) do
1179 @cachex.fetch!(:user_cache, key, fn _ ->
1180 user = get_by_id(id)
1183 @cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
1184 {:commit, user.ap_id}
1190 get_cached_by_ap_id(ap_id)
1193 def get_cached_by_nickname(nickname) do
1194 key = "nickname:#{nickname}"
1196 @cachex.fetch!(:user_cache, key, fn _ ->
1197 case get_or_fetch_by_nickname(nickname) do
1198 {:ok, user} -> {:commit, user}
1199 {:error, _error} -> {:ignore, nil}
1204 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
1205 restrict_to_local = Config.get([:instance, :limit_to_local_content])
1208 is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
1209 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
1211 restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
1212 get_cached_by_nickname(nickname_or_id)
1214 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
1215 get_cached_by_nickname(nickname_or_id)
1222 @spec get_by_nickname(String.t()) :: User.t() | nil
1223 def get_by_nickname(nickname) do
1224 Repo.get_by(User, nickname: nickname) ||
1225 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
1226 Repo.get_by(User, nickname: local_nickname(nickname))
1230 def get_by_email(email), do: Repo.get_by(User, email: email)
1232 def get_by_nickname_or_email(nickname_or_email) do
1233 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
1236 def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname)
1238 def get_or_fetch_by_nickname(nickname) do
1239 with %User{} = user <- get_by_nickname(nickname) do
1243 with [_nick, _domain] <- String.split(nickname, "@"),
1244 {:ok, user} <- fetch_by_nickname(nickname) do
1247 _e -> {:error, "not found " <> nickname}
1252 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
1253 def get_followers_query(%User{} = user, nil) do
1254 User.Query.build(%{followers: user, is_active: true})
1257 def get_followers_query(%User{} = user, page) do
1259 |> get_followers_query(nil)
1260 |> User.Query.paginate(page, 20)
1263 @spec get_followers_query(User.t()) :: Ecto.Query.t()
1264 def get_followers_query(%User{} = user), do: get_followers_query(user, nil)
1266 @spec get_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
1267 def get_followers(%User{} = user, page \\ nil) do
1269 |> get_followers_query(page)
1273 @spec get_external_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
1274 def get_external_followers(%User{} = user, page \\ nil) do
1276 |> get_followers_query(page)
1277 |> User.Query.build(%{external: true})
1281 def get_followers_ids(%User{} = user, page \\ nil) do
1283 |> get_followers_query(page)
1284 |> select([u], u.id)
1288 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
1289 def get_friends_query(%User{} = user, nil) do
1290 User.Query.build(%{friends: user, deactivated: false})
1293 def get_friends_query(%User{} = user, page) do
1295 |> get_friends_query(nil)
1296 |> User.Query.paginate(page, 20)
1299 @spec get_friends_query(User.t()) :: Ecto.Query.t()
1300 def get_friends_query(%User{} = user), do: get_friends_query(user, nil)
1302 def get_friends(%User{} = user, page \\ nil) do
1304 |> get_friends_query(page)
1308 def get_friends_ap_ids(%User{} = user) do
1310 |> get_friends_query(nil)
1311 |> select([u], u.ap_id)
1315 def get_friends_ids(%User{} = user, page \\ nil) do
1317 |> get_friends_query(page)
1318 |> select([u], u.id)
1322 def increase_note_count(%User{} = user) do
1324 |> where(id: ^user.id)
1325 |> update([u], inc: [note_count: 1])
1327 |> Repo.update_all([])
1329 {1, [user]} -> set_cache(user)
1334 def decrease_note_count(%User{} = user) do
1336 |> where(id: ^user.id)
1339 note_count: fragment("greatest(0, note_count - 1)")
1343 |> Repo.update_all([])
1345 {1, [user]} -> set_cache(user)
1350 def update_note_count(%User{} = user, note_count \\ nil) do
1355 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
1361 |> cast(%{note_count: note_count}, [:note_count])
1362 |> update_and_set_cache()
1365 @spec maybe_fetch_follow_information(User.t()) :: User.t()
1366 def maybe_fetch_follow_information(user) do
1367 with {:ok, user} <- fetch_follow_information(user) do
1371 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
1377 def fetch_follow_information(user) do
1378 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
1380 |> follow_information_changeset(info)
1381 |> update_and_set_cache()
1385 defp follow_information_changeset(user, params) do
1392 :hide_followers_count,
1397 @spec update_follower_count(User.t()) :: {:ok, User.t()}
1398 def update_follower_count(%User{} = user) do
1399 if user.local or !Config.get([:instance, :external_user_synchronization]) do
1400 follower_count = FollowingRelationship.follower_count(user)
1403 |> follow_information_changeset(%{follower_count: follower_count})
1404 |> update_and_set_cache
1406 {:ok, maybe_fetch_follow_information(user)}
1410 @spec update_following_count(User.t()) :: {:ok, User.t()}
1411 def update_following_count(%User{local: false} = user) do
1412 if Config.get([:instance, :external_user_synchronization]) do
1413 {:ok, maybe_fetch_follow_information(user)}
1419 def update_following_count(%User{local: true} = user) do
1420 following_count = FollowingRelationship.following_count(user)
1423 |> follow_information_changeset(%{following_count: following_count})
1424 |> update_and_set_cache()
1427 @spec get_users_from_set([String.t()], keyword()) :: [User.t()]
1428 def get_users_from_set(ap_ids, opts \\ []) do
1429 local_only = Keyword.get(opts, :local_only, true)
1430 criteria = %{ap_id: ap_ids, is_active: true}
1431 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
1433 User.Query.build(criteria)
1437 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
1438 def get_recipients_from_activity(%Activity{recipients: to, actor: actor}) do
1441 query = User.Query.build(%{recipients_from_activity: to, local: true, is_active: true})
1447 @spec mute(User.t(), User.t(), map()) ::
1448 {:ok, list(UserRelationship.t())} | {:error, String.t()}
1449 def mute(%User{} = muter, %User{} = mutee, params \\ %{}) do
1450 notifications? = Map.get(params, :notifications, true)
1451 expires_in = Map.get(params, :expires_in, 0)
1453 with {:ok, user_mute} <- UserRelationship.create_mute(muter, mutee),
1454 {:ok, user_notification_mute} <-
1455 (notifications? && UserRelationship.create_notification_mute(muter, mutee)) ||
1457 if expires_in > 0 do
1458 Pleroma.Workers.MuteExpireWorker.enqueue(
1460 %{"muter_id" => muter.id, "mutee_id" => mutee.id},
1461 schedule_in: expires_in
1465 @cachex.del(:user_cache, "muted_users_ap_ids:#{muter.ap_id}")
1467 {:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
1471 def unmute(%User{} = muter, %User{} = mutee) do
1472 with {:ok, user_mute} <- UserRelationship.delete_mute(muter, mutee),
1473 {:ok, user_notification_mute} <-
1474 UserRelationship.delete_notification_mute(muter, mutee) do
1475 @cachex.del(:user_cache, "muted_users_ap_ids:#{muter.ap_id}")
1476 {:ok, [user_mute, user_notification_mute]}
1480 def unmute(muter_id, mutee_id) do
1481 with {:muter, %User{} = muter} <- {:muter, User.get_by_id(muter_id)},
1482 {:mutee, %User{} = mutee} <- {:mutee, User.get_by_id(mutee_id)} do
1483 unmute(muter, mutee)
1485 {who, result} = error ->
1487 "User.unmute/2 failed. #{who}: #{result}, muter_id: #{muter_id}, mutee_id: #{mutee_id}"
1494 def subscribe(%User{} = subscriber, %User{} = target) do
1495 deny_follow_blocked = Config.get([:user, :deny_follow_blocked])
1497 if blocks?(target, subscriber) and deny_follow_blocked do
1498 {:error, "Could not subscribe: #{target.nickname} is blocking you"}
1500 # Note: the relationship is inverse: subscriber acts as relationship target
1501 UserRelationship.create_inverse_subscription(target, subscriber)
1505 def subscribe(%User{} = subscriber, %{ap_id: ap_id}) do
1506 with %User{} = subscribee <- get_cached_by_ap_id(ap_id) do
1507 subscribe(subscriber, subscribee)
1511 def unsubscribe(%User{} = unsubscriber, %User{} = target) do
1512 # Note: the relationship is inverse: subscriber acts as relationship target
1513 UserRelationship.delete_inverse_subscription(target, unsubscriber)
1516 def unsubscribe(%User{} = unsubscriber, %{ap_id: ap_id}) do
1517 with %User{} = user <- get_cached_by_ap_id(ap_id) do
1518 unsubscribe(unsubscriber, user)
1522 def block(%User{} = blocker, %User{} = blocked) do
1523 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
1525 if following?(blocker, blocked) do
1526 {:ok, blocker, _} = unfollow(blocker, blocked)
1532 # clear any requested follows from both sides as well
1534 case CommonAPI.reject_follow_request(blocked, blocker) do
1535 {:ok, %User{} = updated_blocked} -> updated_blocked
1540 case CommonAPI.reject_follow_request(blocker, blocked) do
1541 {:ok, %User{} = updated_blocker} -> updated_blocker
1545 unsubscribe(blocked, blocker)
1547 unfollowing_blocked = Config.get([:activitypub, :unfollow_blocked], true)
1548 if unfollowing_blocked && following?(blocked, blocker), do: unfollow(blocked, blocker)
1550 {:ok, blocker} = update_follower_count(blocker)
1551 {:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
1552 add_to_block(blocker, blocked)
1555 # helper to handle the block given only an actor's AP id
1556 def block(%User{} = blocker, %{ap_id: ap_id}) do
1557 block(blocker, get_cached_by_ap_id(ap_id))
1560 def unblock(%User{} = blocker, %User{} = blocked) do
1561 remove_from_block(blocker, blocked)
1564 # helper to handle the block given only an actor's AP id
1565 def unblock(%User{} = blocker, %{ap_id: ap_id}) do
1566 unblock(blocker, get_cached_by_ap_id(ap_id))
1569 def mutes?(nil, _), do: false
1570 def mutes?(%User{} = user, %User{} = target), do: mutes_user?(user, target)
1572 def mutes_user?(%User{} = user, %User{} = target) do
1573 UserRelationship.mute_exists?(user, target)
1576 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
1577 def muted_notifications?(nil, _), do: false
1579 def muted_notifications?(%User{} = user, %User{} = target),
1580 do: UserRelationship.notification_mute_exists?(user, target)
1582 def blocks?(nil, _), do: false
1584 def blocks?(%User{} = user, %User{} = target) do
1585 blocks_user?(user, target) ||
1586 (blocks_domain?(user, target) and not User.following?(user, target))
1589 def blocks_user?(%User{} = user, %User{} = target) do
1590 UserRelationship.block_exists?(user, target)
1593 def blocks_user?(_, _), do: false
1595 def blocks_domain?(%User{} = user, %User{} = target) do
1596 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
1597 %{host: host} = URI.parse(target.ap_id)
1598 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1601 def blocks_domain?(_, _), do: false
1603 def subscribed_to?(%User{} = user, %User{} = target) do
1604 # Note: the relationship is inverse: subscriber acts as relationship target
1605 UserRelationship.inverse_subscription_exists?(target, user)
1608 def subscribed_to?(%User{} = user, %{ap_id: ap_id}) do
1609 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1610 subscribed_to?(user, target)
1615 Returns map of outgoing (blocked, muted etc.) relationships' user AP IDs by relation type.
1616 E.g. `outgoing_relationships_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}`
1618 @spec outgoing_relationships_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())}
1619 def outgoing_relationships_ap_ids(_user, []), do: %{}
1621 def outgoing_relationships_ap_ids(nil, _relationship_types), do: %{}
1623 def outgoing_relationships_ap_ids(%User{} = user, relationship_types)
1624 when is_list(relationship_types) do
1627 |> assoc(:outgoing_relationships)
1628 |> join(:inner, [user_rel], u in assoc(user_rel, :target))
1629 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1630 |> select([user_rel, u], [user_rel.relationship_type, fragment("array_agg(?)", u.ap_id)])
1631 |> group_by([user_rel, u], user_rel.relationship_type)
1633 |> Enum.into(%{}, fn [k, v] -> {k, v} end)
1638 fn rel_type -> {rel_type, db_result[rel_type] || []} end
1642 def incoming_relationships_ungrouped_ap_ids(user, relationship_types, ap_ids \\ nil)
1644 def incoming_relationships_ungrouped_ap_ids(_user, [], _ap_ids), do: []
1646 def incoming_relationships_ungrouped_ap_ids(nil, _relationship_types, _ap_ids), do: []
1648 def incoming_relationships_ungrouped_ap_ids(%User{} = user, relationship_types, ap_ids)
1649 when is_list(relationship_types) do
1651 |> assoc(:incoming_relationships)
1652 |> join(:inner, [user_rel], u in assoc(user_rel, :source))
1653 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1654 |> maybe_filter_on_ap_id(ap_ids)
1655 |> select([user_rel, u], u.ap_id)
1660 defp maybe_filter_on_ap_id(query, ap_ids) when is_list(ap_ids) do
1661 where(query, [user_rel, u], u.ap_id in ^ap_ids)
1664 defp maybe_filter_on_ap_id(query, _ap_ids), do: query
1666 def set_activation_async(user, status \\ true) do
1667 BackgroundWorker.enqueue("user_activation", %{"user_id" => user.id, "status" => status})
1670 @spec set_activation([User.t()], boolean()) :: {:ok, User.t()} | {:error, Changeset.t()}
1671 def set_activation(users, status) when is_list(users) do
1672 Repo.transaction(fn ->
1673 for user <- users, do: set_activation(user, status)
1677 @spec set_activation(User.t(), boolean()) :: {:ok, User.t()} | {:error, Changeset.t()}
1678 def set_activation(%User{} = user, status) do
1679 with {:ok, user} <- set_activation_status(user, status) do
1682 |> Enum.filter(& &1.local)
1683 |> Enum.each(&set_cache(update_following_count(&1)))
1685 # Only update local user counts, remote will be update during the next pull.
1688 |> Enum.filter(& &1.local)
1689 |> Enum.each(&do_unfollow(user, &1))
1695 def approve(users) when is_list(users) do
1696 Repo.transaction(fn ->
1697 Enum.map(users, fn user ->
1698 with {:ok, user} <- approve(user), do: user
1703 def approve(%User{is_approved: false} = user) do
1704 with chg <- change(user, is_approved: true),
1705 {:ok, user} <- update_and_set_cache(chg) do
1706 post_register_action(user)
1711 def approve(%User{} = user), do: {:ok, user}
1713 def confirm(users) when is_list(users) do
1714 Repo.transaction(fn ->
1715 Enum.map(users, fn user ->
1716 with {:ok, user} <- confirm(user), do: user
1721 def confirm(%User{is_confirmed: false} = user) do
1722 with chg <- confirmation_changeset(user, set_confirmation: true),
1723 {:ok, user} <- update_and_set_cache(chg) do
1724 post_register_action(user)
1729 def confirm(%User{} = user), do: {:ok, user}
1731 def set_suggestion(users, is_suggested) when is_list(users) do
1732 Repo.transaction(fn ->
1733 Enum.map(users, fn user ->
1734 with {:ok, user} <- set_suggestion(user, is_suggested), do: user
1739 def set_suggestion(%User{is_suggested: is_suggested} = user, is_suggested), do: {:ok, user}
1741 def set_suggestion(%User{} = user, is_suggested) when is_boolean(is_suggested) do
1743 |> change(is_suggested: is_suggested)
1744 |> update_and_set_cache()
1747 def update_notification_settings(%User{} = user, settings) do
1749 |> cast(%{notification_settings: settings}, [])
1750 |> cast_embed(:notification_settings)
1751 |> validate_required([:notification_settings])
1752 |> update_and_set_cache()
1755 @spec purge_user_changeset(User.t()) :: Changeset.t()
1756 def purge_user_changeset(user) do
1757 # "Right to be forgotten"
1758 # https://gdpr.eu/right-to-be-forgotten/
1767 last_refreshed_at: nil,
1768 last_digest_emailed_at: nil,
1775 password_reset_pending: false,
1776 registration_reason: nil,
1777 confirmation_token: nil,
1781 is_moderator: false,
1783 mastofe_settings: nil,
1786 pleroma_settings_store: %{},
1789 is_discoverable: false,
1793 # nickname: preserved
1797 # Purge doesn't delete the user from the database.
1798 # It just nulls all its fields and deactivates it.
1799 # See `User.purge_user_changeset/1` above.
1800 defp purge(%User{} = user) do
1802 |> purge_user_changeset()
1803 |> update_and_set_cache()
1806 def delete(users) when is_list(users) do
1807 for user <- users, do: delete(user)
1810 def delete(%User{} = user) do
1811 # Purge the user immediately
1813 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1816 # *Actually* delete the user from the DB
1817 defp delete_from_db(%User{} = user) do
1818 invalidate_cache(user)
1822 # If the user never finalized their account, it's safe to delete them.
1823 defp maybe_delete_from_db(%User{local: true, is_confirmed: false} = user),
1824 do: delete_from_db(user)
1826 defp maybe_delete_from_db(%User{local: true, is_approved: false} = user),
1827 do: delete_from_db(user)
1829 defp maybe_delete_from_db(user), do: {:ok, user}
1831 def perform(:force_password_reset, user), do: force_password_reset(user)
1833 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1834 def perform(:delete, %User{} = user) do
1835 # Purge the user again, in case perform/2 is called directly
1838 # Remove all relationships
1841 |> Enum.each(fn follower ->
1842 ActivityPub.unfollow(follower, user)
1843 unfollow(follower, user)
1848 |> Enum.each(fn followed ->
1849 ActivityPub.unfollow(user, followed)
1850 unfollow(user, followed)
1853 delete_user_activities(user)
1854 delete_notifications_from_user_activities(user)
1855 delete_outgoing_pending_follow_requests(user)
1857 maybe_delete_from_db(user)
1860 def perform(:set_activation_async, user, status), do: set_activation(user, status)
1862 @spec external_users_query() :: Ecto.Query.t()
1863 def external_users_query do
1871 @spec external_users(keyword()) :: [User.t()]
1872 def external_users(opts \\ []) do
1874 external_users_query()
1875 |> select([u], struct(u, [:id, :ap_id]))
1879 do: where(query, [u], u.id > ^opts[:max_id]),
1884 do: limit(query, ^opts[:limit]),
1890 def delete_notifications_from_user_activities(%User{ap_id: ap_id}) do
1892 |> join(:inner, [n], activity in assoc(n, :activity))
1893 |> where([n, a], fragment("? = ?", a.actor, ^ap_id))
1894 |> Repo.delete_all()
1897 def delete_user_activities(%User{ap_id: ap_id} = user) do
1899 |> Activity.Queries.by_actor()
1900 |> Repo.chunk_stream(50, :batches)
1901 |> Stream.each(fn activities ->
1902 Enum.each(activities, fn activity -> delete_activity(activity, user) end)
1907 defp delete_activity(%{data: %{"type" => "Create", "object" => object}} = activity, user) do
1908 with {_, %Object{}} <- {:find_object, Object.get_by_ap_id(object)},
1909 {:ok, delete_data, _} <- Builder.delete(user, object) do
1910 Pipeline.common_pipeline(delete_data, local: user.local)
1912 {:find_object, nil} ->
1913 # We have the create activity, but not the object, it was probably pruned.
1914 # Insert a tombstone and try again
1915 with {:ok, tombstone_data, _} <- Builder.tombstone(user.ap_id, object),
1916 {:ok, _tombstone} <- Object.create(tombstone_data) do
1917 delete_activity(activity, user)
1921 Logger.error("Could not delete #{object} created by #{activity.data["ap_id"]}")
1922 Logger.error("Error: #{inspect(e)}")
1926 defp delete_activity(%{data: %{"type" => type}} = activity, user)
1927 when type in ["Like", "Announce"] do
1928 {:ok, undo, _} = Builder.undo(user, activity)
1929 Pipeline.common_pipeline(undo, local: user.local)
1932 defp delete_activity(_activity, _user), do: "Doing nothing"
1934 defp delete_outgoing_pending_follow_requests(user) do
1936 |> FollowingRelationship.outgoing_pending_follow_requests_query()
1937 |> Repo.delete_all()
1940 def html_filter_policy(%User{no_rich_text: true}) do
1941 Pleroma.HTML.Scrubber.TwitterText
1944 def html_filter_policy(_), do: Config.get([:markup, :scrub_policy])
1946 def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
1948 def get_or_fetch_by_ap_id(ap_id) do
1949 cached_user = get_cached_by_ap_id(ap_id)
1951 maybe_fetched_user = needs_update?(cached_user) && fetch_by_ap_id(ap_id)
1953 case {cached_user, maybe_fetched_user} do
1954 {_, {:ok, %User{} = user}} ->
1957 {%User{} = user, _} ->
1961 Logger.error("Could not fetch user #{ap_id}, #{inspect(e)}")
1962 {:error, :not_found}
1967 Creates an internal service actor by URI if missing.
1968 Optionally takes nickname for addressing.
1970 @spec get_or_create_service_actor_by_ap_id(String.t(), String.t()) :: User.t() | nil
1971 def get_or_create_service_actor_by_ap_id(uri, nickname) do
1973 case get_cached_by_ap_id(uri) do
1975 with {:error, %{errors: errors}} <- create_service_actor(uri, nickname) do
1976 Logger.error("Cannot create service actor: #{uri}/.\n#{inspect(errors)}")
1980 %User{invisible: false} = user ->
1990 @spec set_invisible(User.t()) :: {:ok, User.t()}
1991 defp set_invisible(user) do
1993 |> change(%{invisible: true})
1994 |> update_and_set_cache()
1997 @spec create_service_actor(String.t(), String.t()) ::
1998 {:ok, User.t()} | {:error, Ecto.Changeset.t()}
1999 defp create_service_actor(uri, nickname) do
2003 actor_type: "Application",
2006 follower_address: uri <> "/followers"
2009 |> put_private_key()
2010 |> unique_constraint(:nickname)
2015 def public_key(%{public_key: public_key_pem}) when is_binary(public_key_pem) do
2018 |> :public_key.pem_decode()
2020 |> :public_key.pem_entry_decode()
2025 def public_key(_), do: {:error, "key not found"}
2027 def get_public_key_for_ap_id(ap_id) do
2028 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
2029 {:ok, public_key} <- public_key(user) do
2036 def ap_enabled?(%User{local: true}), do: true
2037 def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled
2038 def ap_enabled?(_), do: false
2040 @doc "Gets or fetch a user by uri or nickname."
2041 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
2042 def get_or_fetch("http://" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
2043 def get_or_fetch("https://" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
2044 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
2046 # wait a period of time and return newest version of the User structs
2047 # this is because we have synchronous follow APIs and need to simulate them
2048 # with an async handshake
2049 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
2050 with %User{} = a <- get_cached_by_id(a.id),
2051 %User{} = b <- get_cached_by_id(b.id) do
2058 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
2059 with :ok <- :timer.sleep(timeout),
2060 %User{} = a <- get_cached_by_id(a.id),
2061 %User{} = b <- get_cached_by_id(b.id) do
2068 def parse_bio(bio) when is_binary(bio) and bio != "" do
2070 |> CommonUtils.format_input("text/plain", mentions_format: :full)
2074 def parse_bio(_), do: ""
2076 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
2077 # TODO: get profile URLs other than user.ap_id
2078 profile_urls = [user.ap_id]
2080 CommonUtils.format_input(bio, "text/plain",
2081 mentions_format: :full,
2083 case RelMe.maybe_put_rel_me(link, profile_urls) do
2092 def parse_bio(_, _), do: ""
2094 def tag(user_identifiers, tags) when is_list(user_identifiers) do
2095 Repo.transaction(fn ->
2096 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
2100 def tag(nickname, tags) when is_binary(nickname),
2101 do: tag(get_by_nickname(nickname), tags)
2103 def tag(%User{} = user, tags),
2104 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
2106 def untag(user_identifiers, tags) when is_list(user_identifiers) do
2107 Repo.transaction(fn ->
2108 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
2112 def untag(nickname, tags) when is_binary(nickname),
2113 do: untag(get_by_nickname(nickname), tags)
2115 def untag(%User{} = user, tags),
2116 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
2118 defp update_tags(%User{} = user, new_tags) do
2119 {:ok, updated_user} =
2121 |> change(%{tags: new_tags})
2122 |> update_and_set_cache()
2127 defp normalize_tags(tags) do
2130 |> Enum.map(&String.downcase/1)
2133 def local_nickname_regex do
2134 if Config.get([:instance, :extended_nickname_format]) do
2135 @extended_local_nickname_regex
2137 @strict_local_nickname_regex
2141 def local_nickname(nickname_or_mention) do
2144 |> String.split("@")
2148 def full_nickname(%User{} = user) do
2149 if String.contains?(user.nickname, "@") do
2152 %{host: host} = URI.parse(user.ap_id)
2153 user.nickname <> "@" <> host
2157 def full_nickname(nickname_or_mention),
2158 do: String.trim_leading(nickname_or_mention, "@")
2160 def error_user(ap_id) do
2164 nickname: "erroruser@example.com",
2165 inserted_at: NaiveDateTime.utc_now()
2169 @spec all_superusers() :: [User.t()]
2170 def all_superusers do
2171 User.Query.build(%{super_users: true, local: true, is_active: true})
2175 def muting_reblogs?(%User{} = user, %User{} = target) do
2176 UserRelationship.reblog_mute_exists?(user, target)
2179 def showing_reblogs?(%User{} = user, %User{} = target) do
2180 not muting_reblogs?(user, target)
2184 The function returns a query to get users with no activity for given interval of days.
2185 Inactive users are those who didn't read any notification, or had any activity where
2186 the user is the activity's actor, during `inactivity_threshold` days.
2187 Deactivated users will not appear in this list.
2191 iex> Pleroma.User.list_inactive_users()
2194 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
2195 def list_inactive_users_query(inactivity_threshold \\ 7) do
2196 negative_inactivity_threshold = -inactivity_threshold
2197 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
2198 # Subqueries are not supported in `where` clauses, join gets too complicated.
2199 has_read_notifications =
2200 from(n in Pleroma.Notification,
2201 where: n.seen == true,
2203 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
2206 |> Pleroma.Repo.all()
2208 from(u in Pleroma.User,
2209 left_join: a in Pleroma.Activity,
2210 on: u.ap_id == a.actor,
2211 where: not is_nil(u.nickname),
2212 where: u.is_active == ^true,
2213 where: u.id not in ^has_read_notifications,
2216 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
2217 is_nil(max(a.inserted_at))
2222 Enable or disable email notifications for user
2226 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => false}}, "digest", true)
2227 Pleroma.User{email_notifications: %{"digest" => true}}
2229 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => true}}, "digest", false)
2230 Pleroma.User{email_notifications: %{"digest" => false}}
2232 @spec switch_email_notifications(t(), String.t(), boolean()) ::
2233 {:ok, t()} | {:error, Ecto.Changeset.t()}
2234 def switch_email_notifications(user, type, status) do
2235 User.update_email_notifications(user, %{type => status})
2239 Set `last_digest_emailed_at` value for the user to current time
2241 @spec touch_last_digest_emailed_at(t()) :: t()
2242 def touch_last_digest_emailed_at(user) do
2243 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
2245 {:ok, updated_user} =
2247 |> change(%{last_digest_emailed_at: now})
2248 |> update_and_set_cache()
2253 @spec set_confirmation(User.t(), boolean()) :: {:ok, User.t()} | {:error, Changeset.t()}
2254 def set_confirmation(%User{} = user, bool) do
2256 |> confirmation_changeset(set_confirmation: bool)
2257 |> update_and_set_cache()
2260 def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
2264 def get_mascot(%{mascot: mascot}) when is_nil(mascot) do
2265 # use instance-default
2266 config = Config.get([:assets, :mascots])
2267 default_mascot = Config.get([:assets, :default_mascot])
2268 mascot = Keyword.get(config, default_mascot)
2271 "id" => "default-mascot",
2272 "url" => mascot[:url],
2273 "preview_url" => mascot[:url],
2275 "mime_type" => mascot[:mime_type]
2280 def get_ap_ids_by_nicknames(nicknames) do
2282 where: u.nickname in ^nicknames,
2288 defp put_password_hash(
2289 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
2291 change(changeset, password_hash: Pleroma.Password.hash_pwd_salt(password))
2294 defp put_password_hash(changeset), do: changeset
2296 def is_internal_user?(%User{nickname: nil}), do: true
2297 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
2298 def is_internal_user?(_), do: false
2300 # A hack because user delete activities have a fake id for whatever reason
2301 # TODO: Get rid of this
2302 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
2304 def get_delivered_users_by_object_id(object_id) do
2306 inner_join: delivery in assoc(u, :deliveries),
2307 where: delivery.object_id == ^object_id
2312 def change_email(user, email) do
2314 |> cast(%{email: email}, [:email])
2315 |> maybe_validate_required_email(false)
2316 |> unique_constraint(:email)
2317 |> validate_format(:email, @email_regex)
2318 |> update_and_set_cache()
2321 def alias_users(user) do
2323 |> Enum.map(&User.get_cached_by_ap_id/1)
2324 |> Enum.filter(fn user -> user != nil end)
2327 def add_alias(user, new_alias_user) do
2328 current_aliases = user.also_known_as || []
2329 new_alias_ap_id = new_alias_user.ap_id
2331 if new_alias_ap_id in current_aliases do
2335 |> cast(%{also_known_as: current_aliases ++ [new_alias_ap_id]}, [:also_known_as])
2336 |> update_and_set_cache()
2340 @spec delete_alias(User.t(), User.t()) :: {:error, :no_such_alias}
2341 def delete_alias(user, alias_user) do
2342 current_aliases = user.also_known_as || []
2343 alias_ap_id = alias_user.ap_id
2345 if alias_ap_id in current_aliases do
2347 |> cast(%{also_known_as: current_aliases -- [alias_ap_id]}, [:also_known_as])
2348 |> update_and_set_cache()
2350 {:error, :no_such_alias}
2354 # Internal function; public one is `deactivate/2`
2355 defp set_activation_status(user, status) do
2357 |> cast(%{is_active: status}, [:is_active])
2358 |> update_and_set_cache()
2361 def update_banner(user, banner) do
2363 |> cast(%{banner: banner}, [:banner])
2364 |> update_and_set_cache()
2367 def update_background(user, background) do
2369 |> cast(%{background: background}, [:background])
2370 |> update_and_set_cache()
2373 @spec validate_fields(Ecto.Changeset.t(), Boolean.t(), User.t()) :: Ecto.Changeset.t()
2374 def validate_fields(changeset, remote? \\ false, struct) do
2375 limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
2376 limit = Config.get([:instance, limit_name], 0)
2379 |> validate_length(:fields, max: limit)
2380 |> validate_change(:fields, fn :fields, fields ->
2381 if Enum.all?(fields, &valid_field?/1) do
2387 |> maybe_validate_rel_me_field(struct)
2390 defp valid_field?(%{"name" => name, "value" => value}) do
2391 name_limit = Config.get([:instance, :account_field_name_length], 255)
2392 value_limit = Config.get([:instance, :account_field_value_length], 255)
2394 is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
2395 String.length(value) <= value_limit
2398 defp valid_field?(_), do: false
2400 defp is_url(nil), do: nil
2403 case URI.parse(uri) do
2404 %URI{host: nil} -> false
2405 %URI{scheme: nil} -> false
2410 @spec maybe_validate_rel_me_field(Changeset.t(), User.t()) :: Changeset.t()
2411 defp maybe_validate_rel_me_field(changeset, %User{ap_id: _ap_id} = struct) do
2412 fields = get_change(changeset, :fields)
2413 raw_fields = get_change(changeset, :raw_fields)
2415 if is_nil(fields) do
2418 validate_rel_me_field(changeset, fields, raw_fields, struct)
2422 defp maybe_validate_rel_me_field(changeset, _), do: changeset
2424 @spec validate_rel_me_field(Changeset.t(), [Map.t()], [Map.t()], User.t()) :: Changeset.t()
2425 defp validate_rel_me_field(changeset, fields, raw_fields, %User{
2431 |> Enum.with_index()
2432 |> Enum.map(fn {%{"name" => name, "value" => value}, index} ->
2434 if is_nil(raw_fields) do
2437 Enum.at(raw_fields, index)["value"]
2440 if is_url(raw_value) do
2442 Pleroma.Web.Router.Helpers.redirect_url(
2443 Pleroma.Web.Endpoint,
2444 :redirector_with_meta,
2448 possible_urls = [ap_id, frontend_url]
2450 with "me" <- RelMe.maybe_put_rel_me(raw_value, possible_urls) do
2454 "verified_at" => DateTime.to_iso8601(DateTime.utc_now())
2458 Logger.error("Could not check for rel=me, #{inspect(e)}")
2459 %{"name" => name, "value" => value}
2462 %{"name" => name, "value" => value}
2466 put_change(changeset, :fields, fields)
2469 defp truncate_field(%{"name" => name, "value" => value}) do
2471 String.split_at(name, Config.get([:instance, :account_field_name_length], 255))
2474 String.split_at(value, Config.get([:instance, :account_field_value_length], 255))
2476 %{"name" => name, "value" => value}
2479 def admin_api_update(user, params) do
2486 |> update_and_set_cache()
2489 @doc "Signs user out of all applications"
2490 def global_sign_out(user) do
2491 OAuth.Authorization.delete_user_authorizations(user)
2492 OAuth.Token.delete_user_tokens(user)
2495 def mascot_update(user, url) do
2497 |> cast(%{mascot: url}, [:mascot])
2498 |> validate_required([:mascot])
2499 |> update_and_set_cache()
2502 def mastodon_settings_update(user, settings) do
2504 |> cast(%{mastofe_settings: settings}, [:mastofe_settings])
2505 |> validate_required([:mastofe_settings])
2506 |> update_and_set_cache()
2509 @spec confirmation_changeset(User.t(), keyword()) :: Changeset.t()
2510 def confirmation_changeset(user, set_confirmation: confirmed?) do
2515 confirmation_token: nil
2519 is_confirmed: false,
2520 confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
2524 cast(user, params, [:is_confirmed, :confirmation_token])
2527 @spec approval_changeset(Changeset.t(), keyword()) :: Changeset.t()
2528 def approval_changeset(user, set_approval: approved?) do
2529 cast(user, %{is_approved: approved?}, [:is_approved])
2532 @spec add_pinned_object_id(User.t(), String.t()) :: {:ok, User.t()} | {:error, term()}
2533 def add_pinned_object_id(%User{} = user, object_id) do
2534 if !user.pinned_objects[object_id] do
2535 params = %{pinned_objects: Map.put(user.pinned_objects, object_id, NaiveDateTime.utc_now())}
2538 |> cast(params, [:pinned_objects])
2539 |> validate_change(:pinned_objects, fn :pinned_objects, pinned_objects ->
2540 max_pinned_statuses = Config.get([:instance, :max_pinned_statuses], 0)
2542 if Enum.count(pinned_objects) <= max_pinned_statuses do
2545 [pinned_objects: "You have already pinned the maximum number of statuses"]
2551 |> update_and_set_cache()
2554 @spec remove_pinned_object_id(User.t(), String.t()) :: {:ok, t()} | {:error, term()}
2555 def remove_pinned_object_id(%User{} = user, object_id) do
2558 %{pinned_objects: Map.delete(user.pinned_objects, object_id)},
2561 |> update_and_set_cache()
2564 def update_email_notifications(user, settings) do
2565 email_notifications =
2566 user.email_notifications
2567 |> Map.merge(settings)
2568 |> Map.take(["digest"])
2570 params = %{email_notifications: email_notifications}
2571 fields = [:email_notifications]
2574 |> cast(params, fields)
2575 |> validate_required(fields)
2576 |> update_and_set_cache()
2579 defp set_domain_blocks(user, domain_blocks) do
2580 params = %{domain_blocks: domain_blocks}
2583 |> cast(params, [:domain_blocks])
2584 |> validate_required([:domain_blocks])
2585 |> update_and_set_cache()
2588 def block_domain(user, domain_blocked) do
2589 set_domain_blocks(user, Enum.uniq([domain_blocked | user.domain_blocks]))
2592 def unblock_domain(user, domain_blocked) do
2593 set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked))
2596 @spec add_to_block(User.t(), User.t()) ::
2597 {:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()}
2598 defp add_to_block(%User{} = user, %User{} = blocked) do
2599 with {:ok, relationship} <- UserRelationship.create_block(user, blocked) do
2600 @cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
2607 @spec remove_from_block(User.t(), User.t()) ::
2608 {:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()}
2609 defp remove_from_block(%User{} = user, %User{} = blocked) do
2610 with {:ok, relationship} <- UserRelationship.delete_block(user, blocked) do
2611 @cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
2618 def set_invisible(user, invisible) do
2619 params = %{invisible: invisible}
2622 |> cast(params, [:invisible])
2623 |> validate_required([:invisible])
2624 |> update_and_set_cache()
2627 def sanitize_html(%User{} = user) do
2628 sanitize_html(user, nil)
2631 # User data that mastodon isn't filtering (treated as plaintext):
2634 def sanitize_html(%User{} = user, filter) do
2636 Enum.map(user.fields, fn %{"value" => value} = field ->
2637 Map.put(field, "value", HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly))
2641 |> Map.put(:bio, HTML.filter_tags(user.bio, filter))
2642 |> Map.put(:fields, fields)
2645 def get_host(%User{ap_id: ap_id} = _user) do
2646 URI.parse(ap_id).host
2649 def update_last_active_at(%__MODULE__{local: true} = user) do
2651 |> cast(%{last_active_at: NaiveDateTime.utc_now()}, [:last_active_at])
2652 |> update_and_set_cache()
2655 def active_user_count(days \\ 30) do
2656 active_after = Timex.shift(NaiveDateTime.utc_now(), days: -days)
2659 |> where([u], u.last_active_at >= ^active_after)
2660 |> where([u], u.local == true)
2661 |> Repo.aggregate(:count)
2664 def update_last_status_at(user) do
2666 |> where(id: ^user.id)
2667 |> update([u], set: [last_status_at: fragment("NOW()")])
2669 |> Repo.update_all([])
2671 {1, [user]} -> set_cache(user)
2676 defp maybe_load_followed_hashtags(%User{followed_hashtags: follows} = user)
2677 when is_list(follows),
2680 defp maybe_load_followed_hashtags(%User{} = user) do
2681 followed_hashtags = HashtagFollow.get_by_user(user)
2682 %{user | followed_hashtags: followed_hashtags}
2685 def followed_hashtags(%User{followed_hashtags: follows})
2686 when is_list(follows),
2689 def followed_hashtags(%User{} = user) do
2692 |> maybe_load_followed_hashtags()
2695 user.followed_hashtags
2698 def follow_hashtag(%User{} = user, %Hashtag{} = hashtag) do
2699 Logger.debug("Follow hashtag #{hashtag.name} for user #{user.nickname}")
2700 user = maybe_load_followed_hashtags(user)
2702 with {:ok, _} <- HashtagFollow.new(user, hashtag),
2703 follows <- HashtagFollow.get_by_user(user),
2704 %User{} = user <- user |> Map.put(:followed_hashtags, follows) do
2710 def unfollow_hashtag(%User{} = user, %Hashtag{} = hashtag) do
2711 Logger.debug("Unfollow hashtag #{hashtag.name} for user #{user.nickname}")
2712 user = maybe_load_followed_hashtags(user)
2714 with {:ok, _} <- HashtagFollow.delete(user, hashtag),
2715 follows <- HashtagFollow.get_by_user(user),
2716 %User{} = user <- user |> Map.put(:followed_hashtags, follows) do
2722 def following_hashtag?(%User{} = user, %Hashtag{} = hashtag) do
2723 not is_nil(HashtagFollow.get(user, hashtag))