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(:last_active_at, :naive_datetime)
149 field(:disclose_client, :boolean, default: true)
150 field(:pinned_objects, :map, default: %{})
151 field(:is_suggested, :boolean, default: false)
152 field(:last_status_at, :naive_datetime)
153 field(:language, :string)
154 field(:status_ttl_days, :integer, default: nil)
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 has_many(:frontend_profiles, Pleroma.Akkoma.FrontendSettingsProfile)
171 for {relationship_type,
173 {outgoing_relation, outgoing_relation_target},
174 {incoming_relation, incoming_relation_source}
175 ]} <- @user_relationships_config do
176 # Definitions of `has_many` relations: :blocker_blocks, :muter_mutes, :reblog_muter_mutes,
177 # :notification_muter_mutes, :subscribee_subscriptions
178 has_many(outgoing_relation, UserRelationship,
179 foreign_key: :source_id,
180 where: [relationship_type: relationship_type]
183 # Definitions of `has_many` relations: :blockee_blocks, :mutee_mutes, :reblog_mutee_mutes,
184 # :notification_mutee_mutes, :subscriber_subscriptions
185 has_many(incoming_relation, UserRelationship,
186 foreign_key: :target_id,
187 where: [relationship_type: relationship_type]
190 # Definitions of `has_many` relations: :blocked_users, :muted_users, :reblog_muted_users,
191 # :notification_muted_users, :subscriber_users
192 has_many(outgoing_relation_target, through: [outgoing_relation, :target])
194 # Definitions of `has_many` relations: :blocker_users, :muter_users, :reblog_muter_users,
195 # :notification_muter_users, :subscribee_users
196 has_many(incoming_relation_source, through: [incoming_relation, :source])
200 :multi_factor_authentication_settings,
208 for {_relationship_type, [{_outgoing_relation, outgoing_relation_target}, _]} <-
209 @user_relationships_config do
210 # `def blocked_users_relation/2`, `def muted_users_relation/2`,
211 # `def reblog_muted_users_relation/2`, `def notification_muted_users/2`,
212 # `def subscriber_users/2`
213 def unquote(:"#{outgoing_relation_target}_relation")(user, restrict_deactivated? \\ false) do
214 target_users_query = assoc(user, unquote(outgoing_relation_target))
216 if restrict_deactivated? do
218 |> User.Query.build(%{deactivated: false})
224 # `def blocked_users/2`, `def muted_users/2`, `def reblog_muted_users/2`,
225 # `def notification_muted_users/2`, `def subscriber_users/2`
226 def unquote(outgoing_relation_target)(user, restrict_deactivated? \\ false) do
228 |> apply(unquote(:"#{outgoing_relation_target}_relation"), [
230 restrict_deactivated?
235 # `def blocked_users_ap_ids/2`, `def muted_users_ap_ids/2`, `def reblog_muted_users_ap_ids/2`,
236 # `def notification_muted_users_ap_ids/2`, `def subscriber_users_ap_ids/2`
237 def unquote(:"#{outgoing_relation_target}_ap_ids")(user, restrict_deactivated? \\ false) do
239 |> apply(unquote(:"#{outgoing_relation_target}_relation"), [
241 restrict_deactivated?
243 |> select([u], u.ap_id)
248 def cached_blocked_users_ap_ids(user) do
249 @cachex.fetch!(:user_cache, "blocked_users_ap_ids:#{user.ap_id}", fn _ ->
250 blocked_users_ap_ids(user)
254 def cached_muted_users_ap_ids(user) do
255 @cachex.fetch!(:user_cache, "muted_users_ap_ids:#{user.ap_id}", fn _ ->
256 muted_users_ap_ids(user)
260 defdelegate following_count(user), to: FollowingRelationship
261 defdelegate following(user), to: FollowingRelationship
262 defdelegate following?(follower, followed), to: FollowingRelationship
263 defdelegate following_ap_ids(user), to: FollowingRelationship
264 defdelegate get_follow_requests(user), to: FollowingRelationship
265 defdelegate search(query, opts \\ []), to: User.Search
268 Dumps Flake Id to SQL-compatible format (16-byte UUID).
269 E.g. "9pQtDGXuq4p3VlcJEm" -> <<0, 0, 1, 110, 179, 218, 42, 92, 213, 41, 44, 227, 95, 213, 0, 0>>
271 def binary_id(source_id) when is_binary(source_id) do
272 with {:ok, dumped_id} <- FlakeId.Ecto.CompatType.dump(source_id) do
279 def binary_id(source_ids) when is_list(source_ids) do
280 Enum.map(source_ids, &binary_id/1)
283 def binary_id(%User{} = user), do: binary_id(user.id)
285 @doc "Returns status account"
286 @spec account_status(User.t()) :: account_status()
287 def account_status(%User{is_active: false}), do: :deactivated
288 def account_status(%User{password_reset_pending: true}), do: :password_reset_pending
289 def account_status(%User{local: true, is_approved: false}), do: :approval_pending
290 def account_status(%User{local: true, is_confirmed: false}), do: :confirmation_pending
291 def account_status(%User{}), do: :active
293 @spec visible_for(User.t(), User.t() | nil) ::
296 | :restricted_unauthenticated
298 | :confirmation_pending
299 def visible_for(user, for_user \\ nil)
301 def visible_for(%User{invisible: true}, _), do: :invisible
303 def visible_for(%User{id: user_id}, %User{id: user_id}), do: :visible
305 def visible_for(%User{} = user, nil) do
306 if restrict_unauthenticated?(user) do
307 :restrict_unauthenticated
309 visible_account_status(user)
313 def visible_for(%User{} = user, for_user) do
314 if superuser?(for_user) do
317 visible_account_status(user)
321 def visible_for(_, _), do: :invisible
323 defp restrict_unauthenticated?(%User{local: true}) do
324 Config.restrict_unauthenticated_access?(:profiles, :local)
327 defp restrict_unauthenticated?(%User{local: _}) do
328 Config.restrict_unauthenticated_access?(:profiles, :remote)
331 defp visible_account_status(user) do
332 status = account_status(user)
334 if status in [:active, :password_reset_pending] do
341 @spec superuser?(User.t()) :: boolean()
342 def superuser?(%User{local: true, is_admin: true}), do: true
343 def superuser?(%User{local: true, is_moderator: true}), do: true
344 def superuser?(_), do: false
346 @spec invisible?(User.t()) :: boolean()
347 def invisible?(%User{invisible: true}), do: true
348 def invisible?(_), do: false
350 def avatar_url(user, options \\ []) do
352 %{"url" => [%{"href" => href} | _]} ->
356 unless options[:no_default] do
357 Config.get([:assets, :default_user_avatar], "#{Endpoint.url()}/images/avi.png")
362 def banner_url(user, options \\ []) do
364 %{"url" => [%{"href" => href} | _]} -> href
365 _ -> !options[:no_default] && "#{Endpoint.url()}/images/banner.png"
369 # Should probably be renamed or removed
370 @spec ap_id(User.t()) :: String.t()
371 def ap_id(%User{nickname: nickname}), do: "#{Endpoint.url()}/users/#{nickname}"
373 @spec ap_followers(User.t()) :: String.t()
374 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
375 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
377 @spec ap_following(User.t()) :: String.t()
378 def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
379 def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
381 @spec ap_featured_collection(User.t()) :: String.t()
382 def ap_featured_collection(%User{featured_address: fa}) when is_binary(fa), do: fa
384 def ap_featured_collection(%User{} = user), do: "#{ap_id(user)}/collections/featured"
386 defp truncate_fields_param(params) do
387 if Map.has_key?(params, :fields) do
388 Map.put(params, :fields, Enum.map(params[:fields], &truncate_field/1))
394 defp truncate_if_exists(params, key, max_length) do
395 if Map.has_key?(params, key) and is_binary(params[key]) do
396 {value, _chopped} = String.split_at(params[key], max_length)
397 Map.put(params, key, value)
403 defp fix_follower_address(%{follower_address: _, following_address: _} = params), do: params
405 defp fix_follower_address(%{nickname: nickname} = params),
406 do: Map.put(params, :follower_address, ap_followers(%User{nickname: nickname}))
408 defp fix_follower_address(params), do: params
410 def remote_user_changeset(struct \\ %User{local: false}, params) do
411 bio_limit = Config.get([:instance, :user_bio_length], 5000)
412 name_limit = Config.get([:instance, :user_name_length], 100)
415 case params[:name] do
416 name when is_binary(name) and byte_size(name) > 0 -> name
417 _ -> params[:nickname]
422 |> Map.put(:name, name)
423 |> Map.put_new(:last_refreshed_at, NaiveDateTime.utc_now())
424 |> truncate_if_exists(:name, name_limit)
425 |> truncate_if_exists(:bio, bio_limit)
426 |> truncate_fields_param()
427 |> fix_follower_address()
451 :hide_followers_count,
463 |> cast(params, [:name], empty_values: [])
464 |> validate_required([:ap_id])
465 |> validate_required([:name], trim: false)
466 |> unique_constraint(:nickname)
467 |> validate_format(:nickname, @email_regex)
468 |> validate_length(:bio, max: bio_limit)
469 |> validate_length(:name, max: name_limit)
470 |> validate_fields(true)
471 |> validate_non_local()
474 defp validate_non_local(cng) do
475 local? = get_field(cng, :local)
479 |> add_error(:local, "User is local, can't update with this changeset.")
485 def update_changeset(struct, params \\ %{}) do
486 bio_limit = Config.get([:instance, :user_bio_length], 5000)
487 name_limit = Config.get([:instance, :user_name_length], 100)
507 :hide_followers_count,
510 :allow_following_move,
514 :skip_thread_containment,
517 :pleroma_settings_store,
524 |> unique_constraint(:nickname)
525 |> validate_format(:nickname, local_nickname_regex())
526 |> validate_length(:bio, max: bio_limit)
527 |> validate_length(:name, min: 1, max: name_limit)
528 |> validate_inclusion(:actor_type, ["Person", "Service"])
529 |> validate_number(:status_ttl_days, greater_than: 0)
532 |> put_change_if_present(:bio, &{:ok, parse_bio(&1, struct)})
533 |> put_change_if_present(:avatar, &put_upload(&1, :avatar))
534 |> put_change_if_present(:banner, &put_upload(&1, :banner))
535 |> put_change_if_present(:background, &put_upload(&1, :background))
536 |> put_change_if_present(
537 :pleroma_settings_store,
538 &{:ok, Map.merge(struct.pleroma_settings_store, &1)}
540 |> validate_fields(false)
543 defp put_fields(changeset) do
544 if raw_fields = get_change(changeset, :raw_fields) do
547 |> Enum.filter(fn %{"name" => n} -> n != "" end)
551 |> Enum.map(fn f -> Map.update!(f, "value", &parse_fields(&1)) end)
554 |> put_change(:raw_fields, raw_fields)
555 |> put_change(:fields, fields)
561 defp parse_fields(value) do
563 |> Formatter.linkify(mentions_format: :full)
567 defp put_emoji(changeset) do
568 emojified_fields = [:bio, :name, :raw_fields]
570 if Enum.any?(changeset.changes, fn {k, _} -> k in emojified_fields end) do
571 bio = Emoji.Formatter.get_emoji_map(get_field(changeset, :bio))
572 name = Emoji.Formatter.get_emoji_map(get_field(changeset, :name))
574 emoji = Map.merge(bio, name)
578 |> get_field(:raw_fields)
579 |> Enum.reduce(emoji, fn x, acc ->
580 Map.merge(acc, Emoji.Formatter.get_emoji_map(x["name"] <> x["value"]))
583 put_change(changeset, :emoji, emoji)
589 defp put_change_if_present(changeset, map_field, value_function) do
590 with {:ok, value} <- fetch_change(changeset, map_field),
591 {:ok, new_value} <- value_function.(value) do
592 put_change(changeset, map_field, new_value)
598 defp put_upload(value, type) do
599 with %Plug.Upload{} <- value,
600 {:ok, object} <- ActivityPub.upload(value, type: type) do
605 def update_as_admin_changeset(struct, params) do
607 |> update_changeset(params)
608 |> cast(params, [:email])
609 |> delete_change(:also_known_as)
610 |> unique_constraint(:email)
611 |> validate_format(:email, @email_regex)
612 |> validate_inclusion(:actor_type, ["Person", "Service"])
615 @spec update_as_admin(User.t(), map()) :: {:ok, User.t()} | {:error, Changeset.t()}
616 def update_as_admin(user, params) do
617 params = Map.put(params, "password_confirmation", params["password"])
618 changeset = update_as_admin_changeset(user, params)
620 if params["password"] do
621 reset_password(user, changeset, params)
623 User.update_and_set_cache(changeset)
627 def password_update_changeset(struct, params) do
629 |> cast(params, [:password, :password_confirmation])
630 |> validate_required([:password, :password_confirmation])
631 |> validate_confirmation(:password)
632 |> put_password_hash()
633 |> put_change(:password_reset_pending, false)
636 @spec reset_password(User.t(), map()) :: {:ok, User.t()} | {:error, Changeset.t()}
637 def reset_password(%User{} = user, params) do
638 reset_password(user, user, params)
641 def reset_password(%User{id: user_id} = user, struct, params) do
644 |> Multi.update(:user, password_update_changeset(struct, params))
645 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
646 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
648 case Repo.transaction(multi) do
649 {:ok, %{user: user} = _} -> set_cache(user)
650 {:error, _, changeset, _} -> {:error, changeset}
654 def update_password_reset_pending(user, value) do
657 |> put_change(:password_reset_pending, value)
658 |> update_and_set_cache()
661 def force_password_reset_async(user) do
662 BackgroundWorker.enqueue("force_password_reset", %{"user_id" => user.id})
665 @spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
666 def force_password_reset(user), do: update_password_reset_pending(user, true)
668 # Used to auto-register LDAP accounts which won't have a password hash stored locally
669 def register_changeset_ldap(struct, params = %{password: password})
670 when is_nil(password) do
672 if Map.has_key?(params, :email) do
673 Map.put_new(params, :email, params[:email])
684 |> validate_required([:name, :nickname])
685 |> unique_constraint(:nickname)
686 |> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames]))
687 |> validate_format(:nickname, local_nickname_regex())
689 |> unique_constraint(:ap_id)
690 |> put_following_and_follower_and_featured_address()
694 def register_changeset(struct, params \\ %{}, opts \\ []) do
695 bio_limit = Config.get([:instance, :user_bio_length], 5000)
696 name_limit = Config.get([:instance, :user_name_length], 100)
697 reason_limit = Config.get([:instance, :registration_reason_length], 500)
700 if is_nil(opts[:confirmed]) do
701 !Config.get([:instance, :account_activation_required])
707 if is_nil(opts[:approved]) do
708 !Config.get([:instance, :account_approval_required])
714 |> confirmation_changeset(set_confirmation: confirmed?)
715 |> approval_changeset(set_approval: approved?)
723 :password_confirmation,
725 :registration_reason,
728 |> validate_required([:name, :nickname, :password, :password_confirmation])
729 |> validate_confirmation(:password)
730 |> unique_constraint(:email)
731 |> validate_format(:email, @email_regex)
732 |> validate_change(:email, fn :email, email ->
734 Config.get([User, :email_blacklist])
735 |> Enum.all?(fn blacklisted_domain ->
736 !String.ends_with?(email, ["@" <> blacklisted_domain, "." <> blacklisted_domain])
739 if valid?, do: [], else: [email: "Invalid email"]
741 |> unique_constraint(:nickname)
742 |> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames]))
743 |> validate_format(:nickname, local_nickname_regex())
744 |> validate_length(:bio, max: bio_limit)
745 |> validate_length(:name, min: 1, max: name_limit)
746 |> validate_length(:registration_reason, max: reason_limit)
747 |> maybe_validate_required_email(opts[:external])
750 |> unique_constraint(:ap_id)
751 |> put_following_and_follower_and_featured_address()
755 def maybe_validate_required_email(changeset, true), do: changeset
757 def maybe_validate_required_email(changeset, _) do
758 if Config.get([:instance, :account_activation_required]) do
759 validate_required(changeset, [:email])
765 def put_ap_id(changeset) do
766 ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
767 put_change(changeset, :ap_id, ap_id)
770 def put_following_and_follower_and_featured_address(changeset) do
771 user = %User{nickname: get_field(changeset, :nickname)}
772 followers = ap_followers(user)
773 following = ap_following(user)
774 featured = ap_featured_collection(user)
777 |> put_change(:follower_address, followers)
778 |> put_change(:following_address, following)
779 |> put_change(:featured_address, featured)
782 defp put_private_key(changeset) do
783 {:ok, pem} = Keys.generate_rsa_pem()
784 put_change(changeset, :keys, pem)
787 defp autofollow_users(user) do
788 candidates = Config.get([:instance, :autofollowed_nicknames])
791 User.Query.build(%{nickname: candidates, local: true, is_active: true})
794 follow_all(user, autofollowed_users)
797 defp autofollowing_users(user) do
798 candidates = Config.get([:instance, :autofollowing_nicknames])
800 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
802 |> Enum.each(&follow(&1, user, :follow_accept))
807 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
808 def register(%Ecto.Changeset{} = changeset) do
809 with {:ok, user} <- Repo.insert(changeset) do
810 post_register_action(user)
814 def post_register_action(%User{is_confirmed: false} = user) do
815 with {:ok, _} <- maybe_send_confirmation_email(user) do
820 def post_register_action(%User{is_approved: false} = user) do
821 with {:ok, _} <- send_user_approval_email(user),
822 {:ok, _} <- send_admin_approval_emails(user) do
827 def post_register_action(%User{is_approved: true, is_confirmed: true} = user) do
828 with {:ok, user} <- autofollow_users(user),
829 {:ok, _} <- autofollowing_users(user),
830 {:ok, user} <- set_cache(user),
831 {:ok, _} <- maybe_send_registration_email(user),
832 {:ok, _} <- maybe_send_welcome_email(user),
833 {:ok, _} <- maybe_send_welcome_message(user) do
838 defp send_user_approval_email(user) do
840 |> Pleroma.Emails.UserEmail.approval_pending_email()
841 |> Pleroma.Emails.Mailer.deliver_async()
846 defp send_admin_approval_emails(user) do
848 |> Enum.filter(fn user -> not is_nil(user.email) end)
849 |> Enum.each(fn superuser ->
851 |> Pleroma.Emails.AdminEmail.new_unapproved_registration(user)
852 |> Pleroma.Emails.Mailer.deliver_async()
858 defp maybe_send_welcome_message(user) do
859 if User.WelcomeMessage.enabled?() do
860 User.WelcomeMessage.post_message(user)
867 defp maybe_send_welcome_email(%User{email: email} = user) when is_binary(email) do
868 if User.WelcomeEmail.enabled?() do
869 User.WelcomeEmail.send_email(user)
876 defp maybe_send_welcome_email(_), do: {:ok, :noop}
878 @spec maybe_send_confirmation_email(User.t()) :: {:ok, :enqueued | :noop}
879 def maybe_send_confirmation_email(%User{is_confirmed: false, email: email} = user)
880 when is_binary(email) do
881 if Config.get([:instance, :account_activation_required]) do
882 send_confirmation_email(user)
889 def maybe_send_confirmation_email(_), do: {:ok, :noop}
891 @spec send_confirmation_email(Uset.t()) :: User.t()
892 def send_confirmation_email(%User{} = user) do
894 |> Pleroma.Emails.UserEmail.account_confirmation_email()
895 |> Pleroma.Emails.Mailer.deliver_async()
900 @spec maybe_send_registration_email(User.t()) :: {:ok, :enqueued | :noop}
901 defp maybe_send_registration_email(%User{email: email} = user) when is_binary(email) do
902 with false <- User.WelcomeEmail.enabled?(),
903 false <- Config.get([:instance, :account_activation_required], false),
904 false <- Config.get([:instance, :account_approval_required], false) do
906 |> Pleroma.Emails.UserEmail.successful_registration_email()
907 |> Pleroma.Emails.Mailer.deliver_async()
916 defp maybe_send_registration_email(_), do: {:ok, :noop}
918 def needs_update?(%User{local: true}), do: false
920 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
922 def needs_update?(%User{local: false} = user) do
923 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
926 def needs_update?(_), do: true
928 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
930 # "Locked" (self-locked) users demand explicit authorization of follow requests
931 def maybe_direct_follow(%User{} = follower, %User{local: true, is_locked: true} = followed) do
932 follow(follower, followed, :follow_pending)
935 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
936 follow(follower, followed)
939 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
940 if not ap_enabled?(followed) do
941 follow(follower, followed)
943 {:ok, follower, followed}
947 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
948 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
949 def follow_all(follower, followeds) do
951 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
952 |> Enum.each(&follow(follower, &1, :follow_accept))
957 def follow(%User{} = follower, %User{} = followed, state \\ :follow_accept) do
958 deny_follow_blocked = Config.get([:user, :deny_follow_blocked])
961 not followed.is_active ->
962 {:error, "Could not follow user: #{followed.nickname} is deactivated."}
964 deny_follow_blocked and blocks?(followed, follower) ->
965 {:error, "Could not follow user: #{followed.nickname} blocked you."}
968 FollowingRelationship.follow(follower, followed, state)
972 def unfollow(%User{ap_id: ap_id}, %User{ap_id: ap_id}) do
973 {:error, "Not subscribed!"}
976 @spec unfollow(User.t(), User.t()) :: {:ok, User.t(), Activity.t()} | {:error, String.t()}
977 def unfollow(%User{} = follower, %User{} = followed) do
978 case do_unfollow(follower, followed) do
979 {:ok, follower, followed} ->
980 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
987 @spec do_unfollow(User.t(), User.t()) :: {:ok, User.t(), User.t()} | {:error, String.t()}
988 defp do_unfollow(%User{} = follower, %User{} = followed) do
989 case get_follow_state(follower, followed) do
990 state when state in [:follow_pending, :follow_accept] ->
991 FollowingRelationship.unfollow(follower, followed)
994 {:error, "Not subscribed!"}
998 @doc "Returns follow state as Pleroma.FollowingRelationship.State value"
999 def get_follow_state(%User{} = follower, %User{} = following) do
1000 following_relationship = FollowingRelationship.get(follower, following)
1001 get_follow_state(follower, following, following_relationship)
1004 def get_follow_state(
1006 %User{} = following,
1007 following_relationship
1009 case {following_relationship, following.local} do
1011 case Utils.fetch_latest_follow(follower, following) do
1012 %Activity{data: %{"state" => state}} when state in ["pending", "accept"] ->
1013 FollowingRelationship.state_to_enum(state)
1019 {%{state: state}, _} ->
1027 def locked?(%User{} = user) do
1028 user.is_locked || false
1031 def get_by_id(id) do
1032 Repo.get_by(User, id: id)
1035 def get_by_ap_id(ap_id) do
1036 Repo.get_by(User, ap_id: ap_id)
1039 def get_all_by_ap_id(ap_ids) do
1040 from(u in __MODULE__,
1041 where: u.ap_id in ^ap_ids
1046 def get_all_by_ids(ids) do
1047 from(u in __MODULE__, where: u.id in ^ids)
1051 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
1052 # of the ap_id and the domain and tries to get that user
1053 def get_by_guessed_nickname(ap_id) do
1054 domain = URI.parse(ap_id).host
1055 name = List.last(String.split(ap_id, "/"))
1056 nickname = "#{name}@#{domain}"
1058 get_cached_by_nickname(nickname)
1061 def set_cache({:ok, user}), do: set_cache(user)
1062 def set_cache({:error, err}), do: {:error, err}
1064 def set_cache(%User{} = user) do
1065 @cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
1066 @cachex.put(:user_cache, "nickname:#{user.nickname}", user)
1067 @cachex.put(:user_cache, "friends_ap_ids:#{user.nickname}", get_user_friends_ap_ids(user))
1071 def update_and_set_cache(struct, params) do
1073 |> update_changeset(params)
1074 |> update_and_set_cache()
1077 def update_and_set_cache(%{data: %Pleroma.User{} = user} = changeset) do
1078 was_superuser_before_update = User.superuser?(user)
1080 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
1083 |> maybe_remove_report_notifications(was_superuser_before_update)
1086 defp maybe_remove_report_notifications({:ok, %Pleroma.User{} = user} = result, true) do
1087 if not User.superuser?(user),
1088 do: user |> Notification.destroy_multiple_from_types(["pleroma:report"])
1093 defp maybe_remove_report_notifications(result, _) do
1097 def get_user_friends_ap_ids(user) do
1098 from(u in User.get_friends_query(user), select: u.ap_id)
1102 @spec get_cached_user_friends_ap_ids(User.t()) :: [String.t()]
1103 def get_cached_user_friends_ap_ids(user) do
1104 @cachex.fetch!(:user_cache, "friends_ap_ids:#{user.ap_id}", fn _ ->
1105 get_user_friends_ap_ids(user)
1109 def invalidate_cache(user) do
1110 @cachex.del(:user_cache, "ap_id:#{user.ap_id}")
1111 @cachex.del(:user_cache, "nickname:#{user.nickname}")
1112 @cachex.del(:user_cache, "friends_ap_ids:#{user.ap_id}")
1113 @cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
1114 @cachex.del(:user_cache, "muted_users_ap_ids:#{user.ap_id}")
1117 @spec get_cached_by_ap_id(String.t()) :: User.t() | nil
1118 def get_cached_by_ap_id(ap_id) do
1119 key = "ap_id:#{ap_id}"
1121 with {:ok, nil} <- @cachex.get(:user_cache, key),
1122 user when not is_nil(user) <- get_by_ap_id(ap_id),
1123 {:ok, true} <- @cachex.put(:user_cache, key, user) do
1131 def get_cached_by_id(id) do
1135 @cachex.fetch!(:user_cache, key, fn _ ->
1136 user = get_by_id(id)
1139 @cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
1140 {:commit, user.ap_id}
1146 get_cached_by_ap_id(ap_id)
1149 def get_cached_by_nickname(nickname) do
1150 key = "nickname:#{nickname}"
1152 @cachex.fetch!(:user_cache, key, fn _ ->
1153 case get_or_fetch_by_nickname(nickname) do
1154 {:ok, user} -> {:commit, user}
1155 {:error, _error} -> {:ignore, nil}
1160 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
1161 restrict_to_local = Config.get([:instance, :limit_to_local_content])
1164 is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
1165 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
1167 restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
1168 get_cached_by_nickname(nickname_or_id)
1170 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
1171 get_cached_by_nickname(nickname_or_id)
1178 @spec get_by_nickname(String.t()) :: User.t() | nil
1179 def get_by_nickname(nickname) do
1180 Repo.get_by(User, nickname: nickname) ||
1181 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
1182 Repo.get_by(User, nickname: local_nickname(nickname))
1186 def get_by_email(email), do: Repo.get_by(User, email: email)
1188 def get_by_nickname_or_email(nickname_or_email) do
1189 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
1192 def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname)
1194 def get_or_fetch_by_nickname(nickname) do
1195 with %User{} = user <- get_by_nickname(nickname) do
1199 with [_nick, _domain] <- String.split(nickname, "@"),
1200 {:ok, user} <- fetch_by_nickname(nickname) do
1203 _e -> {:error, "not found " <> nickname}
1208 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
1209 def get_followers_query(%User{} = user, nil) do
1210 User.Query.build(%{followers: user, is_active: true})
1213 def get_followers_query(%User{} = user, page) do
1215 |> get_followers_query(nil)
1216 |> User.Query.paginate(page, 20)
1219 @spec get_followers_query(User.t()) :: Ecto.Query.t()
1220 def get_followers_query(%User{} = user), do: get_followers_query(user, nil)
1222 @spec get_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
1223 def get_followers(%User{} = user, page \\ nil) do
1225 |> get_followers_query(page)
1229 @spec get_external_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
1230 def get_external_followers(%User{} = user, page \\ nil) do
1232 |> get_followers_query(page)
1233 |> User.Query.build(%{external: true})
1237 def get_followers_ids(%User{} = user, page \\ nil) do
1239 |> get_followers_query(page)
1240 |> select([u], u.id)
1244 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
1245 def get_friends_query(%User{} = user, nil) do
1246 User.Query.build(%{friends: user, deactivated: false})
1249 def get_friends_query(%User{} = user, page) do
1251 |> get_friends_query(nil)
1252 |> User.Query.paginate(page, 20)
1255 @spec get_friends_query(User.t()) :: Ecto.Query.t()
1256 def get_friends_query(%User{} = user), do: get_friends_query(user, nil)
1258 def get_friends(%User{} = user, page \\ nil) do
1260 |> get_friends_query(page)
1264 def get_friends_ap_ids(%User{} = user) do
1266 |> get_friends_query(nil)
1267 |> select([u], u.ap_id)
1271 def get_friends_ids(%User{} = user, page \\ nil) do
1273 |> get_friends_query(page)
1274 |> select([u], u.id)
1278 def increase_note_count(%User{} = user) do
1280 |> where(id: ^user.id)
1281 |> update([u], inc: [note_count: 1])
1283 |> Repo.update_all([])
1285 {1, [user]} -> set_cache(user)
1290 def decrease_note_count(%User{} = user) do
1292 |> where(id: ^user.id)
1295 note_count: fragment("greatest(0, note_count - 1)")
1299 |> Repo.update_all([])
1301 {1, [user]} -> set_cache(user)
1306 def update_note_count(%User{} = user, note_count \\ nil) do
1311 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
1317 |> cast(%{note_count: note_count}, [:note_count])
1318 |> update_and_set_cache()
1321 @spec maybe_fetch_follow_information(User.t()) :: User.t()
1322 def maybe_fetch_follow_information(user) do
1323 with {:ok, user} <- fetch_follow_information(user) do
1327 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
1333 def fetch_follow_information(user) do
1334 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
1336 |> follow_information_changeset(info)
1337 |> update_and_set_cache()
1341 defp follow_information_changeset(user, params) do
1348 :hide_followers_count,
1353 @spec update_follower_count(User.t()) :: {:ok, User.t()}
1354 def update_follower_count(%User{} = user) do
1355 if user.local or !Config.get([:instance, :external_user_synchronization]) do
1356 follower_count = FollowingRelationship.follower_count(user)
1359 |> follow_information_changeset(%{follower_count: follower_count})
1360 |> update_and_set_cache
1362 {:ok, maybe_fetch_follow_information(user)}
1366 @spec update_following_count(User.t()) :: {:ok, User.t()}
1367 def update_following_count(%User{local: false} = user) do
1368 if Config.get([:instance, :external_user_synchronization]) do
1369 {:ok, maybe_fetch_follow_information(user)}
1375 def update_following_count(%User{local: true} = user) do
1376 following_count = FollowingRelationship.following_count(user)
1379 |> follow_information_changeset(%{following_count: following_count})
1380 |> update_and_set_cache()
1383 @spec get_users_from_set([String.t()], keyword()) :: [User.t()]
1384 def get_users_from_set(ap_ids, opts \\ []) do
1385 local_only = Keyword.get(opts, :local_only, true)
1386 criteria = %{ap_id: ap_ids, is_active: true}
1387 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
1389 User.Query.build(criteria)
1393 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
1394 def get_recipients_from_activity(%Activity{recipients: to, actor: actor}) do
1397 query = User.Query.build(%{recipients_from_activity: to, local: true, is_active: true})
1403 @spec mute(User.t(), User.t(), map()) ::
1404 {:ok, list(UserRelationship.t())} | {:error, String.t()}
1405 def mute(%User{} = muter, %User{} = mutee, params \\ %{}) do
1406 notifications? = Map.get(params, :notifications, true)
1407 expires_in = Map.get(params, :expires_in, 0)
1409 with {:ok, user_mute} <- UserRelationship.create_mute(muter, mutee),
1410 {:ok, user_notification_mute} <-
1411 (notifications? && UserRelationship.create_notification_mute(muter, mutee)) ||
1413 if expires_in > 0 do
1414 Pleroma.Workers.MuteExpireWorker.enqueue(
1416 %{"muter_id" => muter.id, "mutee_id" => mutee.id},
1417 schedule_in: expires_in
1421 @cachex.del(:user_cache, "muted_users_ap_ids:#{muter.ap_id}")
1423 {:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
1427 def unmute(%User{} = muter, %User{} = mutee) do
1428 with {:ok, user_mute} <- UserRelationship.delete_mute(muter, mutee),
1429 {:ok, user_notification_mute} <-
1430 UserRelationship.delete_notification_mute(muter, mutee) do
1431 @cachex.del(:user_cache, "muted_users_ap_ids:#{muter.ap_id}")
1432 {:ok, [user_mute, user_notification_mute]}
1436 def unmute(muter_id, mutee_id) do
1437 with {:muter, %User{} = muter} <- {:muter, User.get_by_id(muter_id)},
1438 {:mutee, %User{} = mutee} <- {:mutee, User.get_by_id(mutee_id)} do
1439 unmute(muter, mutee)
1441 {who, result} = error ->
1443 "User.unmute/2 failed. #{who}: #{result}, muter_id: #{muter_id}, mutee_id: #{mutee_id}"
1450 def subscribe(%User{} = subscriber, %User{} = target) do
1451 deny_follow_blocked = Config.get([:user, :deny_follow_blocked])
1453 if blocks?(target, subscriber) and deny_follow_blocked do
1454 {:error, "Could not subscribe: #{target.nickname} is blocking you"}
1456 # Note: the relationship is inverse: subscriber acts as relationship target
1457 UserRelationship.create_inverse_subscription(target, subscriber)
1461 def subscribe(%User{} = subscriber, %{ap_id: ap_id}) do
1462 with %User{} = subscribee <- get_cached_by_ap_id(ap_id) do
1463 subscribe(subscriber, subscribee)
1467 def unsubscribe(%User{} = unsubscriber, %User{} = target) do
1468 # Note: the relationship is inverse: subscriber acts as relationship target
1469 UserRelationship.delete_inverse_subscription(target, unsubscriber)
1472 def unsubscribe(%User{} = unsubscriber, %{ap_id: ap_id}) do
1473 with %User{} = user <- get_cached_by_ap_id(ap_id) do
1474 unsubscribe(unsubscriber, user)
1478 def block(%User{} = blocker, %User{} = blocked) do
1479 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
1481 if following?(blocker, blocked) do
1482 {:ok, blocker, _} = unfollow(blocker, blocked)
1488 # clear any requested follows from both sides as well
1490 case CommonAPI.reject_follow_request(blocked, blocker) do
1491 {:ok, %User{} = updated_blocked} -> updated_blocked
1496 case CommonAPI.reject_follow_request(blocker, blocked) do
1497 {:ok, %User{} = updated_blocker} -> updated_blocker
1501 unsubscribe(blocked, blocker)
1503 unfollowing_blocked = Config.get([:activitypub, :unfollow_blocked], true)
1504 if unfollowing_blocked && following?(blocked, blocker), do: unfollow(blocked, blocker)
1506 {:ok, blocker} = update_follower_count(blocker)
1507 {:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
1508 add_to_block(blocker, blocked)
1511 # helper to handle the block given only an actor's AP id
1512 def block(%User{} = blocker, %{ap_id: ap_id}) do
1513 block(blocker, get_cached_by_ap_id(ap_id))
1516 def unblock(%User{} = blocker, %User{} = blocked) do
1517 remove_from_block(blocker, blocked)
1520 # helper to handle the block given only an actor's AP id
1521 def unblock(%User{} = blocker, %{ap_id: ap_id}) do
1522 unblock(blocker, get_cached_by_ap_id(ap_id))
1525 def mutes?(nil, _), do: false
1526 def mutes?(%User{} = user, %User{} = target), do: mutes_user?(user, target)
1528 def mutes_user?(%User{} = user, %User{} = target) do
1529 UserRelationship.mute_exists?(user, target)
1532 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
1533 def muted_notifications?(nil, _), do: false
1535 def muted_notifications?(%User{} = user, %User{} = target),
1536 do: UserRelationship.notification_mute_exists?(user, target)
1538 def blocks?(nil, _), do: false
1540 def blocks?(%User{} = user, %User{} = target) do
1541 blocks_user?(user, target) ||
1542 (blocks_domain?(user, target) and not User.following?(user, target))
1545 def blocks_user?(%User{} = user, %User{} = target) do
1546 UserRelationship.block_exists?(user, target)
1549 def blocks_user?(_, _), do: false
1551 def blocks_domain?(%User{} = user, %User{} = target) do
1552 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
1553 %{host: host} = URI.parse(target.ap_id)
1554 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1557 def blocks_domain?(_, _), do: false
1559 def subscribed_to?(%User{} = user, %User{} = target) do
1560 # Note: the relationship is inverse: subscriber acts as relationship target
1561 UserRelationship.inverse_subscription_exists?(target, user)
1564 def subscribed_to?(%User{} = user, %{ap_id: ap_id}) do
1565 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1566 subscribed_to?(user, target)
1571 Returns map of outgoing (blocked, muted etc.) relationships' user AP IDs by relation type.
1572 E.g. `outgoing_relationships_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}`
1574 @spec outgoing_relationships_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())}
1575 def outgoing_relationships_ap_ids(_user, []), do: %{}
1577 def outgoing_relationships_ap_ids(nil, _relationship_types), do: %{}
1579 def outgoing_relationships_ap_ids(%User{} = user, relationship_types)
1580 when is_list(relationship_types) do
1583 |> assoc(:outgoing_relationships)
1584 |> join(:inner, [user_rel], u in assoc(user_rel, :target))
1585 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1586 |> select([user_rel, u], [user_rel.relationship_type, fragment("array_agg(?)", u.ap_id)])
1587 |> group_by([user_rel, u], user_rel.relationship_type)
1589 |> Enum.into(%{}, fn [k, v] -> {k, v} end)
1594 fn rel_type -> {rel_type, db_result[rel_type] || []} end
1598 def incoming_relationships_ungrouped_ap_ids(user, relationship_types, ap_ids \\ nil)
1600 def incoming_relationships_ungrouped_ap_ids(_user, [], _ap_ids), do: []
1602 def incoming_relationships_ungrouped_ap_ids(nil, _relationship_types, _ap_ids), do: []
1604 def incoming_relationships_ungrouped_ap_ids(%User{} = user, relationship_types, ap_ids)
1605 when is_list(relationship_types) do
1607 |> assoc(:incoming_relationships)
1608 |> join(:inner, [user_rel], u in assoc(user_rel, :source))
1609 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1610 |> maybe_filter_on_ap_id(ap_ids)
1611 |> select([user_rel, u], u.ap_id)
1616 defp maybe_filter_on_ap_id(query, ap_ids) when is_list(ap_ids) do
1617 where(query, [user_rel, u], u.ap_id in ^ap_ids)
1620 defp maybe_filter_on_ap_id(query, _ap_ids), do: query
1622 def set_activation_async(user, status \\ true) do
1623 BackgroundWorker.enqueue("user_activation", %{"user_id" => user.id, "status" => status})
1626 @spec set_activation([User.t()], boolean()) :: {:ok, User.t()} | {:error, Changeset.t()}
1627 def set_activation(users, status) when is_list(users) do
1628 Repo.transaction(fn ->
1629 for user <- users, do: set_activation(user, status)
1633 @spec set_activation(User.t(), boolean()) :: {:ok, User.t()} | {:error, Changeset.t()}
1634 def set_activation(%User{} = user, status) do
1635 with {:ok, user} <- set_activation_status(user, status) do
1638 |> Enum.filter(& &1.local)
1639 |> Enum.each(&set_cache(update_following_count(&1)))
1641 # Only update local user counts, remote will be update during the next pull.
1644 |> Enum.filter(& &1.local)
1645 |> Enum.each(&do_unfollow(user, &1))
1651 def approve(users) when is_list(users) do
1652 Repo.transaction(fn ->
1653 Enum.map(users, fn user ->
1654 with {:ok, user} <- approve(user), do: user
1659 def approve(%User{is_approved: false} = user) do
1660 with chg <- change(user, is_approved: true),
1661 {:ok, user} <- update_and_set_cache(chg) do
1662 post_register_action(user)
1667 def approve(%User{} = user), do: {:ok, user}
1669 def confirm(users) when is_list(users) do
1670 Repo.transaction(fn ->
1671 Enum.map(users, fn user ->
1672 with {:ok, user} <- confirm(user), do: user
1677 def confirm(%User{is_confirmed: false} = user) do
1678 with chg <- confirmation_changeset(user, set_confirmation: true),
1679 {:ok, user} <- update_and_set_cache(chg) do
1680 post_register_action(user)
1685 def confirm(%User{} = user), do: {:ok, user}
1687 def set_suggestion(users, is_suggested) when is_list(users) do
1688 Repo.transaction(fn ->
1689 Enum.map(users, fn user ->
1690 with {:ok, user} <- set_suggestion(user, is_suggested), do: user
1695 def set_suggestion(%User{is_suggested: is_suggested} = user, is_suggested), do: {:ok, user}
1697 def set_suggestion(%User{} = user, is_suggested) when is_boolean(is_suggested) do
1699 |> change(is_suggested: is_suggested)
1700 |> update_and_set_cache()
1703 def update_notification_settings(%User{} = user, settings) do
1705 |> cast(%{notification_settings: settings}, [])
1706 |> cast_embed(:notification_settings)
1707 |> validate_required([:notification_settings])
1708 |> update_and_set_cache()
1711 @spec purge_user_changeset(User.t()) :: Changeset.t()
1712 def purge_user_changeset(user) do
1713 # "Right to be forgotten"
1714 # https://gdpr.eu/right-to-be-forgotten/
1723 last_refreshed_at: nil,
1724 last_digest_emailed_at: nil,
1731 password_reset_pending: false,
1732 registration_reason: nil,
1733 confirmation_token: nil,
1737 is_moderator: false,
1739 mastofe_settings: nil,
1742 pleroma_settings_store: %{},
1745 is_discoverable: false,
1749 # nickname: preserved
1753 # Purge doesn't delete the user from the database.
1754 # It just nulls all its fields and deactivates it.
1755 # See `User.purge_user_changeset/1` above.
1756 defp purge(%User{} = user) do
1758 |> purge_user_changeset()
1759 |> update_and_set_cache()
1762 def delete(users) when is_list(users) do
1763 for user <- users, do: delete(user)
1766 def delete(%User{} = user) do
1767 # Purge the user immediately
1769 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1772 # *Actually* delete the user from the DB
1773 defp delete_from_db(%User{} = user) do
1774 invalidate_cache(user)
1778 # If the user never finalized their account, it's safe to delete them.
1779 defp maybe_delete_from_db(%User{local: true, is_confirmed: false} = user),
1780 do: delete_from_db(user)
1782 defp maybe_delete_from_db(%User{local: true, is_approved: false} = user),
1783 do: delete_from_db(user)
1785 defp maybe_delete_from_db(user), do: {:ok, user}
1787 def perform(:force_password_reset, user), do: force_password_reset(user)
1789 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1790 def perform(:delete, %User{} = user) do
1791 # Purge the user again, in case perform/2 is called directly
1794 # Remove all relationships
1797 |> Enum.each(fn follower ->
1798 ActivityPub.unfollow(follower, user)
1799 unfollow(follower, user)
1804 |> Enum.each(fn followed ->
1805 ActivityPub.unfollow(user, followed)
1806 unfollow(user, followed)
1809 delete_user_activities(user)
1810 delete_notifications_from_user_activities(user)
1811 delete_outgoing_pending_follow_requests(user)
1813 maybe_delete_from_db(user)
1816 def perform(:set_activation_async, user, status), do: set_activation(user, status)
1818 @spec external_users_query() :: Ecto.Query.t()
1819 def external_users_query do
1827 @spec external_users(keyword()) :: [User.t()]
1828 def external_users(opts \\ []) do
1830 external_users_query()
1831 |> select([u], struct(u, [:id, :ap_id]))
1835 do: where(query, [u], u.id > ^opts[:max_id]),
1840 do: limit(query, ^opts[:limit]),
1846 def delete_notifications_from_user_activities(%User{ap_id: ap_id}) do
1848 |> join(:inner, [n], activity in assoc(n, :activity))
1849 |> where([n, a], fragment("? = ?", a.actor, ^ap_id))
1850 |> Repo.delete_all()
1853 def delete_user_activities(%User{ap_id: ap_id} = user) do
1855 |> Activity.Queries.by_actor()
1856 |> Repo.chunk_stream(50, :batches)
1857 |> Stream.each(fn activities ->
1858 Enum.each(activities, fn activity -> delete_activity(activity, user) end)
1863 defp delete_activity(%{data: %{"type" => "Create", "object" => object}} = activity, user) do
1864 with {_, %Object{}} <- {:find_object, Object.get_by_ap_id(object)},
1865 {:ok, delete_data, _} <- Builder.delete(user, object) do
1866 Pipeline.common_pipeline(delete_data, local: user.local)
1868 {:find_object, nil} ->
1869 # We have the create activity, but not the object, it was probably pruned.
1870 # Insert a tombstone and try again
1871 with {:ok, tombstone_data, _} <- Builder.tombstone(user.ap_id, object),
1872 {:ok, _tombstone} <- Object.create(tombstone_data) do
1873 delete_activity(activity, user)
1877 Logger.error("Could not delete #{object} created by #{activity.data["ap_id"]}")
1878 Logger.error("Error: #{inspect(e)}")
1882 defp delete_activity(%{data: %{"type" => type}} = activity, user)
1883 when type in ["Like", "Announce"] do
1884 {:ok, undo, _} = Builder.undo(user, activity)
1885 Pipeline.common_pipeline(undo, local: user.local)
1888 defp delete_activity(_activity, _user), do: "Doing nothing"
1890 defp delete_outgoing_pending_follow_requests(user) do
1892 |> FollowingRelationship.outgoing_pending_follow_requests_query()
1893 |> Repo.delete_all()
1896 def html_filter_policy(%User{no_rich_text: true}) do
1897 Pleroma.HTML.Scrubber.TwitterText
1900 def html_filter_policy(_), do: Config.get([:markup, :scrub_policy])
1902 def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
1904 def get_or_fetch_by_ap_id(ap_id) do
1905 cached_user = get_cached_by_ap_id(ap_id)
1907 maybe_fetched_user = needs_update?(cached_user) && fetch_by_ap_id(ap_id)
1909 case {cached_user, maybe_fetched_user} do
1910 {_, {:ok, %User{} = user}} ->
1913 {%User{} = user, _} ->
1917 Logger.error("Could not fetch user, #{inspect(e)}")
1918 {:error, :not_found}
1923 Creates an internal service actor by URI if missing.
1924 Optionally takes nickname for addressing.
1926 @spec get_or_create_service_actor_by_ap_id(String.t(), String.t()) :: User.t() | nil
1927 def get_or_create_service_actor_by_ap_id(uri, nickname) do
1929 case get_cached_by_ap_id(uri) do
1931 with {:error, %{errors: errors}} <- create_service_actor(uri, nickname) do
1932 Logger.error("Cannot create service actor: #{uri}/.\n#{inspect(errors)}")
1936 %User{invisible: false} = user ->
1946 @spec set_invisible(User.t()) :: {:ok, User.t()}
1947 defp set_invisible(user) do
1949 |> change(%{invisible: true})
1950 |> update_and_set_cache()
1953 @spec create_service_actor(String.t(), String.t()) ::
1954 {:ok, User.t()} | {:error, Ecto.Changeset.t()}
1955 defp create_service_actor(uri, nickname) do
1961 follower_address: uri <> "/followers"
1964 |> put_private_key()
1965 |> unique_constraint(:nickname)
1970 def public_key(%{public_key: public_key_pem}) when is_binary(public_key_pem) do
1973 |> :public_key.pem_decode()
1975 |> :public_key.pem_entry_decode()
1980 def public_key(_), do: {:error, "key not found"}
1982 def get_public_key_for_ap_id(ap_id) do
1983 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1984 {:ok, public_key} <- public_key(user) do
1991 def ap_enabled?(%User{local: true}), do: true
1992 def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled
1993 def ap_enabled?(_), do: false
1995 @doc "Gets or fetch a user by uri or nickname."
1996 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1997 def get_or_fetch("http://" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1998 def get_or_fetch("https://" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1999 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
2001 # wait a period of time and return newest version of the User structs
2002 # this is because we have synchronous follow APIs and need to simulate them
2003 # with an async handshake
2004 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
2005 with %User{} = a <- get_cached_by_id(a.id),
2006 %User{} = b <- get_cached_by_id(b.id) do
2013 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
2014 with :ok <- :timer.sleep(timeout),
2015 %User{} = a <- get_cached_by_id(a.id),
2016 %User{} = b <- get_cached_by_id(b.id) do
2023 def parse_bio(bio) when is_binary(bio) and bio != "" do
2025 |> CommonUtils.format_input("text/plain", mentions_format: :full)
2029 def parse_bio(_), do: ""
2031 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
2032 # TODO: get profile URLs other than user.ap_id
2033 profile_urls = [user.ap_id]
2036 |> CommonUtils.format_input("text/plain",
2037 mentions_format: :full,
2038 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
2043 def parse_bio(_, _), do: ""
2045 def tag(user_identifiers, tags) when is_list(user_identifiers) do
2046 Repo.transaction(fn ->
2047 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
2051 def tag(nickname, tags) when is_binary(nickname),
2052 do: tag(get_by_nickname(nickname), tags)
2054 def tag(%User{} = user, tags),
2055 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
2057 def untag(user_identifiers, tags) when is_list(user_identifiers) do
2058 Repo.transaction(fn ->
2059 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
2063 def untag(nickname, tags) when is_binary(nickname),
2064 do: untag(get_by_nickname(nickname), tags)
2066 def untag(%User{} = user, tags),
2067 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
2069 defp update_tags(%User{} = user, new_tags) do
2070 {:ok, updated_user} =
2072 |> change(%{tags: new_tags})
2073 |> update_and_set_cache()
2078 defp normalize_tags(tags) do
2081 |> Enum.map(&String.downcase/1)
2084 def local_nickname_regex do
2085 if Config.get([:instance, :extended_nickname_format]) do
2086 @extended_local_nickname_regex
2088 @strict_local_nickname_regex
2092 def local_nickname(nickname_or_mention) do
2095 |> String.split("@")
2099 def full_nickname(%User{} = user) do
2100 if String.contains?(user.nickname, "@") do
2103 %{host: host} = URI.parse(user.ap_id)
2104 user.nickname <> "@" <> host
2108 def full_nickname(nickname_or_mention),
2109 do: String.trim_leading(nickname_or_mention, "@")
2111 def error_user(ap_id) do
2115 nickname: "erroruser@example.com",
2116 inserted_at: NaiveDateTime.utc_now()
2120 @spec all_superusers() :: [User.t()]
2121 def all_superusers do
2122 User.Query.build(%{super_users: true, local: true, is_active: true})
2126 def muting_reblogs?(%User{} = user, %User{} = target) do
2127 UserRelationship.reblog_mute_exists?(user, target)
2130 def showing_reblogs?(%User{} = user, %User{} = target) do
2131 not muting_reblogs?(user, target)
2135 The function returns a query to get users with no activity for given interval of days.
2136 Inactive users are those who didn't read any notification, or had any activity where
2137 the user is the activity's actor, during `inactivity_threshold` days.
2138 Deactivated users will not appear in this list.
2142 iex> Pleroma.User.list_inactive_users()
2145 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
2146 def list_inactive_users_query(inactivity_threshold \\ 7) do
2147 negative_inactivity_threshold = -inactivity_threshold
2148 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
2149 # Subqueries are not supported in `where` clauses, join gets too complicated.
2150 has_read_notifications =
2151 from(n in Pleroma.Notification,
2152 where: n.seen == true,
2154 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
2157 |> Pleroma.Repo.all()
2159 from(u in Pleroma.User,
2160 left_join: a in Pleroma.Activity,
2161 on: u.ap_id == a.actor,
2162 where: not is_nil(u.nickname),
2163 where: u.is_active == ^true,
2164 where: u.id not in ^has_read_notifications,
2167 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
2168 is_nil(max(a.inserted_at))
2173 Enable or disable email notifications for user
2177 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => false}}, "digest", true)
2178 Pleroma.User{email_notifications: %{"digest" => true}}
2180 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => true}}, "digest", false)
2181 Pleroma.User{email_notifications: %{"digest" => false}}
2183 @spec switch_email_notifications(t(), String.t(), boolean()) ::
2184 {:ok, t()} | {:error, Ecto.Changeset.t()}
2185 def switch_email_notifications(user, type, status) do
2186 User.update_email_notifications(user, %{type => status})
2190 Set `last_digest_emailed_at` value for the user to current time
2192 @spec touch_last_digest_emailed_at(t()) :: t()
2193 def touch_last_digest_emailed_at(user) do
2194 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
2196 {:ok, updated_user} =
2198 |> change(%{last_digest_emailed_at: now})
2199 |> update_and_set_cache()
2204 @spec set_confirmation(User.t(), boolean()) :: {:ok, User.t()} | {:error, Changeset.t()}
2205 def set_confirmation(%User{} = user, bool) do
2207 |> confirmation_changeset(set_confirmation: bool)
2208 |> update_and_set_cache()
2211 def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
2215 def get_mascot(%{mascot: mascot}) when is_nil(mascot) do
2216 # use instance-default
2217 config = Config.get([:assets, :mascots])
2218 default_mascot = Config.get([:assets, :default_mascot])
2219 mascot = Keyword.get(config, default_mascot)
2222 "id" => "default-mascot",
2223 "url" => mascot[:url],
2224 "preview_url" => mascot[:url],
2226 "mime_type" => mascot[:mime_type]
2231 def get_ap_ids_by_nicknames(nicknames) do
2233 where: u.nickname in ^nicknames,
2239 defp put_password_hash(
2240 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
2242 change(changeset, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt(password))
2245 defp put_password_hash(changeset), do: changeset
2247 def is_internal_user?(%User{nickname: nil}), do: true
2248 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
2249 def is_internal_user?(_), do: false
2251 # A hack because user delete activities have a fake id for whatever reason
2252 # TODO: Get rid of this
2253 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
2255 def get_delivered_users_by_object_id(object_id) do
2257 inner_join: delivery in assoc(u, :deliveries),
2258 where: delivery.object_id == ^object_id
2263 def change_email(user, email) do
2265 |> cast(%{email: email}, [:email])
2266 |> maybe_validate_required_email(false)
2267 |> unique_constraint(:email)
2268 |> validate_format(:email, @email_regex)
2269 |> update_and_set_cache()
2272 def alias_users(user) do
2274 |> Enum.map(&User.get_cached_by_ap_id/1)
2275 |> Enum.filter(fn user -> user != nil end)
2278 def add_alias(user, new_alias_user) do
2279 current_aliases = user.also_known_as || []
2280 new_alias_ap_id = new_alias_user.ap_id
2282 if new_alias_ap_id in current_aliases do
2286 |> cast(%{also_known_as: current_aliases ++ [new_alias_ap_id]}, [:also_known_as])
2287 |> update_and_set_cache()
2291 def delete_alias(user, alias_user) do
2292 current_aliases = user.also_known_as || []
2293 alias_ap_id = alias_user.ap_id
2295 if alias_ap_id in current_aliases do
2297 |> cast(%{also_known_as: current_aliases -- [alias_ap_id]}, [:also_known_as])
2298 |> update_and_set_cache()
2300 {:error, :no_such_alias}
2304 # Internal function; public one is `deactivate/2`
2305 defp set_activation_status(user, status) do
2307 |> cast(%{is_active: status}, [:is_active])
2308 |> update_and_set_cache()
2311 def update_banner(user, banner) do
2313 |> cast(%{banner: banner}, [:banner])
2314 |> update_and_set_cache()
2317 def update_background(user, background) do
2319 |> cast(%{background: background}, [:background])
2320 |> update_and_set_cache()
2323 def validate_fields(changeset, remote? \\ false) do
2324 limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
2325 limit = Config.get([:instance, limit_name], 0)
2328 |> validate_length(:fields, max: limit)
2329 |> validate_change(:fields, fn :fields, fields ->
2330 if Enum.all?(fields, &valid_field?/1) do
2338 defp valid_field?(%{"name" => name, "value" => value}) do
2339 name_limit = Config.get([:instance, :account_field_name_length], 255)
2340 value_limit = Config.get([:instance, :account_field_value_length], 255)
2342 is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
2343 String.length(value) <= value_limit
2346 defp valid_field?(_), do: false
2348 defp truncate_field(%{"name" => name, "value" => value}) do
2350 String.split_at(name, Config.get([:instance, :account_field_name_length], 255))
2353 String.split_at(value, Config.get([:instance, :account_field_value_length], 255))
2355 %{"name" => name, "value" => value}
2358 def admin_api_update(user, params) do
2365 |> update_and_set_cache()
2368 @doc "Signs user out of all applications"
2369 def global_sign_out(user) do
2370 OAuth.Authorization.delete_user_authorizations(user)
2371 OAuth.Token.delete_user_tokens(user)
2374 def mascot_update(user, url) do
2376 |> cast(%{mascot: url}, [:mascot])
2377 |> validate_required([:mascot])
2378 |> update_and_set_cache()
2381 def mastodon_settings_update(user, settings) do
2383 |> cast(%{mastofe_settings: settings}, [:mastofe_settings])
2384 |> validate_required([:mastofe_settings])
2385 |> update_and_set_cache()
2388 @spec confirmation_changeset(User.t(), keyword()) :: Changeset.t()
2389 def confirmation_changeset(user, set_confirmation: confirmed?) do
2394 confirmation_token: nil
2398 is_confirmed: false,
2399 confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
2403 cast(user, params, [:is_confirmed, :confirmation_token])
2406 @spec approval_changeset(User.t(), keyword()) :: Changeset.t()
2407 def approval_changeset(user, set_approval: approved?) do
2408 cast(user, %{is_approved: approved?}, [:is_approved])
2411 @spec add_pinned_object_id(User.t(), String.t()) :: {:ok, User.t()} | {:error, term()}
2412 def add_pinned_object_id(%User{} = user, object_id) do
2413 if !user.pinned_objects[object_id] do
2414 params = %{pinned_objects: Map.put(user.pinned_objects, object_id, NaiveDateTime.utc_now())}
2417 |> cast(params, [:pinned_objects])
2418 |> validate_change(:pinned_objects, fn :pinned_objects, pinned_objects ->
2419 max_pinned_statuses = Config.get([:instance, :max_pinned_statuses], 0)
2421 if Enum.count(pinned_objects) <= max_pinned_statuses do
2424 [pinned_objects: "You have already pinned the maximum number of statuses"]
2430 |> update_and_set_cache()
2433 @spec remove_pinned_object_id(User.t(), String.t()) :: {:ok, t()} | {:error, term()}
2434 def remove_pinned_object_id(%User{} = user, object_id) do
2437 %{pinned_objects: Map.delete(user.pinned_objects, object_id)},
2440 |> update_and_set_cache()
2443 def update_email_notifications(user, settings) do
2444 email_notifications =
2445 user.email_notifications
2446 |> Map.merge(settings)
2447 |> Map.take(["digest"])
2449 params = %{email_notifications: email_notifications}
2450 fields = [:email_notifications]
2453 |> cast(params, fields)
2454 |> validate_required(fields)
2455 |> update_and_set_cache()
2458 defp set_domain_blocks(user, domain_blocks) do
2459 params = %{domain_blocks: domain_blocks}
2462 |> cast(params, [:domain_blocks])
2463 |> validate_required([:domain_blocks])
2464 |> update_and_set_cache()
2467 def block_domain(user, domain_blocked) do
2468 set_domain_blocks(user, Enum.uniq([domain_blocked | user.domain_blocks]))
2471 def unblock_domain(user, domain_blocked) do
2472 set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked))
2475 @spec add_to_block(User.t(), User.t()) ::
2476 {:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()}
2477 defp add_to_block(%User{} = user, %User{} = blocked) do
2478 with {:ok, relationship} <- UserRelationship.create_block(user, blocked) do
2479 @cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
2484 @spec add_to_block(User.t(), User.t()) ::
2485 {:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()}
2486 defp remove_from_block(%User{} = user, %User{} = blocked) do
2487 with {:ok, relationship} <- UserRelationship.delete_block(user, blocked) do
2488 @cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
2493 def set_invisible(user, invisible) do
2494 params = %{invisible: invisible}
2497 |> cast(params, [:invisible])
2498 |> validate_required([:invisible])
2499 |> update_and_set_cache()
2502 def sanitize_html(%User{} = user) do
2503 sanitize_html(user, nil)
2506 # User data that mastodon isn't filtering (treated as plaintext):
2509 def sanitize_html(%User{} = user, filter) do
2511 Enum.map(user.fields, fn %{"name" => name, "value" => value} ->
2514 "value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
2519 |> Map.put(:bio, HTML.filter_tags(user.bio, filter))
2520 |> Map.put(:fields, fields)
2523 def get_host(%User{ap_id: ap_id} = _user) do
2524 URI.parse(ap_id).host
2527 def update_last_active_at(%__MODULE__{local: true} = user) do
2529 |> cast(%{last_active_at: NaiveDateTime.utc_now()}, [:last_active_at])
2530 |> update_and_set_cache()
2533 def active_user_count(days \\ 30) do
2534 active_after = Timex.shift(NaiveDateTime.utc_now(), days: -days)
2537 |> where([u], u.last_active_at >= ^active_after)
2538 |> where([u], u.local == true)
2539 |> Repo.aggregate(:count)
2542 def update_last_status_at(user) do
2544 |> where(id: ^user.id)
2545 |> update([u], set: [last_status_at: fragment("NOW()")])
2547 |> Repo.update_all([])
2549 {1, [user]} -> set_cache(user)