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
1098 Pleroma.Elasticsearch.maybe_put_into_elasticsearch(user)
1101 |> maybe_remove_report_notifications(was_superuser_before_update)
1104 defp maybe_remove_report_notifications({:ok, %Pleroma.User{} = user} = result, true) do
1105 if not User.superuser?(user),
1106 do: user |> Notification.destroy_multiple_from_types(["pleroma:report"])
1111 defp maybe_remove_report_notifications(result, _) do
1115 def get_user_friends_ap_ids(user) do
1116 from(u in User.get_friends_query(user), select: u.ap_id)
1120 @spec get_cached_user_friends_ap_ids(User.t()) :: [String.t()]
1121 def get_cached_user_friends_ap_ids(user) do
1122 @cachex.fetch!(:user_cache, "friends_ap_ids:#{user.ap_id}", fn _ ->
1123 get_user_friends_ap_ids(user)
1127 def invalidate_cache(user) do
1128 @cachex.del(:user_cache, "ap_id:#{user.ap_id}")
1129 @cachex.del(:user_cache, "nickname:#{user.nickname}")
1130 @cachex.del(:user_cache, "friends_ap_ids:#{user.ap_id}")
1131 @cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
1132 @cachex.del(:user_cache, "muted_users_ap_ids:#{user.ap_id}")
1135 @spec get_cached_by_ap_id(String.t()) :: User.t() | nil
1136 def get_cached_by_ap_id(ap_id) do
1137 key = "ap_id:#{ap_id}"
1139 with {:ok, nil} <- @cachex.get(:user_cache, key),
1140 user when not is_nil(user) <- get_by_ap_id(ap_id),
1141 {:ok, true} <- @cachex.put(:user_cache, key, user) do
1149 def get_cached_by_id(id) do
1153 @cachex.fetch!(:user_cache, key, fn _ ->
1154 user = get_by_id(id)
1157 @cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
1158 {:commit, user.ap_id}
1164 get_cached_by_ap_id(ap_id)
1167 def get_cached_by_nickname(nickname) do
1168 key = "nickname:#{nickname}"
1170 @cachex.fetch!(:user_cache, key, fn _ ->
1171 case get_or_fetch_by_nickname(nickname) do
1172 {:ok, user} -> {:commit, user}
1173 {:error, _error} -> {:ignore, nil}
1178 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
1179 restrict_to_local = Config.get([:instance, :limit_to_local_content])
1182 is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
1183 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
1185 restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
1186 get_cached_by_nickname(nickname_or_id)
1188 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
1189 get_cached_by_nickname(nickname_or_id)
1196 @spec get_by_nickname(String.t()) :: User.t() | nil
1197 def get_by_nickname(nickname) do
1198 Repo.get_by(User, nickname: nickname) ||
1199 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
1200 Repo.get_by(User, nickname: local_nickname(nickname))
1204 def get_by_email(email), do: Repo.get_by(User, email: email)
1206 def get_by_nickname_or_email(nickname_or_email) do
1207 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
1210 def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname)
1212 def get_or_fetch_by_nickname(nickname) do
1213 with %User{} = user <- get_by_nickname(nickname) do
1217 with [_nick, _domain] <- String.split(nickname, "@"),
1218 {:ok, user} <- fetch_by_nickname(nickname) do
1221 _e -> {:error, "not found " <> nickname}
1226 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
1227 def get_followers_query(%User{} = user, nil) do
1228 User.Query.build(%{followers: user, is_active: true})
1231 def get_followers_query(%User{} = user, page) do
1233 |> get_followers_query(nil)
1234 |> User.Query.paginate(page, 20)
1237 @spec get_followers_query(User.t()) :: Ecto.Query.t()
1238 def get_followers_query(%User{} = user), do: get_followers_query(user, nil)
1240 @spec get_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
1241 def get_followers(%User{} = user, page \\ nil) do
1243 |> get_followers_query(page)
1247 @spec get_external_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
1248 def get_external_followers(%User{} = user, page \\ nil) do
1250 |> get_followers_query(page)
1251 |> User.Query.build(%{external: true})
1255 def get_followers_ids(%User{} = user, page \\ nil) do
1257 |> get_followers_query(page)
1258 |> select([u], u.id)
1262 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
1263 def get_friends_query(%User{} = user, nil) do
1264 User.Query.build(%{friends: user, deactivated: false})
1267 def get_friends_query(%User{} = user, page) do
1269 |> get_friends_query(nil)
1270 |> User.Query.paginate(page, 20)
1273 @spec get_friends_query(User.t()) :: Ecto.Query.t()
1274 def get_friends_query(%User{} = user), do: get_friends_query(user, nil)
1276 def get_friends(%User{} = user, page \\ nil) do
1278 |> get_friends_query(page)
1282 def get_friends_ap_ids(%User{} = user) do
1284 |> get_friends_query(nil)
1285 |> select([u], u.ap_id)
1289 def get_friends_ids(%User{} = user, page \\ nil) do
1291 |> get_friends_query(page)
1292 |> select([u], u.id)
1296 def increase_note_count(%User{} = user) do
1298 |> where(id: ^user.id)
1299 |> update([u], inc: [note_count: 1])
1301 |> Repo.update_all([])
1303 {1, [user]} -> set_cache(user)
1308 def decrease_note_count(%User{} = user) do
1310 |> where(id: ^user.id)
1313 note_count: fragment("greatest(0, note_count - 1)")
1317 |> Repo.update_all([])
1319 {1, [user]} -> set_cache(user)
1324 def update_note_count(%User{} = user, note_count \\ nil) do
1329 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
1335 |> cast(%{note_count: note_count}, [:note_count])
1336 |> update_and_set_cache()
1339 @spec maybe_fetch_follow_information(User.t()) :: User.t()
1340 def maybe_fetch_follow_information(user) do
1341 with {:ok, user} <- fetch_follow_information(user) do
1345 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
1351 def fetch_follow_information(user) do
1352 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
1354 |> follow_information_changeset(info)
1355 |> update_and_set_cache()
1359 defp follow_information_changeset(user, params) do
1366 :hide_followers_count,
1371 @spec update_follower_count(User.t()) :: {:ok, User.t()}
1372 def update_follower_count(%User{} = user) do
1373 if user.local or !Config.get([:instance, :external_user_synchronization]) do
1374 follower_count = FollowingRelationship.follower_count(user)
1377 |> follow_information_changeset(%{follower_count: follower_count})
1378 |> update_and_set_cache
1380 {:ok, maybe_fetch_follow_information(user)}
1384 @spec update_following_count(User.t()) :: {:ok, User.t()}
1385 def update_following_count(%User{local: false} = user) do
1386 if Config.get([:instance, :external_user_synchronization]) do
1387 {:ok, maybe_fetch_follow_information(user)}
1393 def update_following_count(%User{local: true} = user) do
1394 following_count = FollowingRelationship.following_count(user)
1397 |> follow_information_changeset(%{following_count: following_count})
1398 |> update_and_set_cache()
1401 @spec get_users_from_set([String.t()], keyword()) :: [User.t()]
1402 def get_users_from_set(ap_ids, opts \\ []) do
1403 local_only = Keyword.get(opts, :local_only, true)
1404 criteria = %{ap_id: ap_ids, is_active: true}
1405 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
1407 User.Query.build(criteria)
1411 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
1412 def get_recipients_from_activity(%Activity{recipients: to, actor: actor}) do
1415 query = User.Query.build(%{recipients_from_activity: to, local: true, is_active: true})
1421 @spec mute(User.t(), User.t(), map()) ::
1422 {:ok, list(UserRelationship.t())} | {:error, String.t()}
1423 def mute(%User{} = muter, %User{} = mutee, params \\ %{}) do
1424 notifications? = Map.get(params, :notifications, true)
1425 expires_in = Map.get(params, :expires_in, 0)
1427 with {:ok, user_mute} <- UserRelationship.create_mute(muter, mutee),
1428 {:ok, user_notification_mute} <-
1429 (notifications? && UserRelationship.create_notification_mute(muter, mutee)) ||
1431 if expires_in > 0 do
1432 Pleroma.Workers.MuteExpireWorker.enqueue(
1434 %{"muter_id" => muter.id, "mutee_id" => mutee.id},
1435 schedule_in: expires_in
1439 @cachex.del(:user_cache, "muted_users_ap_ids:#{muter.ap_id}")
1441 {:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
1445 def unmute(%User{} = muter, %User{} = mutee) do
1446 with {:ok, user_mute} <- UserRelationship.delete_mute(muter, mutee),
1447 {:ok, user_notification_mute} <-
1448 UserRelationship.delete_notification_mute(muter, mutee) do
1449 @cachex.del(:user_cache, "muted_users_ap_ids:#{muter.ap_id}")
1450 {:ok, [user_mute, user_notification_mute]}
1454 def unmute(muter_id, mutee_id) do
1455 with {:muter, %User{} = muter} <- {:muter, User.get_by_id(muter_id)},
1456 {:mutee, %User{} = mutee} <- {:mutee, User.get_by_id(mutee_id)} do
1457 unmute(muter, mutee)
1459 {who, result} = error ->
1461 "User.unmute/2 failed. #{who}: #{result}, muter_id: #{muter_id}, mutee_id: #{mutee_id}"
1468 def subscribe(%User{} = subscriber, %User{} = target) do
1469 deny_follow_blocked = Config.get([:user, :deny_follow_blocked])
1471 if blocks?(target, subscriber) and deny_follow_blocked do
1472 {:error, "Could not subscribe: #{target.nickname} is blocking you"}
1474 # Note: the relationship is inverse: subscriber acts as relationship target
1475 UserRelationship.create_inverse_subscription(target, subscriber)
1479 def subscribe(%User{} = subscriber, %{ap_id: ap_id}) do
1480 with %User{} = subscribee <- get_cached_by_ap_id(ap_id) do
1481 subscribe(subscriber, subscribee)
1485 def unsubscribe(%User{} = unsubscriber, %User{} = target) do
1486 # Note: the relationship is inverse: subscriber acts as relationship target
1487 UserRelationship.delete_inverse_subscription(target, unsubscriber)
1490 def unsubscribe(%User{} = unsubscriber, %{ap_id: ap_id}) do
1491 with %User{} = user <- get_cached_by_ap_id(ap_id) do
1492 unsubscribe(unsubscriber, user)
1496 def block(%User{} = blocker, %User{} = blocked) do
1497 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
1499 if following?(blocker, blocked) do
1500 {:ok, blocker, _} = unfollow(blocker, blocked)
1506 # clear any requested follows as well
1508 case CommonAPI.reject_follow_request(blocked, blocker) do
1509 {:ok, %User{} = updated_blocked} -> updated_blocked
1513 unsubscribe(blocked, blocker)
1515 unfollowing_blocked = Config.get([:activitypub, :unfollow_blocked], true)
1516 if unfollowing_blocked && following?(blocked, blocker), do: unfollow(blocked, blocker)
1518 {:ok, blocker} = update_follower_count(blocker)
1519 {:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
1520 add_to_block(blocker, blocked)
1523 # helper to handle the block given only an actor's AP id
1524 def block(%User{} = blocker, %{ap_id: ap_id}) do
1525 block(blocker, get_cached_by_ap_id(ap_id))
1528 def unblock(%User{} = blocker, %User{} = blocked) do
1529 remove_from_block(blocker, blocked)
1532 # helper to handle the block given only an actor's AP id
1533 def unblock(%User{} = blocker, %{ap_id: ap_id}) do
1534 unblock(blocker, get_cached_by_ap_id(ap_id))
1537 def mutes?(nil, _), do: false
1538 def mutes?(%User{} = user, %User{} = target), do: mutes_user?(user, target)
1540 def mutes_user?(%User{} = user, %User{} = target) do
1541 UserRelationship.mute_exists?(user, target)
1544 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
1545 def muted_notifications?(nil, _), do: false
1547 def muted_notifications?(%User{} = user, %User{} = target),
1548 do: UserRelationship.notification_mute_exists?(user, target)
1550 def blocks?(nil, _), do: false
1552 def blocks?(%User{} = user, %User{} = target) do
1553 blocks_user?(user, target) ||
1554 (blocks_domain?(user, target) and not User.following?(user, target))
1557 def blocks_user?(%User{} = user, %User{} = target) do
1558 UserRelationship.block_exists?(user, target)
1561 def blocks_user?(_, _), do: false
1563 def blocks_domain?(%User{} = user, %User{} = target) do
1564 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
1565 %{host: host} = URI.parse(target.ap_id)
1566 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1569 def blocks_domain?(_, _), do: false
1571 def subscribed_to?(%User{} = user, %User{} = target) do
1572 # Note: the relationship is inverse: subscriber acts as relationship target
1573 UserRelationship.inverse_subscription_exists?(target, user)
1576 def subscribed_to?(%User{} = user, %{ap_id: ap_id}) do
1577 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1578 subscribed_to?(user, target)
1583 Returns map of outgoing (blocked, muted etc.) relationships' user AP IDs by relation type.
1584 E.g. `outgoing_relationships_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}`
1586 @spec outgoing_relationships_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())}
1587 def outgoing_relationships_ap_ids(_user, []), do: %{}
1589 def outgoing_relationships_ap_ids(nil, _relationship_types), do: %{}
1591 def outgoing_relationships_ap_ids(%User{} = user, relationship_types)
1592 when is_list(relationship_types) do
1595 |> assoc(:outgoing_relationships)
1596 |> join(:inner, [user_rel], u in assoc(user_rel, :target))
1597 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1598 |> select([user_rel, u], [user_rel.relationship_type, fragment("array_agg(?)", u.ap_id)])
1599 |> group_by([user_rel, u], user_rel.relationship_type)
1601 |> Enum.into(%{}, fn [k, v] -> {k, v} end)
1606 fn rel_type -> {rel_type, db_result[rel_type] || []} end
1610 def incoming_relationships_ungrouped_ap_ids(user, relationship_types, ap_ids \\ nil)
1612 def incoming_relationships_ungrouped_ap_ids(_user, [], _ap_ids), do: []
1614 def incoming_relationships_ungrouped_ap_ids(nil, _relationship_types, _ap_ids), do: []
1616 def incoming_relationships_ungrouped_ap_ids(%User{} = user, relationship_types, ap_ids)
1617 when is_list(relationship_types) do
1619 |> assoc(:incoming_relationships)
1620 |> join(:inner, [user_rel], u in assoc(user_rel, :source))
1621 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1622 |> maybe_filter_on_ap_id(ap_ids)
1623 |> select([user_rel, u], u.ap_id)
1628 defp maybe_filter_on_ap_id(query, ap_ids) when is_list(ap_ids) do
1629 where(query, [user_rel, u], u.ap_id in ^ap_ids)
1632 defp maybe_filter_on_ap_id(query, _ap_ids), do: query
1634 def set_activation_async(user, status \\ true) do
1635 BackgroundWorker.enqueue("user_activation", %{"user_id" => user.id, "status" => status})
1638 @spec set_activation([User.t()], boolean()) :: {:ok, User.t()} | {:error, Changeset.t()}
1639 def set_activation(users, status) when is_list(users) do
1640 Repo.transaction(fn ->
1641 for user <- users, do: set_activation(user, status)
1645 @spec set_activation(User.t(), boolean()) :: {:ok, User.t()} | {:error, Changeset.t()}
1646 def set_activation(%User{} = user, status) do
1647 with {:ok, user} <- set_activation_status(user, status) do
1650 |> Enum.filter(& &1.local)
1651 |> Enum.each(&set_cache(update_following_count(&1)))
1653 # Only update local user counts, remote will be update during the next pull.
1656 |> Enum.filter(& &1.local)
1657 |> Enum.each(&do_unfollow(user, &1))
1663 def approve(users) when is_list(users) do
1664 Repo.transaction(fn ->
1665 Enum.map(users, fn user ->
1666 with {:ok, user} <- approve(user), do: user
1671 def approve(%User{is_approved: false} = user) do
1672 with chg <- change(user, is_approved: true),
1673 {:ok, user} <- update_and_set_cache(chg) do
1674 post_register_action(user)
1679 def approve(%User{} = user), do: {:ok, user}
1681 def confirm(users) when is_list(users) do
1682 Repo.transaction(fn ->
1683 Enum.map(users, fn user ->
1684 with {:ok, user} <- confirm(user), do: user
1689 def confirm(%User{is_confirmed: false} = user) do
1690 with chg <- confirmation_changeset(user, set_confirmation: true),
1691 {:ok, user} <- update_and_set_cache(chg) do
1692 post_register_action(user)
1697 def confirm(%User{} = user), do: {:ok, user}
1699 def set_suggestion(users, is_suggested) when is_list(users) do
1700 Repo.transaction(fn ->
1701 Enum.map(users, fn user ->
1702 with {:ok, user} <- set_suggestion(user, is_suggested), do: user
1707 def set_suggestion(%User{is_suggested: is_suggested} = user, is_suggested), do: {:ok, user}
1709 def set_suggestion(%User{} = user, is_suggested) when is_boolean(is_suggested) do
1711 |> change(is_suggested: is_suggested)
1712 |> update_and_set_cache()
1715 def update_notification_settings(%User{} = user, settings) do
1717 |> cast(%{notification_settings: settings}, [])
1718 |> cast_embed(:notification_settings)
1719 |> validate_required([:notification_settings])
1720 |> update_and_set_cache()
1723 @spec purge_user_changeset(User.t()) :: Changeset.t()
1724 def purge_user_changeset(user) do
1725 # "Right to be forgotten"
1726 # https://gdpr.eu/right-to-be-forgotten/
1735 last_refreshed_at: nil,
1736 last_digest_emailed_at: nil,
1743 password_reset_pending: false,
1744 registration_reason: nil,
1745 confirmation_token: nil,
1749 is_moderator: false,
1751 mastofe_settings: nil,
1754 pleroma_settings_store: %{},
1757 is_discoverable: false,
1761 # nickname: preserved
1765 # Purge doesn't delete the user from the database.
1766 # It just nulls all its fields and deactivates it.
1767 # See `User.purge_user_changeset/1` above.
1768 defp purge(%User{} = user) do
1770 |> purge_user_changeset()
1771 |> update_and_set_cache()
1774 def delete(users) when is_list(users) do
1775 for user <- users, do: delete(user)
1778 def delete(%User{} = user) do
1779 # Purge the user immediately
1781 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1784 # *Actually* delete the user from the DB
1785 defp delete_from_db(%User{} = user) do
1786 invalidate_cache(user)
1790 # If the user never finalized their account, it's safe to delete them.
1791 defp maybe_delete_from_db(%User{local: true, is_confirmed: false} = user),
1792 do: delete_from_db(user)
1794 defp maybe_delete_from_db(%User{local: true, is_approved: false} = user),
1795 do: delete_from_db(user)
1797 defp maybe_delete_from_db(user), do: {:ok, user}
1799 def perform(:force_password_reset, user), do: force_password_reset(user)
1801 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1802 def perform(:delete, %User{} = user) do
1803 # Purge the user again, in case perform/2 is called directly
1806 # Remove all relationships
1809 |> Enum.each(fn follower ->
1810 ActivityPub.unfollow(follower, user)
1811 unfollow(follower, user)
1816 |> Enum.each(fn followed ->
1817 ActivityPub.unfollow(user, followed)
1818 unfollow(user, followed)
1821 delete_user_activities(user)
1822 delete_notifications_from_user_activities(user)
1823 delete_outgoing_pending_follow_requests(user)
1825 maybe_delete_from_db(user)
1828 def perform(:set_activation_async, user, status), do: set_activation(user, status)
1830 @spec external_users_query() :: Ecto.Query.t()
1831 def external_users_query do
1839 @spec external_users(keyword()) :: [User.t()]
1840 def external_users(opts \\ []) do
1842 external_users_query()
1843 |> select([u], struct(u, [:id, :ap_id]))
1847 do: where(query, [u], u.id > ^opts[:max_id]),
1852 do: limit(query, ^opts[:limit]),
1858 def delete_notifications_from_user_activities(%User{ap_id: ap_id}) do
1860 |> join(:inner, [n], activity in assoc(n, :activity))
1861 |> where([n, a], fragment("? = ?", a.actor, ^ap_id))
1862 |> Repo.delete_all()
1865 def delete_user_activities(%User{ap_id: ap_id} = user) do
1867 |> Activity.Queries.by_actor()
1868 |> Repo.chunk_stream(50, :batches)
1869 |> Stream.each(fn activities ->
1870 Enum.each(activities, fn activity -> delete_activity(activity, user) end)
1875 defp delete_activity(%{data: %{"type" => "Create", "object" => object}} = activity, user) do
1876 with {_, %Object{}} <- {:find_object, Object.get_by_ap_id(object)},
1877 {:ok, delete_data, _} <- Builder.delete(user, object) do
1878 Pipeline.common_pipeline(delete_data, local: user.local)
1880 {:find_object, nil} ->
1881 # We have the create activity, but not the object, it was probably pruned.
1882 # Insert a tombstone and try again
1883 with {:ok, tombstone_data, _} <- Builder.tombstone(user.ap_id, object),
1884 {:ok, _tombstone} <- Object.create(tombstone_data) do
1885 delete_activity(activity, user)
1889 Logger.error("Could not delete #{object} created by #{activity.data["ap_id"]}")
1890 Logger.error("Error: #{inspect(e)}")
1894 defp delete_activity(%{data: %{"type" => type}} = activity, user)
1895 when type in ["Like", "Announce"] do
1896 {:ok, undo, _} = Builder.undo(user, activity)
1897 Pipeline.common_pipeline(undo, local: user.local)
1900 defp delete_activity(_activity, _user), do: "Doing nothing"
1902 defp delete_outgoing_pending_follow_requests(user) do
1904 |> FollowingRelationship.outgoing_pending_follow_requests_query()
1905 |> Repo.delete_all()
1908 def html_filter_policy(%User{no_rich_text: true}) do
1909 Pleroma.HTML.Scrubber.TwitterText
1912 def html_filter_policy(_), do: Config.get([:markup, :scrub_policy])
1914 def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
1916 def get_or_fetch_by_ap_id(ap_id) do
1917 cached_user = get_cached_by_ap_id(ap_id)
1919 maybe_fetched_user = needs_update?(cached_user) && fetch_by_ap_id(ap_id)
1921 case {cached_user, maybe_fetched_user} do
1922 {_, {:ok, %User{} = user}} ->
1925 {%User{} = user, _} ->
1929 {:error, :not_found}
1934 Creates an internal service actor by URI if missing.
1935 Optionally takes nickname for addressing.
1937 @spec get_or_create_service_actor_by_ap_id(String.t(), String.t()) :: User.t() | nil
1938 def get_or_create_service_actor_by_ap_id(uri, nickname) do
1940 case get_cached_by_ap_id(uri) do
1942 with {:error, %{errors: errors}} <- create_service_actor(uri, nickname) do
1943 Logger.error("Cannot create service actor: #{uri}/.\n#{inspect(errors)}")
1947 %User{invisible: false} = user ->
1957 @spec set_invisible(User.t()) :: {:ok, User.t()}
1958 defp set_invisible(user) do
1960 |> change(%{invisible: true})
1961 |> update_and_set_cache()
1964 @spec create_service_actor(String.t(), String.t()) ::
1965 {:ok, User.t()} | {:error, Ecto.Changeset.t()}
1966 defp create_service_actor(uri, nickname) do
1972 follower_address: uri <> "/followers"
1975 |> unique_constraint(:nickname)
1980 def public_key(%{public_key: public_key_pem}) when is_binary(public_key_pem) do
1983 |> :public_key.pem_decode()
1985 |> :public_key.pem_entry_decode()
1990 def public_key(_), do: {:error, "key not found"}
1992 def get_public_key_for_ap_id(ap_id) do
1993 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1994 {:ok, public_key} <- public_key(user) do
2001 def ap_enabled?(%User{local: true}), do: true
2002 def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled
2003 def ap_enabled?(_), do: false
2005 @doc "Gets or fetch a user by uri or nickname."
2006 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
2007 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
2008 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
2010 # wait a period of time and return newest version of the User structs
2011 # this is because we have synchronous follow APIs and need to simulate them
2012 # with an async handshake
2013 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
2014 with %User{} = a <- get_cached_by_id(a.id),
2015 %User{} = b <- get_cached_by_id(b.id) do
2022 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
2023 with :ok <- :timer.sleep(timeout),
2024 %User{} = a <- get_cached_by_id(a.id),
2025 %User{} = b <- get_cached_by_id(b.id) do
2032 def parse_bio(bio) when is_binary(bio) and bio != "" do
2034 |> CommonUtils.format_input("text/plain", mentions_format: :full)
2038 def parse_bio(_), do: ""
2040 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
2041 # TODO: get profile URLs other than user.ap_id
2042 profile_urls = [user.ap_id]
2045 |> CommonUtils.format_input("text/plain",
2046 mentions_format: :full,
2047 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
2052 def parse_bio(_, _), do: ""
2054 def tag(user_identifiers, tags) when is_list(user_identifiers) do
2055 Repo.transaction(fn ->
2056 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
2060 def tag(nickname, tags) when is_binary(nickname),
2061 do: tag(get_by_nickname(nickname), tags)
2063 def tag(%User{} = user, tags),
2064 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
2066 def untag(user_identifiers, tags) when is_list(user_identifiers) do
2067 Repo.transaction(fn ->
2068 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
2072 def untag(nickname, tags) when is_binary(nickname),
2073 do: untag(get_by_nickname(nickname), tags)
2075 def untag(%User{} = user, tags),
2076 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
2078 defp update_tags(%User{} = user, new_tags) do
2079 {:ok, updated_user} =
2081 |> change(%{tags: new_tags})
2082 |> update_and_set_cache()
2087 defp normalize_tags(tags) do
2090 |> Enum.map(&String.downcase/1)
2093 defp local_nickname_regex do
2094 if Config.get([:instance, :extended_nickname_format]) do
2095 @extended_local_nickname_regex
2097 @strict_local_nickname_regex
2101 def local_nickname(nickname_or_mention) do
2104 |> String.split("@")
2108 def full_nickname(%User{} = user) do
2109 if String.contains?(user.nickname, "@") do
2112 %{host: host} = URI.parse(user.ap_id)
2113 user.nickname <> "@" <> host
2117 def full_nickname(nickname_or_mention),
2118 do: String.trim_leading(nickname_or_mention, "@")
2120 def error_user(ap_id) do
2124 nickname: "erroruser@example.com",
2125 inserted_at: NaiveDateTime.utc_now()
2129 @spec all_superusers() :: [User.t()]
2130 def all_superusers do
2131 User.Query.build(%{super_users: true, local: true, is_active: true})
2135 def muting_reblogs?(%User{} = user, %User{} = target) do
2136 UserRelationship.reblog_mute_exists?(user, target)
2139 def showing_reblogs?(%User{} = user, %User{} = target) do
2140 not muting_reblogs?(user, target)
2144 The function returns a query to get users with no activity for given interval of days.
2145 Inactive users are those who didn't read any notification, or had any activity where
2146 the user is the activity's actor, during `inactivity_threshold` days.
2147 Deactivated users will not appear in this list.
2151 iex> Pleroma.User.list_inactive_users()
2154 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
2155 def list_inactive_users_query(inactivity_threshold \\ 7) do
2156 negative_inactivity_threshold = -inactivity_threshold
2157 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
2158 # Subqueries are not supported in `where` clauses, join gets too complicated.
2159 has_read_notifications =
2160 from(n in Pleroma.Notification,
2161 where: n.seen == true,
2163 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
2166 |> Pleroma.Repo.all()
2168 from(u in Pleroma.User,
2169 left_join: a in Pleroma.Activity,
2170 on: u.ap_id == a.actor,
2171 where: not is_nil(u.nickname),
2172 where: u.is_active == ^true,
2173 where: u.id not in ^has_read_notifications,
2176 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
2177 is_nil(max(a.inserted_at))
2182 Enable or disable email notifications for user
2186 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => false}}, "digest", true)
2187 Pleroma.User{email_notifications: %{"digest" => true}}
2189 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => true}}, "digest", false)
2190 Pleroma.User{email_notifications: %{"digest" => false}}
2192 @spec switch_email_notifications(t(), String.t(), boolean()) ::
2193 {:ok, t()} | {:error, Ecto.Changeset.t()}
2194 def switch_email_notifications(user, type, status) do
2195 User.update_email_notifications(user, %{type => status})
2199 Set `last_digest_emailed_at` value for the user to current time
2201 @spec touch_last_digest_emailed_at(t()) :: t()
2202 def touch_last_digest_emailed_at(user) do
2203 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
2205 {:ok, updated_user} =
2207 |> change(%{last_digest_emailed_at: now})
2208 |> update_and_set_cache()
2213 @spec set_confirmation(User.t(), boolean()) :: {:ok, User.t()} | {:error, Changeset.t()}
2214 def set_confirmation(%User{} = user, bool) do
2216 |> confirmation_changeset(set_confirmation: bool)
2217 |> update_and_set_cache()
2220 def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
2224 def get_mascot(%{mascot: mascot}) when is_nil(mascot) do
2225 # use instance-default
2226 config = Config.get([:assets, :mascots])
2227 default_mascot = Config.get([:assets, :default_mascot])
2228 mascot = Keyword.get(config, default_mascot)
2231 "id" => "default-mascot",
2232 "url" => mascot[:url],
2233 "preview_url" => mascot[:url],
2235 "mime_type" => mascot[:mime_type]
2240 def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
2242 def ensure_keys_present(%User{} = user) do
2243 with {:ok, pem} <- Keys.generate_rsa_pem() do
2245 |> cast(%{keys: pem}, [:keys])
2246 |> validate_required([:keys])
2247 |> update_and_set_cache()
2251 def get_ap_ids_by_nicknames(nicknames) do
2253 where: u.nickname in ^nicknames,
2259 defp put_password_hash(
2260 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
2262 change(changeset, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt(password))
2265 defp put_password_hash(changeset), do: changeset
2267 def is_internal_user?(%User{nickname: nil}), do: true
2268 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
2269 def is_internal_user?(_), do: false
2271 # A hack because user delete activities have a fake id for whatever reason
2272 # TODO: Get rid of this
2273 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
2275 def get_delivered_users_by_object_id(object_id) do
2277 inner_join: delivery in assoc(u, :deliveries),
2278 where: delivery.object_id == ^object_id
2283 def change_email(user, email) do
2285 |> cast(%{email: email}, [:email])
2286 |> maybe_validate_required_email(false)
2287 |> unique_constraint(:email)
2288 |> validate_format(:email, @email_regex)
2289 |> update_and_set_cache()
2292 # Internal function; public one is `deactivate/2`
2293 defp set_activation_status(user, status) do
2295 |> cast(%{is_active: status}, [:is_active])
2296 |> update_and_set_cache()
2299 def update_banner(user, banner) do
2301 |> cast(%{banner: banner}, [:banner])
2302 |> update_and_set_cache()
2305 def update_background(user, background) do
2307 |> cast(%{background: background}, [:background])
2308 |> update_and_set_cache()
2311 def validate_fields(changeset, remote? \\ false) do
2312 limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
2313 limit = Config.get([:instance, limit_name], 0)
2316 |> validate_length(:fields, max: limit)
2317 |> validate_change(:fields, fn :fields, fields ->
2318 if Enum.all?(fields, &valid_field?/1) do
2326 defp valid_field?(%{"name" => name, "value" => value}) do
2327 name_limit = Config.get([:instance, :account_field_name_length], 255)
2328 value_limit = Config.get([:instance, :account_field_value_length], 255)
2330 is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
2331 String.length(value) <= value_limit
2334 defp valid_field?(_), do: false
2336 defp truncate_field(%{"name" => name, "value" => value}) do
2338 String.split_at(name, Config.get([:instance, :account_field_name_length], 255))
2341 String.split_at(value, Config.get([:instance, :account_field_value_length], 255))
2343 %{"name" => name, "value" => value}
2346 def admin_api_update(user, params) do
2353 |> update_and_set_cache()
2356 @doc "Signs user out of all applications"
2357 def global_sign_out(user) do
2358 OAuth.Authorization.delete_user_authorizations(user)
2359 OAuth.Token.delete_user_tokens(user)
2362 def mascot_update(user, url) do
2364 |> cast(%{mascot: url}, [:mascot])
2365 |> validate_required([:mascot])
2366 |> update_and_set_cache()
2369 def mastodon_settings_update(user, settings) do
2371 |> cast(%{mastofe_settings: settings}, [:mastofe_settings])
2372 |> validate_required([:mastofe_settings])
2373 |> update_and_set_cache()
2376 @spec confirmation_changeset(User.t(), keyword()) :: Changeset.t()
2377 def confirmation_changeset(user, set_confirmation: confirmed?) do
2382 confirmation_token: nil
2386 is_confirmed: false,
2387 confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
2391 cast(user, params, [:is_confirmed, :confirmation_token])
2394 @spec approval_changeset(User.t(), keyword()) :: Changeset.t()
2395 def approval_changeset(user, set_approval: approved?) do
2396 cast(user, %{is_approved: approved?}, [:is_approved])
2399 @spec add_pinned_object_id(User.t(), String.t()) :: {:ok, User.t()} | {:error, term()}
2400 def add_pinned_object_id(%User{} = user, object_id) do
2401 if !user.pinned_objects[object_id] do
2402 params = %{pinned_objects: Map.put(user.pinned_objects, object_id, NaiveDateTime.utc_now())}
2405 |> cast(params, [:pinned_objects])
2406 |> validate_change(:pinned_objects, fn :pinned_objects, pinned_objects ->
2407 max_pinned_statuses = Config.get([:instance, :max_pinned_statuses], 0)
2409 if Enum.count(pinned_objects) <= max_pinned_statuses do
2412 [pinned_objects: "You have already pinned the maximum number of statuses"]
2418 |> update_and_set_cache()
2421 @spec remove_pinned_object_id(User.t(), String.t()) :: {:ok, t()} | {:error, term()}
2422 def remove_pinned_object_id(%User{} = user, object_id) do
2425 %{pinned_objects: Map.delete(user.pinned_objects, object_id)},
2428 |> update_and_set_cache()
2431 def update_email_notifications(user, settings) do
2432 email_notifications =
2433 user.email_notifications
2434 |> Map.merge(settings)
2435 |> Map.take(["digest"])
2437 params = %{email_notifications: email_notifications}
2438 fields = [:email_notifications]
2441 |> cast(params, fields)
2442 |> validate_required(fields)
2443 |> update_and_set_cache()
2446 defp set_domain_blocks(user, domain_blocks) do
2447 params = %{domain_blocks: domain_blocks}
2450 |> cast(params, [:domain_blocks])
2451 |> validate_required([:domain_blocks])
2452 |> update_and_set_cache()
2455 def block_domain(user, domain_blocked) do
2456 set_domain_blocks(user, Enum.uniq([domain_blocked | user.domain_blocks]))
2459 def unblock_domain(user, domain_blocked) do
2460 set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked))
2463 @spec add_to_block(User.t(), User.t()) ::
2464 {:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()}
2465 defp add_to_block(%User{} = user, %User{} = blocked) do
2466 with {:ok, relationship} <- UserRelationship.create_block(user, blocked) do
2467 @cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
2472 @spec add_to_block(User.t(), User.t()) ::
2473 {:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()}
2474 defp remove_from_block(%User{} = user, %User{} = blocked) do
2475 with {:ok, relationship} <- UserRelationship.delete_block(user, blocked) do
2476 @cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
2481 def set_invisible(user, invisible) do
2482 params = %{invisible: invisible}
2485 |> cast(params, [:invisible])
2486 |> validate_required([:invisible])
2487 |> update_and_set_cache()
2490 def sanitize_html(%User{} = user) do
2491 sanitize_html(user, nil)
2494 # User data that mastodon isn't filtering (treated as plaintext):
2497 def sanitize_html(%User{} = user, filter) do
2499 Enum.map(user.fields, fn %{"name" => name, "value" => value} ->
2502 "value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
2507 |> Map.put(:bio, HTML.filter_tags(user.bio, filter))
2508 |> Map.put(:fields, fields)
2511 def get_host(%User{ap_id: ap_id} = _user) do
2512 URI.parse(ap_id).host
2515 def update_last_active_at(%__MODULE__{local: true} = user) do
2517 |> cast(%{last_active_at: NaiveDateTime.utc_now()}, [:last_active_at])
2518 |> update_and_set_cache()
2521 def active_user_count(days \\ 30) do
2522 active_after = Timex.shift(NaiveDateTime.utc_now(), days: -days)
2525 |> where([u], u.last_active_at >= ^active_after)
2526 |> where([u], u.local == true)
2527 |> Repo.aggregate(:count)
2530 def update_last_status_at(user) do
2532 |> where(id: ^user.id)
2533 |> update([u], set: [last_status_at: fragment("NOW()")])
2535 |> Repo.update_all([])
2537 {1, [user]} -> set_cache(user)