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
10 import Ecto, only: [assoc: 2]
13 alias Pleroma.Activity
15 alias Pleroma.Conversation.Participation
16 alias Pleroma.Delivery
17 alias Pleroma.EctoType.ActivityPub.ObjectValidators
19 alias Pleroma.FollowingRelationship
20 alias Pleroma.Formatter
24 alias Pleroma.Notification
26 alias Pleroma.Registration
29 alias Pleroma.UserRelationship
30 alias Pleroma.Web.ActivityPub.ActivityPub
31 alias Pleroma.Web.ActivityPub.Builder
32 alias Pleroma.Web.ActivityPub.Pipeline
33 alias Pleroma.Web.ActivityPub.Utils
34 alias Pleroma.Web.CommonAPI
35 alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
36 alias Pleroma.Web.Endpoint
37 alias Pleroma.Web.OAuth
38 alias Pleroma.Web.RelMe
39 alias Pleroma.Workers.BackgroundWorker
43 @type t :: %__MODULE__{}
44 @type account_status ::
47 | :password_reset_pending
48 | :confirmation_pending
50 @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
52 # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
53 @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])?)*$/
55 @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
56 @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
58 # AP ID user relationships (blocks, mutes etc.)
59 # Format: [rel_type: [outgoing_rel: :outgoing_rel_target, incoming_rel: :incoming_rel_source]]
60 @user_relationships_config [
62 blocker_blocks: :blocked_users,
63 blockee_blocks: :blocker_users
66 muter_mutes: :muted_users,
67 mutee_mutes: :muter_users
70 reblog_muter_mutes: :reblog_muted_users,
71 reblog_mutee_mutes: :reblog_muter_users
74 notification_muter_mutes: :notification_muted_users,
75 notification_mutee_mutes: :notification_muter_users
77 # Note: `inverse_subscription` relationship is inverse: subscriber acts as relationship target
78 inverse_subscription: [
79 subscribee_subscriptions: :subscriber_users,
80 subscriber_subscriptions: :subscribee_users
84 @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
87 field(:bio, :string, default: "")
88 field(:raw_bio, :string)
89 field(:email, :string)
91 field(:nickname, :string)
92 field(:password_hash, :string)
93 field(:password, :string, virtual: true)
94 field(:password_confirmation, :string, virtual: true)
96 field(:public_key, :string)
97 field(:ap_id, :string)
98 field(:avatar, :map, default: %{})
99 field(:local, :boolean, default: true)
100 field(:follower_address, :string)
101 field(:following_address, :string)
102 field(:featured_address, :string)
103 field(:search_rank, :float, virtual: true)
104 field(:search_type, :integer, virtual: true)
105 field(:tags, {:array, :string}, default: [])
106 field(:last_refreshed_at, :naive_datetime_usec)
107 field(:last_digest_emailed_at, :naive_datetime)
108 field(:banner, :map, default: %{})
109 field(:background, :map, default: %{})
110 field(:note_count, :integer, default: 0)
111 field(:follower_count, :integer, default: 0)
112 field(:following_count, :integer, default: 0)
113 field(:is_locked, :boolean, default: false)
114 field(:is_confirmed, :boolean, default: true)
115 field(:password_reset_pending, :boolean, default: false)
116 field(:is_approved, :boolean, default: true)
117 field(:registration_reason, :string, default: nil)
118 field(:confirmation_token, :string, default: nil)
119 field(:default_scope, :string, default: "public")
120 field(:domain_blocks, {:array, :string}, default: [])
121 field(:is_active, :boolean, default: true)
122 field(:no_rich_text, :boolean, default: false)
123 field(:ap_enabled, :boolean, default: false)
124 field(:is_moderator, :boolean, default: false)
125 field(:is_admin, :boolean, default: false)
126 field(:show_role, :boolean, default: true)
127 field(:mastofe_settings, :map, default: nil)
128 field(:uri, ObjectValidators.Uri, default: nil)
129 field(:hide_followers_count, :boolean, default: false)
130 field(:hide_follows_count, :boolean, default: false)
131 field(:hide_followers, :boolean, default: false)
132 field(:hide_follows, :boolean, default: false)
133 field(:hide_favorites, :boolean, default: true)
134 field(:email_notifications, :map, default: %{"digest" => false})
135 field(:mascot, :map, default: nil)
136 field(:emoji, :map, default: %{})
137 field(:pleroma_settings_store, :map, default: %{})
138 field(:fields, {:array, :map}, default: [])
139 field(:raw_fields, {:array, :map}, default: [])
140 field(:is_discoverable, :boolean, default: false)
141 field(:invisible, :boolean, default: false)
142 field(:allow_following_move, :boolean, default: true)
143 field(:skip_thread_containment, :boolean, default: false)
144 field(:actor_type, :string, default: "Person")
145 field(:also_known_as, {:array, ObjectValidators.ObjectID}, default: [])
146 field(:inbox, :string)
147 field(:shared_inbox, :string)
148 field(:accepts_chat_messages, :boolean, default: nil)
149 field(:last_active_at, :naive_datetime)
150 field(:disclose_client, :boolean, default: true)
151 field(:pinned_objects, :map, default: %{})
152 field(:is_suggested, :boolean, default: false)
153 field(:last_status_at, :naive_datetime)
154 field(:language, :string)
157 :notification_settings,
158 Pleroma.User.NotificationSetting,
162 has_many(:notifications, Notification)
163 has_many(:registrations, Registration)
164 has_many(:deliveries, Delivery)
166 has_many(:outgoing_relationships, UserRelationship, foreign_key: :source_id)
167 has_many(:incoming_relationships, UserRelationship, foreign_key: :target_id)
169 for {relationship_type,
171 {outgoing_relation, outgoing_relation_target},
172 {incoming_relation, incoming_relation_source}
173 ]} <- @user_relationships_config do
174 # Definitions of `has_many` relations: :blocker_blocks, :muter_mutes, :reblog_muter_mutes,
175 # :notification_muter_mutes, :subscribee_subscriptions
176 has_many(outgoing_relation, UserRelationship,
177 foreign_key: :source_id,
178 where: [relationship_type: relationship_type]
181 # Definitions of `has_many` relations: :blockee_blocks, :mutee_mutes, :reblog_mutee_mutes,
182 # :notification_mutee_mutes, :subscriber_subscriptions
183 has_many(incoming_relation, UserRelationship,
184 foreign_key: :target_id,
185 where: [relationship_type: relationship_type]
188 # Definitions of `has_many` relations: :blocked_users, :muted_users, :reblog_muted_users,
189 # :notification_muted_users, :subscriber_users
190 has_many(outgoing_relation_target, through: [outgoing_relation, :target])
192 # Definitions of `has_many` relations: :blocker_users, :muter_users, :reblog_muter_users,
193 # :notification_muter_users, :subscribee_users
194 has_many(incoming_relation_source, through: [incoming_relation, :source])
197 # `:blocks` is deprecated (replaced with `blocked_users` relation)
198 field(:blocks, {:array, :string}, default: [])
199 # `:mutes` is deprecated (replaced with `muted_users` relation)
200 field(:mutes, {:array, :string}, default: [])
201 # `:muted_reblogs` is deprecated (replaced with `reblog_muted_users` relation)
202 field(:muted_reblogs, {:array, :string}, default: [])
203 # `:muted_notifications` is deprecated (replaced with `notification_muted_users` relation)
204 field(:muted_notifications, {:array, :string}, default: [])
205 # `:subscribers` is deprecated (replaced with `subscriber_users` relation)
206 field(:subscribers, {:array, :string}, default: [])
209 :multi_factor_authentication_settings,
217 for {_relationship_type, [{_outgoing_relation, outgoing_relation_target}, _]} <-
218 @user_relationships_config do
219 # `def blocked_users_relation/2`, `def muted_users_relation/2`,
220 # `def reblog_muted_users_relation/2`, `def notification_muted_users/2`,
221 # `def subscriber_users/2`
222 def unquote(:"#{outgoing_relation_target}_relation")(user, restrict_deactivated? \\ false) do
223 target_users_query = assoc(user, unquote(outgoing_relation_target))
225 if restrict_deactivated? do
227 |> User.Query.build(%{deactivated: false})
233 # `def blocked_users/2`, `def muted_users/2`, `def reblog_muted_users/2`,
234 # `def notification_muted_users/2`, `def subscriber_users/2`
235 def unquote(outgoing_relation_target)(user, restrict_deactivated? \\ false) do
237 |> apply(unquote(:"#{outgoing_relation_target}_relation"), [
239 restrict_deactivated?
244 # `def blocked_users_ap_ids/2`, `def muted_users_ap_ids/2`, `def reblog_muted_users_ap_ids/2`,
245 # `def notification_muted_users_ap_ids/2`, `def subscriber_users_ap_ids/2`
246 def unquote(:"#{outgoing_relation_target}_ap_ids")(user, restrict_deactivated? \\ false) do
248 |> apply(unquote(:"#{outgoing_relation_target}_relation"), [
250 restrict_deactivated?
252 |> select([u], u.ap_id)
257 def cached_blocked_users_ap_ids(user) do
258 @cachex.fetch!(:user_cache, "blocked_users_ap_ids:#{user.ap_id}", fn _ ->
259 blocked_users_ap_ids(user)
263 def cached_muted_users_ap_ids(user) do
264 @cachex.fetch!(:user_cache, "muted_users_ap_ids:#{user.ap_id}", fn _ ->
265 muted_users_ap_ids(user)
269 defdelegate following_count(user), to: FollowingRelationship
270 defdelegate following(user), to: FollowingRelationship
271 defdelegate following?(follower, followed), to: FollowingRelationship
272 defdelegate following_ap_ids(user), to: FollowingRelationship
273 defdelegate get_follow_requests(user), to: FollowingRelationship
274 defdelegate search(query, opts \\ []), to: User.Search
277 Dumps Flake Id to SQL-compatible format (16-byte UUID).
278 E.g. "9pQtDGXuq4p3VlcJEm" -> <<0, 0, 1, 110, 179, 218, 42, 92, 213, 41, 44, 227, 95, 213, 0, 0>>
280 def binary_id(source_id) when is_binary(source_id) do
281 with {:ok, dumped_id} <- FlakeId.Ecto.CompatType.dump(source_id) do
288 def binary_id(source_ids) when is_list(source_ids) do
289 Enum.map(source_ids, &binary_id/1)
292 def binary_id(%User{} = user), do: binary_id(user.id)
294 @doc "Returns status account"
295 @spec account_status(User.t()) :: account_status()
296 def account_status(%User{is_active: false}), do: :deactivated
297 def account_status(%User{password_reset_pending: true}), do: :password_reset_pending
298 def account_status(%User{local: true, is_approved: false}), do: :approval_pending
299 def account_status(%User{local: true, is_confirmed: false}), do: :confirmation_pending
300 def account_status(%User{}), do: :active
302 @spec visible_for(User.t(), User.t() | nil) ::
305 | :restricted_unauthenticated
307 | :confirmation_pending
308 def visible_for(user, for_user \\ nil)
310 def visible_for(%User{invisible: true}, _), do: :invisible
312 def visible_for(%User{id: user_id}, %User{id: user_id}), do: :visible
314 def visible_for(%User{} = user, nil) do
315 if restrict_unauthenticated?(user) do
316 :restrict_unauthenticated
318 visible_account_status(user)
322 def visible_for(%User{} = user, for_user) do
323 if superuser?(for_user) do
326 visible_account_status(user)
330 def visible_for(_, _), do: :invisible
332 defp restrict_unauthenticated?(%User{local: true}) do
333 Config.restrict_unauthenticated_access?(:profiles, :local)
336 defp restrict_unauthenticated?(%User{local: _}) do
337 Config.restrict_unauthenticated_access?(:profiles, :remote)
340 defp visible_account_status(user) do
341 status = account_status(user)
343 if status in [:active, :password_reset_pending] do
350 @spec superuser?(User.t()) :: boolean()
351 def superuser?(%User{local: true, is_admin: true}), do: true
352 def superuser?(%User{local: true, is_moderator: true}), do: true
353 def superuser?(_), do: false
355 @spec invisible?(User.t()) :: boolean()
356 def invisible?(%User{invisible: true}), do: true
357 def invisible?(_), do: false
359 def avatar_url(user, options \\ []) do
361 %{"url" => [%{"href" => href} | _]} ->
365 unless options[:no_default] do
366 Config.get([:assets, :default_user_avatar], "#{Endpoint.url()}/images/avi.png")
371 def banner_url(user, options \\ []) do
373 %{"url" => [%{"href" => href} | _]} -> href
374 _ -> !options[:no_default] && "#{Endpoint.url()}/images/banner.png"
378 # Should probably be renamed or removed
379 @spec ap_id(User.t()) :: String.t()
380 def ap_id(%User{nickname: nickname}), do: "#{Endpoint.url()}/users/#{nickname}"
382 @spec ap_followers(User.t()) :: String.t()
383 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
384 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
386 @spec ap_following(User.t()) :: String.t()
387 def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
388 def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
390 @spec ap_featured_collection(User.t()) :: String.t()
391 def ap_featured_collection(%User{featured_address: fa}) when is_binary(fa), do: fa
393 def ap_featured_collection(%User{} = user), do: "#{ap_id(user)}/collections/featured"
395 defp truncate_fields_param(params) do
396 if Map.has_key?(params, :fields) do
397 Map.put(params, :fields, Enum.map(params[:fields], &truncate_field/1))
403 defp truncate_if_exists(params, key, max_length) do
404 if Map.has_key?(params, key) and is_binary(params[key]) do
405 {value, _chopped} = String.split_at(params[key], max_length)
406 Map.put(params, key, value)
412 defp fix_follower_address(%{follower_address: _, following_address: _} = params), do: params
414 defp fix_follower_address(%{nickname: nickname} = params),
415 do: Map.put(params, :follower_address, ap_followers(%User{nickname: nickname}))
417 defp fix_follower_address(params), do: params
419 def remote_user_changeset(struct \\ %User{local: false}, params) do
420 bio_limit = Config.get([:instance, :user_bio_length], 5000)
421 name_limit = Config.get([:instance, :user_name_length], 100)
424 case params[:name] do
425 name when is_binary(name) and byte_size(name) > 0 -> name
426 _ -> params[:nickname]
431 |> Map.put(:name, name)
432 |> Map.put_new(:last_refreshed_at, NaiveDateTime.utc_now())
433 |> truncate_if_exists(:name, name_limit)
434 |> truncate_if_exists(:bio, bio_limit)
435 |> truncate_fields_param()
436 |> fix_follower_address()
460 :hide_followers_count,
469 :accepts_chat_messages,
473 |> cast(params, [:name], empty_values: [])
474 |> validate_required([:ap_id])
475 |> validate_required([:name], trim: false)
476 |> unique_constraint(:nickname)
477 |> validate_format(:nickname, @email_regex)
478 |> validate_length(:bio, max: bio_limit)
479 |> validate_length(:name, max: name_limit)
480 |> validate_fields(true)
481 |> validate_non_local()
484 defp validate_non_local(cng) do
485 local? = get_field(cng, :local)
489 |> add_error(:local, "User is local, can't update with this changeset.")
495 def update_changeset(struct, params \\ %{}) do
496 bio_limit = Config.get([:instance, :user_bio_length], 5000)
497 name_limit = Config.get([:instance, :user_name_length], 100)
517 :hide_followers_count,
520 :allow_following_move,
524 :skip_thread_containment,
527 :pleroma_settings_store,
530 :accepts_chat_messages,
534 |> unique_constraint(:nickname)
535 |> validate_format(:nickname, local_nickname_regex())
536 |> validate_length(:bio, max: bio_limit)
537 |> validate_length(:name, min: 1, max: name_limit)
538 |> validate_inclusion(:actor_type, ["Person", "Service"])
541 |> put_change_if_present(:bio, &{:ok, parse_bio(&1, struct)})
542 |> put_change_if_present(:avatar, &put_upload(&1, :avatar))
543 |> put_change_if_present(:banner, &put_upload(&1, :banner))
544 |> put_change_if_present(:background, &put_upload(&1, :background))
545 |> put_change_if_present(
546 :pleroma_settings_store,
547 &{:ok, Map.merge(struct.pleroma_settings_store, &1)}
549 |> validate_fields(false)
552 defp put_fields(changeset) do
553 if raw_fields = get_change(changeset, :raw_fields) do
556 |> Enum.filter(fn %{"name" => n} -> n != "" end)
560 |> Enum.map(fn f -> Map.update!(f, "value", &parse_fields(&1)) end)
563 |> put_change(:raw_fields, raw_fields)
564 |> put_change(:fields, fields)
570 defp parse_fields(value) do
572 |> Formatter.linkify(mentions_format: :full)
576 defp put_emoji(changeset) do
577 emojified_fields = [:bio, :name, :raw_fields]
579 if Enum.any?(changeset.changes, fn {k, _} -> k in emojified_fields end) do
580 bio = Emoji.Formatter.get_emoji_map(get_field(changeset, :bio))
581 name = Emoji.Formatter.get_emoji_map(get_field(changeset, :name))
583 emoji = Map.merge(bio, name)
587 |> get_field(:raw_fields)
588 |> Enum.reduce(emoji, fn x, acc ->
589 Map.merge(acc, Emoji.Formatter.get_emoji_map(x["name"] <> x["value"]))
592 put_change(changeset, :emoji, emoji)
598 defp put_change_if_present(changeset, map_field, value_function) do
599 with {:ok, value} <- fetch_change(changeset, map_field),
600 {:ok, new_value} <- value_function.(value) do
601 put_change(changeset, map_field, new_value)
607 defp put_upload(value, type) do
608 with %Plug.Upload{} <- value,
609 {:ok, object} <- ActivityPub.upload(value, type: type) do
614 def update_as_admin_changeset(struct, params) do
616 |> update_changeset(params)
617 |> cast(params, [:email])
618 |> delete_change(:also_known_as)
619 |> unique_constraint(:email)
620 |> validate_format(:email, @email_regex)
621 |> validate_inclusion(:actor_type, ["Person", "Service"])
624 @spec update_as_admin(User.t(), map()) :: {:ok, User.t()} | {:error, Changeset.t()}
625 def update_as_admin(user, params) do
626 params = Map.put(params, "password_confirmation", params["password"])
627 changeset = update_as_admin_changeset(user, params)
629 if params["password"] do
630 reset_password(user, changeset, params)
632 User.update_and_set_cache(changeset)
636 def password_update_changeset(struct, params) do
638 |> cast(params, [:password, :password_confirmation])
639 |> validate_required([:password, :password_confirmation])
640 |> validate_confirmation(:password)
641 |> put_password_hash()
642 |> put_change(:password_reset_pending, false)
645 @spec reset_password(User.t(), map()) :: {:ok, User.t()} | {:error, Changeset.t()}
646 def reset_password(%User{} = user, params) do
647 reset_password(user, user, params)
650 def reset_password(%User{id: user_id} = user, struct, params) do
653 |> Multi.update(:user, password_update_changeset(struct, params))
654 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
655 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
657 case Repo.transaction(multi) do
658 {:ok, %{user: user} = _} -> set_cache(user)
659 {:error, _, changeset, _} -> {:error, changeset}
663 def update_password_reset_pending(user, value) do
666 |> put_change(:password_reset_pending, value)
667 |> update_and_set_cache()
670 def force_password_reset_async(user) do
671 BackgroundWorker.enqueue("force_password_reset", %{"user_id" => user.id})
674 @spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
675 def force_password_reset(user), do: update_password_reset_pending(user, true)
677 # Used to auto-register LDAP accounts which won't have a password hash stored locally
678 def register_changeset_ldap(struct, params = %{password: password})
679 when is_nil(password) do
680 params = Map.put_new(params, :accepts_chat_messages, true)
683 if Map.has_key?(params, :email) do
684 Map.put_new(params, :email, params[:email])
694 :accepts_chat_messages
696 |> validate_required([:name, :nickname])
697 |> unique_constraint(:nickname)
698 |> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames]))
699 |> validate_format(:nickname, local_nickname_regex())
701 |> unique_constraint(:ap_id)
702 |> put_following_and_follower_and_featured_address()
705 def register_changeset(struct, params \\ %{}, opts \\ []) do
706 bio_limit = Config.get([:instance, :user_bio_length], 5000)
707 name_limit = Config.get([:instance, :user_name_length], 100)
708 reason_limit = Config.get([:instance, :registration_reason_length], 500)
709 params = Map.put_new(params, :accepts_chat_messages, true)
712 if is_nil(opts[:confirmed]) do
713 !Config.get([:instance, :account_activation_required])
719 if is_nil(opts[:approved]) do
720 !Config.get([:instance, :account_approval_required])
726 |> confirmation_changeset(set_confirmation: confirmed?)
727 |> approval_changeset(set_approval: approved?)
735 :password_confirmation,
737 :accepts_chat_messages,
738 :registration_reason,
741 |> validate_required([:name, :nickname, :password, :password_confirmation])
742 |> validate_confirmation(:password)
743 |> unique_constraint(:email)
744 |> validate_format(:email, @email_regex)
745 |> validate_change(:email, fn :email, email ->
747 Config.get([User, :email_blacklist])
748 |> Enum.all?(fn blacklisted_domain ->
749 !String.ends_with?(email, ["@" <> blacklisted_domain, "." <> blacklisted_domain])
752 if valid?, do: [], else: [email: "Invalid email"]
754 |> unique_constraint(:nickname)
755 |> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames]))
756 |> validate_format(:nickname, local_nickname_regex())
757 |> validate_length(:bio, max: bio_limit)
758 |> validate_length(:name, min: 1, max: name_limit)
759 |> validate_length(:registration_reason, max: reason_limit)
760 |> maybe_validate_required_email(opts[:external])
763 |> unique_constraint(:ap_id)
764 |> put_following_and_follower_and_featured_address()
767 def maybe_validate_required_email(changeset, true), do: changeset
769 def maybe_validate_required_email(changeset, _) do
770 if Config.get([:instance, :account_activation_required]) do
771 validate_required(changeset, [:email])
777 defp put_ap_id(changeset) do
778 ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
779 put_change(changeset, :ap_id, ap_id)
782 defp put_following_and_follower_and_featured_address(changeset) do
783 user = %User{nickname: get_field(changeset, :nickname)}
784 followers = ap_followers(user)
785 following = ap_following(user)
786 featured = ap_featured_collection(user)
789 |> put_change(:follower_address, followers)
790 |> put_change(:following_address, following)
791 |> put_change(:featured_address, featured)
794 defp autofollow_users(user) do
795 candidates = Config.get([:instance, :autofollowed_nicknames])
798 User.Query.build(%{nickname: candidates, local: true, is_active: true})
801 follow_all(user, autofollowed_users)
804 defp autofollowing_users(user) do
805 candidates = Config.get([:instance, :autofollowing_nicknames])
807 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
809 |> Enum.each(&follow(&1, user, :follow_accept))
814 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
815 def register(%Ecto.Changeset{} = changeset) do
816 with {:ok, user} <- Repo.insert(changeset) do
817 post_register_action(user)
821 def post_register_action(%User{is_confirmed: false} = user) do
822 with {:ok, _} <- maybe_send_confirmation_email(user) do
827 def post_register_action(%User{is_approved: false} = user) do
828 with {:ok, _} <- send_user_approval_email(user),
829 {:ok, _} <- send_admin_approval_emails(user) do
834 def post_register_action(%User{is_approved: true, is_confirmed: true} = user) do
835 with {:ok, user} <- autofollow_users(user),
836 {:ok, _} <- autofollowing_users(user),
837 {:ok, user} <- set_cache(user),
838 {:ok, _} <- maybe_send_registration_email(user),
839 {:ok, _} <- maybe_send_welcome_email(user),
840 {:ok, _} <- maybe_send_welcome_message(user),
841 {:ok, _} <- maybe_send_welcome_chat_message(user) do
846 defp send_user_approval_email(user) do
848 |> Pleroma.Emails.UserEmail.approval_pending_email()
849 |> Pleroma.Emails.Mailer.deliver_async()
854 defp send_admin_approval_emails(user) do
856 |> Enum.filter(fn user -> not is_nil(user.email) end)
857 |> Enum.each(fn superuser ->
859 |> Pleroma.Emails.AdminEmail.new_unapproved_registration(user)
860 |> Pleroma.Emails.Mailer.deliver_async()
866 defp maybe_send_welcome_message(user) do
867 if User.WelcomeMessage.enabled?() do
868 User.WelcomeMessage.post_message(user)
875 defp maybe_send_welcome_chat_message(user) do
876 if User.WelcomeChatMessage.enabled?() do
877 User.WelcomeChatMessage.post_message(user)
884 defp maybe_send_welcome_email(%User{email: email} = user) when is_binary(email) do
885 if User.WelcomeEmail.enabled?() do
886 User.WelcomeEmail.send_email(user)
893 defp maybe_send_welcome_email(_), do: {:ok, :noop}
895 @spec maybe_send_confirmation_email(User.t()) :: {:ok, :enqueued | :noop}
896 def maybe_send_confirmation_email(%User{is_confirmed: false, email: email} = user)
897 when is_binary(email) do
898 if Config.get([:instance, :account_activation_required]) do
899 send_confirmation_email(user)
906 def maybe_send_confirmation_email(_), do: {:ok, :noop}
908 @spec send_confirmation_email(Uset.t()) :: User.t()
909 def send_confirmation_email(%User{} = user) do
911 |> Pleroma.Emails.UserEmail.account_confirmation_email()
912 |> Pleroma.Emails.Mailer.deliver_async()
917 @spec maybe_send_registration_email(User.t()) :: {:ok, :enqueued | :noop}
918 defp maybe_send_registration_email(%User{email: email} = user) when is_binary(email) do
919 with false <- User.WelcomeEmail.enabled?(),
920 false <- Config.get([:instance, :account_activation_required], false),
921 false <- Config.get([:instance, :account_approval_required], false) do
923 |> Pleroma.Emails.UserEmail.successful_registration_email()
924 |> Pleroma.Emails.Mailer.deliver_async()
933 defp maybe_send_registration_email(_), do: {:ok, :noop}
935 def needs_update?(%User{local: true}), do: false
937 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
939 def needs_update?(%User{local: false} = user) do
940 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
943 def needs_update?(_), do: true
945 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
947 # "Locked" (self-locked) users demand explicit authorization of follow requests
948 def maybe_direct_follow(%User{} = follower, %User{local: true, is_locked: true} = followed) do
949 follow(follower, followed, :follow_pending)
952 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
953 follow(follower, followed)
956 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
957 if not ap_enabled?(followed) do
958 follow(follower, followed)
960 {:ok, follower, followed}
964 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
965 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
966 def follow_all(follower, followeds) do
968 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
969 |> Enum.each(&follow(follower, &1, :follow_accept))
974 def follow(%User{} = follower, %User{} = followed, state \\ :follow_accept) do
975 deny_follow_blocked = Config.get([:user, :deny_follow_blocked])
978 not followed.is_active ->
979 {:error, "Could not follow user: #{followed.nickname} is deactivated."}
981 deny_follow_blocked and blocks?(followed, follower) ->
982 {:error, "Could not follow user: #{followed.nickname} blocked you."}
985 FollowingRelationship.follow(follower, followed, state)
989 def unfollow(%User{ap_id: ap_id}, %User{ap_id: ap_id}) do
990 {:error, "Not subscribed!"}
993 @spec unfollow(User.t(), User.t()) :: {:ok, User.t(), Activity.t()} | {:error, String.t()}
994 def unfollow(%User{} = follower, %User{} = followed) do
995 case do_unfollow(follower, followed) do
996 {:ok, follower, followed} ->
997 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
1004 @spec do_unfollow(User.t(), User.t()) :: {:ok, User.t(), User.t()} | {:error, String.t()}
1005 defp do_unfollow(%User{} = follower, %User{} = followed) do
1006 case get_follow_state(follower, followed) do
1007 state when state in [:follow_pending, :follow_accept] ->
1008 FollowingRelationship.unfollow(follower, followed)
1011 {:error, "Not subscribed!"}
1015 @doc "Returns follow state as Pleroma.FollowingRelationship.State value"
1016 def get_follow_state(%User{} = follower, %User{} = following) do
1017 following_relationship = FollowingRelationship.get(follower, following)
1018 get_follow_state(follower, following, following_relationship)
1021 def get_follow_state(
1023 %User{} = following,
1024 following_relationship
1026 case {following_relationship, following.local} do
1028 case Utils.fetch_latest_follow(follower, following) do
1029 %Activity{data: %{"state" => state}} when state in ["pending", "accept"] ->
1030 FollowingRelationship.state_to_enum(state)
1036 {%{state: state}, _} ->
1044 def locked?(%User{} = user) do
1045 user.is_locked || false
1048 def get_by_id(id) do
1049 Repo.get_by(User, id: id)
1052 def get_by_ap_id(ap_id) do
1053 Repo.get_by(User, ap_id: ap_id)
1056 def get_all_by_ap_id(ap_ids) do
1057 from(u in __MODULE__,
1058 where: u.ap_id in ^ap_ids
1063 def get_all_by_ids(ids) do
1064 from(u in __MODULE__, where: u.id in ^ids)
1068 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
1069 # of the ap_id and the domain and tries to get that user
1070 def get_by_guessed_nickname(ap_id) do
1071 domain = URI.parse(ap_id).host
1072 name = List.last(String.split(ap_id, "/"))
1073 nickname = "#{name}@#{domain}"
1075 get_cached_by_nickname(nickname)
1078 def set_cache({:ok, user}), do: set_cache(user)
1079 def set_cache({:error, err}), do: {:error, err}
1081 def set_cache(%User{} = user) do
1082 @cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
1083 @cachex.put(:user_cache, "nickname:#{user.nickname}", user)
1084 @cachex.put(:user_cache, "friends_ap_ids:#{user.nickname}", get_user_friends_ap_ids(user))
1088 def update_and_set_cache(struct, params) do
1090 |> update_changeset(params)
1091 |> update_and_set_cache()
1094 def update_and_set_cache(%{data: %Pleroma.User{} = user} = changeset) do
1095 was_superuser_before_update = User.superuser?(user)
1097 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
1100 |> maybe_remove_report_notifications(was_superuser_before_update)
1103 defp maybe_remove_report_notifications({:ok, %Pleroma.User{} = user} = result, true) do
1104 if not User.superuser?(user),
1105 do: user |> Notification.destroy_multiple_from_types(["pleroma:report"])
1110 defp maybe_remove_report_notifications(result, _) do
1114 def get_user_friends_ap_ids(user) do
1115 from(u in User.get_friends_query(user), select: u.ap_id)
1119 @spec get_cached_user_friends_ap_ids(User.t()) :: [String.t()]
1120 def get_cached_user_friends_ap_ids(user) do
1121 @cachex.fetch!(:user_cache, "friends_ap_ids:#{user.ap_id}", fn _ ->
1122 get_user_friends_ap_ids(user)
1126 def invalidate_cache(user) do
1127 @cachex.del(:user_cache, "ap_id:#{user.ap_id}")
1128 @cachex.del(:user_cache, "nickname:#{user.nickname}")
1129 @cachex.del(:user_cache, "friends_ap_ids:#{user.ap_id}")
1130 @cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
1131 @cachex.del(:user_cache, "muted_users_ap_ids:#{user.ap_id}")
1134 @spec get_cached_by_ap_id(String.t()) :: User.t() | nil
1135 def get_cached_by_ap_id(ap_id) do
1136 key = "ap_id:#{ap_id}"
1138 with {:ok, nil} <- @cachex.get(:user_cache, key),
1139 user when not is_nil(user) <- get_by_ap_id(ap_id),
1140 {:ok, true} <- @cachex.put(:user_cache, key, user) do
1148 def get_cached_by_id(id) do
1152 @cachex.fetch!(:user_cache, key, fn _ ->
1153 user = get_by_id(id)
1156 @cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
1157 {:commit, user.ap_id}
1163 get_cached_by_ap_id(ap_id)
1166 def get_cached_by_nickname(nickname) do
1167 key = "nickname:#{nickname}"
1169 @cachex.fetch!(:user_cache, key, fn _ ->
1170 case get_or_fetch_by_nickname(nickname) do
1171 {:ok, user} -> {:commit, user}
1172 {:error, _error} -> {:ignore, nil}
1177 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
1178 restrict_to_local = Config.get([:instance, :limit_to_local_content])
1181 is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
1182 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
1184 restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
1185 get_cached_by_nickname(nickname_or_id)
1187 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
1188 get_cached_by_nickname(nickname_or_id)
1195 @spec get_by_nickname(String.t()) :: User.t() | nil
1196 def get_by_nickname(nickname) do
1197 Repo.get_by(User, nickname: nickname) ||
1198 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
1199 Repo.get_by(User, nickname: local_nickname(nickname))
1203 def get_by_email(email), do: Repo.get_by(User, email: email)
1205 def get_by_nickname_or_email(nickname_or_email) do
1206 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
1209 def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname)
1211 def get_or_fetch_by_nickname(nickname) do
1212 with %User{} = user <- get_by_nickname(nickname) do
1216 with [_nick, _domain] <- String.split(nickname, "@"),
1217 {:ok, user} <- fetch_by_nickname(nickname) do
1220 _e -> {:error, "not found " <> nickname}
1225 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
1226 def get_followers_query(%User{} = user, nil) do
1227 User.Query.build(%{followers: user, is_active: true})
1230 def get_followers_query(%User{} = user, page) do
1232 |> get_followers_query(nil)
1233 |> User.Query.paginate(page, 20)
1236 @spec get_followers_query(User.t()) :: Ecto.Query.t()
1237 def get_followers_query(%User{} = user), do: get_followers_query(user, nil)
1239 @spec get_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
1240 def get_followers(%User{} = user, page \\ nil) do
1242 |> get_followers_query(page)
1246 @spec get_external_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
1247 def get_external_followers(%User{} = user, page \\ nil) do
1249 |> get_followers_query(page)
1250 |> User.Query.build(%{external: true})
1254 def get_followers_ids(%User{} = user, page \\ nil) do
1256 |> get_followers_query(page)
1257 |> select([u], u.id)
1261 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
1262 def get_friends_query(%User{} = user, nil) do
1263 User.Query.build(%{friends: user, deactivated: false})
1266 def get_friends_query(%User{} = user, page) do
1268 |> get_friends_query(nil)
1269 |> User.Query.paginate(page, 20)
1272 @spec get_friends_query(User.t()) :: Ecto.Query.t()
1273 def get_friends_query(%User{} = user), do: get_friends_query(user, nil)
1275 def get_friends(%User{} = user, page \\ nil) do
1277 |> get_friends_query(page)
1281 def get_friends_ap_ids(%User{} = user) do
1283 |> get_friends_query(nil)
1284 |> select([u], u.ap_id)
1288 def get_friends_ids(%User{} = user, page \\ nil) do
1290 |> get_friends_query(page)
1291 |> select([u], u.id)
1295 def increase_note_count(%User{} = user) do
1297 |> where(id: ^user.id)
1298 |> update([u], inc: [note_count: 1])
1300 |> Repo.update_all([])
1302 {1, [user]} -> set_cache(user)
1307 def decrease_note_count(%User{} = user) do
1309 |> where(id: ^user.id)
1312 note_count: fragment("greatest(0, note_count - 1)")
1316 |> Repo.update_all([])
1318 {1, [user]} -> set_cache(user)
1323 def update_note_count(%User{} = user, note_count \\ nil) do
1328 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
1334 |> cast(%{note_count: note_count}, [:note_count])
1335 |> update_and_set_cache()
1338 @spec maybe_fetch_follow_information(User.t()) :: User.t()
1339 def maybe_fetch_follow_information(user) do
1340 with {:ok, user} <- fetch_follow_information(user) do
1344 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
1350 def fetch_follow_information(user) do
1351 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
1353 |> follow_information_changeset(info)
1354 |> update_and_set_cache()
1358 defp follow_information_changeset(user, params) do
1365 :hide_followers_count,
1370 @spec update_follower_count(User.t()) :: {:ok, User.t()}
1371 def update_follower_count(%User{} = user) do
1372 if user.local or !Config.get([:instance, :external_user_synchronization]) do
1373 follower_count = FollowingRelationship.follower_count(user)
1376 |> follow_information_changeset(%{follower_count: follower_count})
1377 |> update_and_set_cache
1379 {:ok, maybe_fetch_follow_information(user)}
1383 @spec update_following_count(User.t()) :: {:ok, User.t()}
1384 def update_following_count(%User{local: false} = user) do
1385 if Config.get([:instance, :external_user_synchronization]) do
1386 {:ok, maybe_fetch_follow_information(user)}
1392 def update_following_count(%User{local: true} = user) do
1393 following_count = FollowingRelationship.following_count(user)
1396 |> follow_information_changeset(%{following_count: following_count})
1397 |> update_and_set_cache()
1400 @spec get_users_from_set([String.t()], keyword()) :: [User.t()]
1401 def get_users_from_set(ap_ids, opts \\ []) do
1402 local_only = Keyword.get(opts, :local_only, true)
1403 criteria = %{ap_id: ap_ids, is_active: true}
1404 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
1406 User.Query.build(criteria)
1410 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
1411 def get_recipients_from_activity(%Activity{recipients: to, actor: actor}) do
1414 query = User.Query.build(%{recipients_from_activity: to, local: true, is_active: true})
1420 @spec mute(User.t(), User.t(), map()) ::
1421 {:ok, list(UserRelationship.t())} | {:error, String.t()}
1422 def mute(%User{} = muter, %User{} = mutee, params \\ %{}) do
1423 notifications? = Map.get(params, :notifications, true)
1424 expires_in = Map.get(params, :expires_in, 0)
1426 with {:ok, user_mute} <- UserRelationship.create_mute(muter, mutee),
1427 {:ok, user_notification_mute} <-
1428 (notifications? && UserRelationship.create_notification_mute(muter, mutee)) ||
1430 if expires_in > 0 do
1431 Pleroma.Workers.MuteExpireWorker.enqueue(
1433 %{"muter_id" => muter.id, "mutee_id" => mutee.id},
1434 schedule_in: expires_in
1438 @cachex.del(:user_cache, "muted_users_ap_ids:#{muter.ap_id}")
1440 {:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
1444 def unmute(%User{} = muter, %User{} = mutee) do
1445 with {:ok, user_mute} <- UserRelationship.delete_mute(muter, mutee),
1446 {:ok, user_notification_mute} <-
1447 UserRelationship.delete_notification_mute(muter, mutee) do
1448 @cachex.del(:user_cache, "muted_users_ap_ids:#{muter.ap_id}")
1449 {:ok, [user_mute, user_notification_mute]}
1453 def unmute(muter_id, mutee_id) do
1454 with {:muter, %User{} = muter} <- {:muter, User.get_by_id(muter_id)},
1455 {:mutee, %User{} = mutee} <- {:mutee, User.get_by_id(mutee_id)} do
1456 unmute(muter, mutee)
1458 {who, result} = error ->
1460 "User.unmute/2 failed. #{who}: #{result}, muter_id: #{muter_id}, mutee_id: #{mutee_id}"
1467 def subscribe(%User{} = subscriber, %User{} = target) do
1468 deny_follow_blocked = Config.get([:user, :deny_follow_blocked])
1470 if blocks?(target, subscriber) and deny_follow_blocked do
1471 {:error, "Could not subscribe: #{target.nickname} is blocking you"}
1473 # Note: the relationship is inverse: subscriber acts as relationship target
1474 UserRelationship.create_inverse_subscription(target, subscriber)
1478 def subscribe(%User{} = subscriber, %{ap_id: ap_id}) do
1479 with %User{} = subscribee <- get_cached_by_ap_id(ap_id) do
1480 subscribe(subscriber, subscribee)
1484 def unsubscribe(%User{} = unsubscriber, %User{} = target) do
1485 # Note: the relationship is inverse: subscriber acts as relationship target
1486 UserRelationship.delete_inverse_subscription(target, unsubscriber)
1489 def unsubscribe(%User{} = unsubscriber, %{ap_id: ap_id}) do
1490 with %User{} = user <- get_cached_by_ap_id(ap_id) do
1491 unsubscribe(unsubscriber, user)
1495 def block(%User{} = blocker, %User{} = blocked) do
1496 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
1498 if following?(blocker, blocked) do
1499 {:ok, blocker, _} = unfollow(blocker, blocked)
1505 # clear any requested follows from both sides as well
1507 case CommonAPI.reject_follow_request(blocked, blocker) do
1508 {:ok, %User{} = updated_blocked} -> updated_blocked
1513 case CommonAPI.reject_follow_request(blocker, blocked) do
1514 {:ok, %User{} = updated_blocker} -> updated_blocker
1518 unsubscribe(blocked, blocker)
1520 unfollowing_blocked = Config.get([:activitypub, :unfollow_blocked], true)
1521 if unfollowing_blocked && following?(blocked, blocker), do: unfollow(blocked, blocker)
1523 {:ok, blocker} = update_follower_count(blocker)
1524 {:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
1525 add_to_block(blocker, blocked)
1528 # helper to handle the block given only an actor's AP id
1529 def block(%User{} = blocker, %{ap_id: ap_id}) do
1530 block(blocker, get_cached_by_ap_id(ap_id))
1533 def unblock(%User{} = blocker, %User{} = blocked) do
1534 remove_from_block(blocker, blocked)
1537 # helper to handle the block given only an actor's AP id
1538 def unblock(%User{} = blocker, %{ap_id: ap_id}) do
1539 unblock(blocker, get_cached_by_ap_id(ap_id))
1542 def mutes?(nil, _), do: false
1543 def mutes?(%User{} = user, %User{} = target), do: mutes_user?(user, target)
1545 def mutes_user?(%User{} = user, %User{} = target) do
1546 UserRelationship.mute_exists?(user, target)
1549 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
1550 def muted_notifications?(nil, _), do: false
1552 def muted_notifications?(%User{} = user, %User{} = target),
1553 do: UserRelationship.notification_mute_exists?(user, target)
1555 def blocks?(nil, _), do: false
1557 def blocks?(%User{} = user, %User{} = target) do
1558 blocks_user?(user, target) ||
1559 (blocks_domain?(user, target) and not User.following?(user, target))
1562 def blocks_user?(%User{} = user, %User{} = target) do
1563 UserRelationship.block_exists?(user, target)
1566 def blocks_user?(_, _), do: false
1568 def blocks_domain?(%User{} = user, %User{} = target) do
1569 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
1570 %{host: host} = URI.parse(target.ap_id)
1571 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1574 def blocks_domain?(_, _), do: false
1576 def subscribed_to?(%User{} = user, %User{} = target) do
1577 # Note: the relationship is inverse: subscriber acts as relationship target
1578 UserRelationship.inverse_subscription_exists?(target, user)
1581 def subscribed_to?(%User{} = user, %{ap_id: ap_id}) do
1582 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1583 subscribed_to?(user, target)
1588 Returns map of outgoing (blocked, muted etc.) relationships' user AP IDs by relation type.
1589 E.g. `outgoing_relationships_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}`
1591 @spec outgoing_relationships_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())}
1592 def outgoing_relationships_ap_ids(_user, []), do: %{}
1594 def outgoing_relationships_ap_ids(nil, _relationship_types), do: %{}
1596 def outgoing_relationships_ap_ids(%User{} = user, relationship_types)
1597 when is_list(relationship_types) do
1600 |> assoc(:outgoing_relationships)
1601 |> join(:inner, [user_rel], u in assoc(user_rel, :target))
1602 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1603 |> select([user_rel, u], [user_rel.relationship_type, fragment("array_agg(?)", u.ap_id)])
1604 |> group_by([user_rel, u], user_rel.relationship_type)
1606 |> Enum.into(%{}, fn [k, v] -> {k, v} end)
1611 fn rel_type -> {rel_type, db_result[rel_type] || []} end
1615 def incoming_relationships_ungrouped_ap_ids(user, relationship_types, ap_ids \\ nil)
1617 def incoming_relationships_ungrouped_ap_ids(_user, [], _ap_ids), do: []
1619 def incoming_relationships_ungrouped_ap_ids(nil, _relationship_types, _ap_ids), do: []
1621 def incoming_relationships_ungrouped_ap_ids(%User{} = user, relationship_types, ap_ids)
1622 when is_list(relationship_types) do
1624 |> assoc(:incoming_relationships)
1625 |> join(:inner, [user_rel], u in assoc(user_rel, :source))
1626 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1627 |> maybe_filter_on_ap_id(ap_ids)
1628 |> select([user_rel, u], u.ap_id)
1633 defp maybe_filter_on_ap_id(query, ap_ids) when is_list(ap_ids) do
1634 where(query, [user_rel, u], u.ap_id in ^ap_ids)
1637 defp maybe_filter_on_ap_id(query, _ap_ids), do: query
1639 def set_activation_async(user, status \\ true) do
1640 BackgroundWorker.enqueue("user_activation", %{"user_id" => user.id, "status" => status})
1643 @spec set_activation([User.t()], boolean()) :: {:ok, User.t()} | {:error, Changeset.t()}
1644 def set_activation(users, status) when is_list(users) do
1645 Repo.transaction(fn ->
1646 for user <- users, do: set_activation(user, status)
1650 @spec set_activation(User.t(), boolean()) :: {:ok, User.t()} | {:error, Changeset.t()}
1651 def set_activation(%User{} = user, status) do
1652 with {:ok, user} <- set_activation_status(user, status) do
1655 |> Enum.filter(& &1.local)
1656 |> Enum.each(&set_cache(update_following_count(&1)))
1658 # Only update local user counts, remote will be update during the next pull.
1661 |> Enum.filter(& &1.local)
1662 |> Enum.each(&do_unfollow(user, &1))
1668 def approve(users) when is_list(users) do
1669 Repo.transaction(fn ->
1670 Enum.map(users, fn user ->
1671 with {:ok, user} <- approve(user), do: user
1676 def approve(%User{is_approved: false} = user) do
1677 with chg <- change(user, is_approved: true),
1678 {:ok, user} <- update_and_set_cache(chg) do
1679 post_register_action(user)
1684 def approve(%User{} = user), do: {:ok, user}
1686 def confirm(users) when is_list(users) do
1687 Repo.transaction(fn ->
1688 Enum.map(users, fn user ->
1689 with {:ok, user} <- confirm(user), do: user
1694 def confirm(%User{is_confirmed: false} = user) do
1695 with chg <- confirmation_changeset(user, set_confirmation: true),
1696 {:ok, user} <- update_and_set_cache(chg) do
1697 post_register_action(user)
1702 def confirm(%User{} = user), do: {:ok, user}
1704 def set_suggestion(users, is_suggested) when is_list(users) do
1705 Repo.transaction(fn ->
1706 Enum.map(users, fn user ->
1707 with {:ok, user} <- set_suggestion(user, is_suggested), do: user
1712 def set_suggestion(%User{is_suggested: is_suggested} = user, is_suggested), do: {:ok, user}
1714 def set_suggestion(%User{} = user, is_suggested) when is_boolean(is_suggested) do
1716 |> change(is_suggested: is_suggested)
1717 |> update_and_set_cache()
1720 def update_notification_settings(%User{} = user, settings) do
1722 |> cast(%{notification_settings: settings}, [])
1723 |> cast_embed(:notification_settings)
1724 |> validate_required([:notification_settings])
1725 |> update_and_set_cache()
1728 @spec purge_user_changeset(User.t()) :: Changeset.t()
1729 def purge_user_changeset(user) do
1730 # "Right to be forgotten"
1731 # https://gdpr.eu/right-to-be-forgotten/
1740 last_refreshed_at: nil,
1741 last_digest_emailed_at: nil,
1748 password_reset_pending: false,
1749 registration_reason: nil,
1750 confirmation_token: nil,
1754 is_moderator: false,
1756 mastofe_settings: nil,
1759 pleroma_settings_store: %{},
1762 is_discoverable: false,
1766 # nickname: preserved
1770 # Purge doesn't delete the user from the database.
1771 # It just nulls all its fields and deactivates it.
1772 # See `User.purge_user_changeset/1` above.
1773 defp purge(%User{} = user) do
1775 |> purge_user_changeset()
1776 |> update_and_set_cache()
1779 def delete(users) when is_list(users) do
1780 for user <- users, do: delete(user)
1783 def delete(%User{} = user) do
1784 # Purge the user immediately
1786 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1789 # *Actually* delete the user from the DB
1790 defp delete_from_db(%User{} = user) do
1791 invalidate_cache(user)
1795 # If the user never finalized their account, it's safe to delete them.
1796 defp maybe_delete_from_db(%User{local: true, is_confirmed: false} = user),
1797 do: delete_from_db(user)
1799 defp maybe_delete_from_db(%User{local: true, is_approved: false} = user),
1800 do: delete_from_db(user)
1802 defp maybe_delete_from_db(user), do: {:ok, user}
1804 def perform(:force_password_reset, user), do: force_password_reset(user)
1806 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1807 def perform(:delete, %User{} = user) do
1808 # Purge the user again, in case perform/2 is called directly
1811 # Remove all relationships
1814 |> Enum.each(fn follower ->
1815 ActivityPub.unfollow(follower, user)
1816 unfollow(follower, user)
1821 |> Enum.each(fn followed ->
1822 ActivityPub.unfollow(user, followed)
1823 unfollow(user, followed)
1826 delete_user_activities(user)
1827 delete_notifications_from_user_activities(user)
1828 delete_outgoing_pending_follow_requests(user)
1830 maybe_delete_from_db(user)
1833 def perform(:set_activation_async, user, status), do: set_activation(user, status)
1835 @spec external_users_query() :: Ecto.Query.t()
1836 def external_users_query do
1844 @spec external_users(keyword()) :: [User.t()]
1845 def external_users(opts \\ []) do
1847 external_users_query()
1848 |> select([u], struct(u, [:id, :ap_id]))
1852 do: where(query, [u], u.id > ^opts[:max_id]),
1857 do: limit(query, ^opts[:limit]),
1863 def delete_notifications_from_user_activities(%User{ap_id: ap_id}) do
1865 |> join(:inner, [n], activity in assoc(n, :activity))
1866 |> where([n, a], fragment("? = ?", a.actor, ^ap_id))
1867 |> Repo.delete_all()
1870 def delete_user_activities(%User{ap_id: ap_id} = user) do
1872 |> Activity.Queries.by_actor()
1873 |> Repo.chunk_stream(50, :batches)
1874 |> Stream.each(fn activities ->
1875 Enum.each(activities, fn activity -> delete_activity(activity, user) end)
1880 defp delete_activity(%{data: %{"type" => "Create", "object" => object}} = activity, user) do
1881 with {_, %Object{}} <- {:find_object, Object.get_by_ap_id(object)},
1882 {:ok, delete_data, _} <- Builder.delete(user, object) do
1883 Pipeline.common_pipeline(delete_data, local: user.local)
1885 {:find_object, nil} ->
1886 # We have the create activity, but not the object, it was probably pruned.
1887 # Insert a tombstone and try again
1888 with {:ok, tombstone_data, _} <- Builder.tombstone(user.ap_id, object),
1889 {:ok, _tombstone} <- Object.create(tombstone_data) do
1890 delete_activity(activity, user)
1894 Logger.error("Could not delete #{object} created by #{activity.data["ap_id"]}")
1895 Logger.error("Error: #{inspect(e)}")
1899 defp delete_activity(%{data: %{"type" => type}} = activity, user)
1900 when type in ["Like", "Announce"] do
1901 {:ok, undo, _} = Builder.undo(user, activity)
1902 Pipeline.common_pipeline(undo, local: user.local)
1905 defp delete_activity(_activity, _user), do: "Doing nothing"
1907 defp delete_outgoing_pending_follow_requests(user) do
1909 |> FollowingRelationship.outgoing_pending_follow_requests_query()
1910 |> Repo.delete_all()
1913 def html_filter_policy(%User{no_rich_text: true}) do
1914 Pleroma.HTML.Scrubber.TwitterText
1917 def html_filter_policy(_), do: Config.get([:markup, :scrub_policy])
1919 def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
1921 def get_or_fetch_by_ap_id(ap_id) do
1922 cached_user = get_cached_by_ap_id(ap_id)
1924 maybe_fetched_user = needs_update?(cached_user) && fetch_by_ap_id(ap_id)
1926 case {cached_user, maybe_fetched_user} do
1927 {_, {:ok, %User{} = user}} ->
1930 {%User{} = user, _} ->
1934 {:error, :not_found}
1939 Creates an internal service actor by URI if missing.
1940 Optionally takes nickname for addressing.
1942 @spec get_or_create_service_actor_by_ap_id(String.t(), String.t()) :: User.t() | nil
1943 def get_or_create_service_actor_by_ap_id(uri, nickname) do
1945 case get_cached_by_ap_id(uri) do
1947 with {:error, %{errors: errors}} <- create_service_actor(uri, nickname) do
1948 Logger.error("Cannot create service actor: #{uri}/.\n#{inspect(errors)}")
1952 %User{invisible: false} = user ->
1962 @spec set_invisible(User.t()) :: {:ok, User.t()}
1963 defp set_invisible(user) do
1965 |> change(%{invisible: true})
1966 |> update_and_set_cache()
1969 @spec create_service_actor(String.t(), String.t()) ::
1970 {:ok, User.t()} | {:error, Ecto.Changeset.t()}
1971 defp create_service_actor(uri, nickname) do
1977 follower_address: uri <> "/followers"
1980 |> unique_constraint(:nickname)
1985 def public_key(%{public_key: public_key_pem}) when is_binary(public_key_pem) do
1988 |> :public_key.pem_decode()
1990 |> :public_key.pem_entry_decode()
1995 def public_key(_), do: {:error, "key not found"}
1997 def get_public_key_for_ap_id(ap_id) do
1998 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1999 {:ok, public_key} <- public_key(user) do
2006 def ap_enabled?(%User{local: true}), do: true
2007 def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled
2008 def ap_enabled?(_), do: false
2010 @doc "Gets or fetch a user by uri or nickname."
2011 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
2012 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
2013 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
2015 # wait a period of time and return newest version of the User structs
2016 # this is because we have synchronous follow APIs and need to simulate them
2017 # with an async handshake
2018 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
2019 with %User{} = a <- get_cached_by_id(a.id),
2020 %User{} = b <- get_cached_by_id(b.id) do
2027 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
2028 with :ok <- :timer.sleep(timeout),
2029 %User{} = a <- get_cached_by_id(a.id),
2030 %User{} = b <- get_cached_by_id(b.id) do
2037 def parse_bio(bio) when is_binary(bio) and bio != "" do
2039 |> CommonUtils.format_input("text/plain", mentions_format: :full)
2043 def parse_bio(_), do: ""
2045 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
2046 # TODO: get profile URLs other than user.ap_id
2047 profile_urls = [user.ap_id]
2050 |> CommonUtils.format_input("text/plain",
2051 mentions_format: :full,
2052 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
2057 def parse_bio(_, _), do: ""
2059 def tag(user_identifiers, tags) when is_list(user_identifiers) do
2060 Repo.transaction(fn ->
2061 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
2065 def tag(nickname, tags) when is_binary(nickname),
2066 do: tag(get_by_nickname(nickname), tags)
2068 def tag(%User{} = user, tags),
2069 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
2071 def untag(user_identifiers, tags) when is_list(user_identifiers) do
2072 Repo.transaction(fn ->
2073 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
2077 def untag(nickname, tags) when is_binary(nickname),
2078 do: untag(get_by_nickname(nickname), tags)
2080 def untag(%User{} = user, tags),
2081 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
2083 defp update_tags(%User{} = user, new_tags) do
2084 {:ok, updated_user} =
2086 |> change(%{tags: new_tags})
2087 |> update_and_set_cache()
2092 defp normalize_tags(tags) do
2095 |> Enum.map(&String.downcase/1)
2098 defp local_nickname_regex do
2099 if Config.get([:instance, :extended_nickname_format]) do
2100 @extended_local_nickname_regex
2102 @strict_local_nickname_regex
2106 def local_nickname(nickname_or_mention) do
2109 |> String.split("@")
2113 def full_nickname(%User{} = user) do
2114 if String.contains?(user.nickname, "@") do
2117 %{host: host} = URI.parse(user.ap_id)
2118 user.nickname <> "@" <> host
2122 def full_nickname(nickname_or_mention),
2123 do: String.trim_leading(nickname_or_mention, "@")
2125 def error_user(ap_id) do
2129 nickname: "erroruser@example.com",
2130 inserted_at: NaiveDateTime.utc_now()
2134 @spec all_superusers() :: [User.t()]
2135 def all_superusers do
2136 User.Query.build(%{super_users: true, local: true, is_active: true})
2140 def muting_reblogs?(%User{} = user, %User{} = target) do
2141 UserRelationship.reblog_mute_exists?(user, target)
2144 def showing_reblogs?(%User{} = user, %User{} = target) do
2145 not muting_reblogs?(user, target)
2149 The function returns a query to get users with no activity for given interval of days.
2150 Inactive users are those who didn't read any notification, or had any activity where
2151 the user is the activity's actor, during `inactivity_threshold` days.
2152 Deactivated users will not appear in this list.
2156 iex> Pleroma.User.list_inactive_users()
2159 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
2160 def list_inactive_users_query(inactivity_threshold \\ 7) do
2161 negative_inactivity_threshold = -inactivity_threshold
2162 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
2163 # Subqueries are not supported in `where` clauses, join gets too complicated.
2164 has_read_notifications =
2165 from(n in Pleroma.Notification,
2166 where: n.seen == true,
2168 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
2171 |> Pleroma.Repo.all()
2173 from(u in Pleroma.User,
2174 left_join: a in Pleroma.Activity,
2175 on: u.ap_id == a.actor,
2176 where: not is_nil(u.nickname),
2177 where: u.is_active == ^true,
2178 where: u.id not in ^has_read_notifications,
2181 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
2182 is_nil(max(a.inserted_at))
2187 Enable or disable email notifications for user
2191 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => false}}, "digest", true)
2192 Pleroma.User{email_notifications: %{"digest" => true}}
2194 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => true}}, "digest", false)
2195 Pleroma.User{email_notifications: %{"digest" => false}}
2197 @spec switch_email_notifications(t(), String.t(), boolean()) ::
2198 {:ok, t()} | {:error, Ecto.Changeset.t()}
2199 def switch_email_notifications(user, type, status) do
2200 User.update_email_notifications(user, %{type => status})
2204 Set `last_digest_emailed_at` value for the user to current time
2206 @spec touch_last_digest_emailed_at(t()) :: t()
2207 def touch_last_digest_emailed_at(user) do
2208 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
2210 {:ok, updated_user} =
2212 |> change(%{last_digest_emailed_at: now})
2213 |> update_and_set_cache()
2218 @spec set_confirmation(User.t(), boolean()) :: {:ok, User.t()} | {:error, Changeset.t()}
2219 def set_confirmation(%User{} = user, bool) do
2221 |> confirmation_changeset(set_confirmation: bool)
2222 |> update_and_set_cache()
2225 def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
2229 def get_mascot(%{mascot: mascot}) when is_nil(mascot) do
2230 # use instance-default
2231 config = Config.get([:assets, :mascots])
2232 default_mascot = Config.get([:assets, :default_mascot])
2233 mascot = Keyword.get(config, default_mascot)
2236 "id" => "default-mascot",
2237 "url" => mascot[:url],
2238 "preview_url" => mascot[:url],
2240 "mime_type" => mascot[:mime_type]
2245 def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
2247 def ensure_keys_present(%User{} = user) do
2248 with {:ok, pem} <- Keys.generate_rsa_pem() do
2250 |> cast(%{keys: pem}, [:keys])
2251 |> validate_required([:keys])
2252 |> update_and_set_cache()
2256 def get_ap_ids_by_nicknames(nicknames) do
2258 where: u.nickname in ^nicknames,
2264 defp put_password_hash(
2265 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
2267 change(changeset, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt(password))
2270 defp put_password_hash(changeset), do: changeset
2272 def is_internal_user?(%User{nickname: nil}), do: true
2273 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
2274 def is_internal_user?(_), do: false
2276 # A hack because user delete activities have a fake id for whatever reason
2277 # TODO: Get rid of this
2278 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
2280 def get_delivered_users_by_object_id(object_id) do
2282 inner_join: delivery in assoc(u, :deliveries),
2283 where: delivery.object_id == ^object_id
2288 def change_email(user, email) do
2290 |> cast(%{email: email}, [:email])
2291 |> maybe_validate_required_email(false)
2292 |> unique_constraint(:email)
2293 |> validate_format(:email, @email_regex)
2294 |> update_and_set_cache()
2297 def alias_users(user) do
2299 |> Enum.map(&User.get_cached_by_ap_id/1)
2300 |> Enum.filter(fn user -> user != nil end)
2303 def add_alias(user, new_alias_user) do
2304 current_aliases = user.also_known_as || []
2305 new_alias_ap_id = new_alias_user.ap_id
2307 if new_alias_ap_id in current_aliases do
2311 |> cast(%{also_known_as: current_aliases ++ [new_alias_ap_id]}, [:also_known_as])
2312 |> update_and_set_cache()
2316 def delete_alias(user, alias_user) do
2317 current_aliases = user.also_known_as || []
2318 alias_ap_id = alias_user.ap_id
2320 if alias_ap_id in current_aliases do
2322 |> cast(%{also_known_as: current_aliases -- [alias_ap_id]}, [:also_known_as])
2323 |> update_and_set_cache()
2325 {:error, :no_such_alias}
2329 # Internal function; public one is `deactivate/2`
2330 defp set_activation_status(user, status) do
2332 |> cast(%{is_active: status}, [:is_active])
2333 |> update_and_set_cache()
2336 def update_banner(user, banner) do
2338 |> cast(%{banner: banner}, [:banner])
2339 |> update_and_set_cache()
2342 def update_background(user, background) do
2344 |> cast(%{background: background}, [:background])
2345 |> update_and_set_cache()
2348 def validate_fields(changeset, remote? \\ false) do
2349 limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
2350 limit = Config.get([:instance, limit_name], 0)
2353 |> validate_length(:fields, max: limit)
2354 |> validate_change(:fields, fn :fields, fields ->
2355 if Enum.all?(fields, &valid_field?/1) do
2363 defp valid_field?(%{"name" => name, "value" => value}) do
2364 name_limit = Config.get([:instance, :account_field_name_length], 255)
2365 value_limit = Config.get([:instance, :account_field_value_length], 255)
2367 is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
2368 String.length(value) <= value_limit
2371 defp valid_field?(_), do: false
2373 defp truncate_field(%{"name" => name, "value" => value}) do
2375 String.split_at(name, Config.get([:instance, :account_field_name_length], 255))
2378 String.split_at(value, Config.get([:instance, :account_field_value_length], 255))
2380 %{"name" => name, "value" => value}
2383 def admin_api_update(user, params) do
2390 |> update_and_set_cache()
2393 @doc "Signs user out of all applications"
2394 def global_sign_out(user) do
2395 OAuth.Authorization.delete_user_authorizations(user)
2396 OAuth.Token.delete_user_tokens(user)
2399 def mascot_update(user, url) do
2401 |> cast(%{mascot: url}, [:mascot])
2402 |> validate_required([:mascot])
2403 |> update_and_set_cache()
2406 def mastodon_settings_update(user, settings) do
2408 |> cast(%{mastofe_settings: settings}, [:mastofe_settings])
2409 |> validate_required([:mastofe_settings])
2410 |> update_and_set_cache()
2413 @spec confirmation_changeset(User.t(), keyword()) :: Changeset.t()
2414 def confirmation_changeset(user, set_confirmation: confirmed?) do
2419 confirmation_token: nil
2423 is_confirmed: false,
2424 confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
2428 cast(user, params, [:is_confirmed, :confirmation_token])
2431 @spec approval_changeset(User.t(), keyword()) :: Changeset.t()
2432 def approval_changeset(user, set_approval: approved?) do
2433 cast(user, %{is_approved: approved?}, [:is_approved])
2436 @spec add_pinned_object_id(User.t(), String.t()) :: {:ok, User.t()} | {:error, term()}
2437 def add_pinned_object_id(%User{} = user, object_id) do
2438 if !user.pinned_objects[object_id] do
2439 params = %{pinned_objects: Map.put(user.pinned_objects, object_id, NaiveDateTime.utc_now())}
2442 |> cast(params, [:pinned_objects])
2443 |> validate_change(:pinned_objects, fn :pinned_objects, pinned_objects ->
2444 max_pinned_statuses = Config.get([:instance, :max_pinned_statuses], 0)
2446 if Enum.count(pinned_objects) <= max_pinned_statuses do
2449 [pinned_objects: "You have already pinned the maximum number of statuses"]
2455 |> update_and_set_cache()
2458 @spec remove_pinned_object_id(User.t(), String.t()) :: {:ok, t()} | {:error, term()}
2459 def remove_pinned_object_id(%User{} = user, object_id) do
2462 %{pinned_objects: Map.delete(user.pinned_objects, object_id)},
2465 |> update_and_set_cache()
2468 def update_email_notifications(user, settings) do
2469 email_notifications =
2470 user.email_notifications
2471 |> Map.merge(settings)
2472 |> Map.take(["digest"])
2474 params = %{email_notifications: email_notifications}
2475 fields = [:email_notifications]
2478 |> cast(params, fields)
2479 |> validate_required(fields)
2480 |> update_and_set_cache()
2483 defp set_domain_blocks(user, domain_blocks) do
2484 params = %{domain_blocks: domain_blocks}
2487 |> cast(params, [:domain_blocks])
2488 |> validate_required([:domain_blocks])
2489 |> update_and_set_cache()
2492 def block_domain(user, domain_blocked) do
2493 set_domain_blocks(user, Enum.uniq([domain_blocked | user.domain_blocks]))
2496 def unblock_domain(user, domain_blocked) do
2497 set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked))
2500 @spec add_to_block(User.t(), User.t()) ::
2501 {:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()}
2502 defp add_to_block(%User{} = user, %User{} = blocked) do
2503 with {:ok, relationship} <- UserRelationship.create_block(user, blocked) do
2504 @cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
2509 @spec add_to_block(User.t(), User.t()) ::
2510 {:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()}
2511 defp remove_from_block(%User{} = user, %User{} = blocked) do
2512 with {:ok, relationship} <- UserRelationship.delete_block(user, blocked) do
2513 @cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
2518 def set_invisible(user, invisible) do
2519 params = %{invisible: invisible}
2522 |> cast(params, [:invisible])
2523 |> validate_required([:invisible])
2524 |> update_and_set_cache()
2527 def sanitize_html(%User{} = user) do
2528 sanitize_html(user, nil)
2531 # User data that mastodon isn't filtering (treated as plaintext):
2534 def sanitize_html(%User{} = user, filter) do
2536 Enum.map(user.fields, fn %{"name" => name, "value" => value} ->
2539 "value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
2544 |> Map.put(:bio, HTML.filter_tags(user.bio, filter))
2545 |> Map.put(:fields, fields)
2548 def get_host(%User{ap_id: ap_id} = _user) do
2549 URI.parse(ap_id).host
2552 def update_last_active_at(%__MODULE__{local: true} = user) do
2554 |> cast(%{last_active_at: NaiveDateTime.utc_now()}, [:last_active_at])
2555 |> update_and_set_cache()
2558 def active_user_count(days \\ 30) do
2559 active_after = Timex.shift(NaiveDateTime.utc_now(), days: -days)
2562 |> where([u], u.last_active_at >= ^active_after)
2563 |> where([u], u.local == true)
2564 |> Repo.aggregate(:count)
2567 def update_last_status_at(user) do
2569 |> where(id: ^user.id)
2570 |> update([u], set: [last_status_at: fragment("NOW()")])
2572 |> Repo.update_all([])
2574 {1, [user]} -> set_cache(user)