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)
156 :notification_settings,
157 Pleroma.User.NotificationSetting,
161 has_many(:notifications, Notification)
162 has_many(:registrations, Registration)
163 has_many(:deliveries, Delivery)
165 has_many(:outgoing_relationships, UserRelationship, foreign_key: :source_id)
166 has_many(:incoming_relationships, UserRelationship, foreign_key: :target_id)
168 for {relationship_type,
170 {outgoing_relation, outgoing_relation_target},
171 {incoming_relation, incoming_relation_source}
172 ]} <- @user_relationships_config do
173 # Definitions of `has_many` relations: :blocker_blocks, :muter_mutes, :reblog_muter_mutes,
174 # :notification_muter_mutes, :subscribee_subscriptions
175 has_many(outgoing_relation, UserRelationship,
176 foreign_key: :source_id,
177 where: [relationship_type: relationship_type]
180 # Definitions of `has_many` relations: :blockee_blocks, :mutee_mutes, :reblog_mutee_mutes,
181 # :notification_mutee_mutes, :subscriber_subscriptions
182 has_many(incoming_relation, UserRelationship,
183 foreign_key: :target_id,
184 where: [relationship_type: relationship_type]
187 # Definitions of `has_many` relations: :blocked_users, :muted_users, :reblog_muted_users,
188 # :notification_muted_users, :subscriber_users
189 has_many(outgoing_relation_target, through: [outgoing_relation, :target])
191 # Definitions of `has_many` relations: :blocker_users, :muter_users, :reblog_muter_users,
192 # :notification_muter_users, :subscribee_users
193 has_many(incoming_relation_source, through: [incoming_relation, :source])
196 # `:blocks` is deprecated (replaced with `blocked_users` relation)
197 field(:blocks, {:array, :string}, default: [])
198 # `:mutes` is deprecated (replaced with `muted_users` relation)
199 field(:mutes, {:array, :string}, default: [])
200 # `:muted_reblogs` is deprecated (replaced with `reblog_muted_users` relation)
201 field(:muted_reblogs, {:array, :string}, default: [])
202 # `:muted_notifications` is deprecated (replaced with `notification_muted_users` relation)
203 field(:muted_notifications, {:array, :string}, default: [])
204 # `:subscribers` is deprecated (replaced with `subscriber_users` relation)
205 field(:subscribers, {:array, :string}, default: [])
208 :multi_factor_authentication_settings,
216 for {_relationship_type, [{_outgoing_relation, outgoing_relation_target}, _]} <-
217 @user_relationships_config do
218 # `def blocked_users_relation/2`, `def muted_users_relation/2`,
219 # `def reblog_muted_users_relation/2`, `def notification_muted_users/2`,
220 # `def subscriber_users/2`
221 def unquote(:"#{outgoing_relation_target}_relation")(user, restrict_deactivated? \\ false) do
222 target_users_query = assoc(user, unquote(outgoing_relation_target))
224 if restrict_deactivated? do
226 |> User.Query.build(%{deactivated: false})
232 # `def blocked_users/2`, `def muted_users/2`, `def reblog_muted_users/2`,
233 # `def notification_muted_users/2`, `def subscriber_users/2`
234 def unquote(outgoing_relation_target)(user, restrict_deactivated? \\ false) do
236 |> apply(unquote(:"#{outgoing_relation_target}_relation"), [
238 restrict_deactivated?
243 # `def blocked_users_ap_ids/2`, `def muted_users_ap_ids/2`, `def reblog_muted_users_ap_ids/2`,
244 # `def notification_muted_users_ap_ids/2`, `def subscriber_users_ap_ids/2`
245 def unquote(:"#{outgoing_relation_target}_ap_ids")(user, restrict_deactivated? \\ false) do
247 |> apply(unquote(:"#{outgoing_relation_target}_relation"), [
249 restrict_deactivated?
251 |> select([u], u.ap_id)
256 def cached_blocked_users_ap_ids(user) do
257 @cachex.fetch!(:user_cache, "blocked_users_ap_ids:#{user.ap_id}", fn _ ->
258 blocked_users_ap_ids(user)
262 def cached_muted_users_ap_ids(user) do
263 @cachex.fetch!(:user_cache, "muted_users_ap_ids:#{user.ap_id}", fn _ ->
264 muted_users_ap_ids(user)
268 defdelegate following_count(user), to: FollowingRelationship
269 defdelegate following(user), to: FollowingRelationship
270 defdelegate following?(follower, followed), to: FollowingRelationship
271 defdelegate following_ap_ids(user), to: FollowingRelationship
272 defdelegate get_follow_requests(user), to: FollowingRelationship
273 defdelegate search(query, opts \\ []), to: User.Search
276 Dumps Flake Id to SQL-compatible format (16-byte UUID).
277 E.g. "9pQtDGXuq4p3VlcJEm" -> <<0, 0, 1, 110, 179, 218, 42, 92, 213, 41, 44, 227, 95, 213, 0, 0>>
279 def binary_id(source_id) when is_binary(source_id) do
280 with {:ok, dumped_id} <- FlakeId.Ecto.CompatType.dump(source_id) do
287 def binary_id(source_ids) when is_list(source_ids) do
288 Enum.map(source_ids, &binary_id/1)
291 def binary_id(%User{} = user), do: binary_id(user.id)
293 @doc "Returns status account"
294 @spec account_status(User.t()) :: account_status()
295 def account_status(%User{is_active: false}), do: :deactivated
296 def account_status(%User{password_reset_pending: true}), do: :password_reset_pending
297 def account_status(%User{local: true, is_approved: false}), do: :approval_pending
298 def account_status(%User{local: true, is_confirmed: false}), do: :confirmation_pending
299 def account_status(%User{}), do: :active
301 @spec visible_for(User.t(), User.t() | nil) ::
304 | :restricted_unauthenticated
306 | :confirmation_pending
307 def visible_for(user, for_user \\ nil)
309 def visible_for(%User{invisible: true}, _), do: :invisible
311 def visible_for(%User{id: user_id}, %User{id: user_id}), do: :visible
313 def visible_for(%User{} = user, nil) do
314 if restrict_unauthenticated?(user) do
315 :restrict_unauthenticated
317 visible_account_status(user)
321 def visible_for(%User{} = user, for_user) do
322 if superuser?(for_user) do
325 visible_account_status(user)
329 def visible_for(_, _), do: :invisible
331 defp restrict_unauthenticated?(%User{local: true}) do
332 Config.restrict_unauthenticated_access?(:profiles, :local)
335 defp restrict_unauthenticated?(%User{local: _}) do
336 Config.restrict_unauthenticated_access?(:profiles, :remote)
339 defp visible_account_status(user) do
340 status = account_status(user)
342 if status in [:active, :password_reset_pending] do
349 @spec superuser?(User.t()) :: boolean()
350 def superuser?(%User{local: true, is_admin: true}), do: true
351 def superuser?(%User{local: true, is_moderator: true}), do: true
352 def superuser?(_), do: false
354 @spec invisible?(User.t()) :: boolean()
355 def invisible?(%User{invisible: true}), do: true
356 def invisible?(_), do: false
358 def avatar_url(user, options \\ []) do
360 %{"url" => [%{"href" => href} | _]} ->
364 unless options[:no_default] do
365 Config.get([:assets, :default_user_avatar], "#{Endpoint.url()}/images/avi.png")
370 def banner_url(user, options \\ []) do
372 %{"url" => [%{"href" => href} | _]} -> href
373 _ -> !options[:no_default] && "#{Endpoint.url()}/images/banner.png"
377 # Should probably be renamed or removed
378 @spec ap_id(User.t()) :: String.t()
379 def ap_id(%User{nickname: nickname}), do: "#{Endpoint.url()}/users/#{nickname}"
381 @spec ap_followers(User.t()) :: String.t()
382 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
383 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
385 @spec ap_following(User.t()) :: String.t()
386 def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
387 def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
389 @spec ap_featured_collection(User.t()) :: String.t()
390 def ap_featured_collection(%User{featured_address: fa}) when is_binary(fa), do: fa
392 def ap_featured_collection(%User{} = user), do: "#{ap_id(user)}/collections/featured"
394 defp truncate_fields_param(params) do
395 if Map.has_key?(params, :fields) do
396 Map.put(params, :fields, Enum.map(params[:fields], &truncate_field/1))
402 defp truncate_if_exists(params, key, max_length) do
403 if Map.has_key?(params, key) and is_binary(params[key]) do
404 {value, _chopped} = String.split_at(params[key], max_length)
405 Map.put(params, key, value)
411 defp fix_follower_address(%{follower_address: _, following_address: _} = params), do: params
413 defp fix_follower_address(%{nickname: nickname} = params),
414 do: Map.put(params, :follower_address, ap_followers(%User{nickname: nickname}))
416 defp fix_follower_address(params), do: params
418 def remote_user_changeset(struct \\ %User{local: false}, params) do
419 bio_limit = Config.get([:instance, :user_bio_length], 5000)
420 name_limit = Config.get([:instance, :user_name_length], 100)
423 case params[:name] do
424 name when is_binary(name) and byte_size(name) > 0 -> name
425 _ -> params[:nickname]
430 |> Map.put(:name, name)
431 |> Map.put_new(:last_refreshed_at, NaiveDateTime.utc_now())
432 |> truncate_if_exists(:name, name_limit)
433 |> truncate_if_exists(:bio, bio_limit)
434 |> truncate_fields_param()
435 |> fix_follower_address()
459 :hide_followers_count,
468 :accepts_chat_messages,
472 |> cast(params, [:name], empty_values: [])
473 |> validate_required([:ap_id])
474 |> validate_required([:name], trim: false)
475 |> unique_constraint(:nickname)
476 |> validate_format(:nickname, @email_regex)
477 |> validate_length(:bio, max: bio_limit)
478 |> validate_length(:name, max: name_limit)
479 |> validate_fields(true)
480 |> validate_non_local()
483 defp validate_non_local(cng) do
484 local? = get_field(cng, :local)
488 |> add_error(:local, "User is local, can't update with this changeset.")
494 def update_changeset(struct, params \\ %{}) do
495 bio_limit = Config.get([:instance, :user_bio_length], 5000)
496 name_limit = Config.get([:instance, :user_name_length], 100)
516 :hide_followers_count,
519 :allow_following_move,
523 :skip_thread_containment,
526 :pleroma_settings_store,
529 :accepts_chat_messages,
533 |> unique_constraint(:nickname)
534 |> validate_format(:nickname, local_nickname_regex())
535 |> validate_length(:bio, max: bio_limit)
536 |> validate_length(:name, min: 1, max: name_limit)
537 |> validate_inclusion(:actor_type, ["Person", "Service"])
540 |> put_change_if_present(:bio, &{:ok, parse_bio(&1, struct)})
541 |> put_change_if_present(:avatar, &put_upload(&1, :avatar))
542 |> put_change_if_present(:banner, &put_upload(&1, :banner))
543 |> put_change_if_present(:background, &put_upload(&1, :background))
544 |> put_change_if_present(
545 :pleroma_settings_store,
546 &{:ok, Map.merge(struct.pleroma_settings_store, &1)}
548 |> validate_fields(false)
551 defp put_fields(changeset) do
552 if raw_fields = get_change(changeset, :raw_fields) do
555 |> Enum.filter(fn %{"name" => n} -> n != "" end)
559 |> Enum.map(fn f -> Map.update!(f, "value", &parse_fields(&1)) end)
562 |> put_change(:raw_fields, raw_fields)
563 |> put_change(:fields, fields)
569 defp parse_fields(value) do
571 |> Formatter.linkify(mentions_format: :full)
575 defp put_emoji(changeset) do
576 emojified_fields = [:bio, :name, :raw_fields]
578 if Enum.any?(changeset.changes, fn {k, _} -> k in emojified_fields end) do
579 bio = Emoji.Formatter.get_emoji_map(get_field(changeset, :bio))
580 name = Emoji.Formatter.get_emoji_map(get_field(changeset, :name))
582 emoji = Map.merge(bio, name)
586 |> get_field(:raw_fields)
587 |> Enum.reduce(emoji, fn x, acc ->
588 Map.merge(acc, Emoji.Formatter.get_emoji_map(x["name"] <> x["value"]))
591 put_change(changeset, :emoji, emoji)
597 defp put_change_if_present(changeset, map_field, value_function) do
598 with {:ok, value} <- fetch_change(changeset, map_field),
599 {:ok, new_value} <- value_function.(value) do
600 put_change(changeset, map_field, new_value)
606 defp put_upload(value, type) do
607 with %Plug.Upload{} <- value,
608 {:ok, object} <- ActivityPub.upload(value, type: type) do
613 def update_as_admin_changeset(struct, params) do
615 |> update_changeset(params)
616 |> cast(params, [:email])
617 |> delete_change(:also_known_as)
618 |> unique_constraint(:email)
619 |> validate_format(:email, @email_regex)
620 |> validate_inclusion(:actor_type, ["Person", "Service"])
623 @spec update_as_admin(User.t(), map()) :: {:ok, User.t()} | {:error, Changeset.t()}
624 def update_as_admin(user, params) do
625 params = Map.put(params, "password_confirmation", params["password"])
626 changeset = update_as_admin_changeset(user, params)
628 if params["password"] do
629 reset_password(user, changeset, params)
631 User.update_and_set_cache(changeset)
635 def password_update_changeset(struct, params) do
637 |> cast(params, [:password, :password_confirmation])
638 |> validate_required([:password, :password_confirmation])
639 |> validate_confirmation(:password)
640 |> put_password_hash()
641 |> put_change(:password_reset_pending, false)
644 @spec reset_password(User.t(), map()) :: {:ok, User.t()} | {:error, Changeset.t()}
645 def reset_password(%User{} = user, params) do
646 reset_password(user, user, params)
649 def reset_password(%User{id: user_id} = user, struct, params) do
652 |> Multi.update(:user, password_update_changeset(struct, params))
653 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
654 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
656 case Repo.transaction(multi) do
657 {:ok, %{user: user} = _} -> set_cache(user)
658 {:error, _, changeset, _} -> {:error, changeset}
662 def update_password_reset_pending(user, value) do
665 |> put_change(:password_reset_pending, value)
666 |> update_and_set_cache()
669 def force_password_reset_async(user) do
670 BackgroundWorker.enqueue("force_password_reset", %{"user_id" => user.id})
673 @spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
674 def force_password_reset(user), do: update_password_reset_pending(user, true)
676 # Used to auto-register LDAP accounts which won't have a password hash stored locally
677 def register_changeset_ldap(struct, params = %{password: password})
678 when is_nil(password) do
679 params = Map.put_new(params, :accepts_chat_messages, true)
682 if Map.has_key?(params, :email) do
683 Map.put_new(params, :email, params[:email])
693 :accepts_chat_messages
695 |> validate_required([:name, :nickname])
696 |> unique_constraint(:nickname)
697 |> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames]))
698 |> validate_format(:nickname, local_nickname_regex())
700 |> unique_constraint(:ap_id)
701 |> put_following_and_follower_and_featured_address()
704 def register_changeset(struct, params \\ %{}, opts \\ []) do
705 bio_limit = Config.get([:instance, :user_bio_length], 5000)
706 name_limit = Config.get([:instance, :user_name_length], 100)
707 reason_limit = Config.get([:instance, :registration_reason_length], 500)
708 params = Map.put_new(params, :accepts_chat_messages, true)
711 if is_nil(opts[:confirmed]) do
712 !Config.get([:instance, :account_activation_required])
718 if is_nil(opts[:approved]) do
719 !Config.get([:instance, :account_approval_required])
725 |> confirmation_changeset(set_confirmation: confirmed?)
726 |> approval_changeset(set_approval: approved?)
734 :password_confirmation,
736 :accepts_chat_messages,
739 |> validate_required([:name, :nickname, :password, :password_confirmation])
740 |> validate_confirmation(:password)
741 |> unique_constraint(:email)
742 |> validate_format(:email, @email_regex)
743 |> validate_change(:email, fn :email, email ->
745 Config.get([User, :email_blacklist])
746 |> Enum.all?(fn blacklisted_domain ->
747 !String.ends_with?(email, ["@" <> blacklisted_domain, "." <> blacklisted_domain])
750 if valid?, do: [], else: [email: "Invalid email"]
752 |> unique_constraint(:nickname)
753 |> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames]))
754 |> validate_format(:nickname, local_nickname_regex())
755 |> validate_length(:bio, max: bio_limit)
756 |> validate_length(:name, min: 1, max: name_limit)
757 |> validate_length(:registration_reason, max: reason_limit)
758 |> maybe_validate_required_email(opts[:external])
761 |> unique_constraint(:ap_id)
762 |> put_following_and_follower_and_featured_address()
765 def maybe_validate_required_email(changeset, true), do: changeset
767 def maybe_validate_required_email(changeset, _) do
768 if Config.get([:instance, :account_activation_required]) do
769 validate_required(changeset, [:email])
775 defp put_ap_id(changeset) do
776 ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
777 put_change(changeset, :ap_id, ap_id)
780 defp put_following_and_follower_and_featured_address(changeset) do
781 user = %User{nickname: get_field(changeset, :nickname)}
782 followers = ap_followers(user)
783 following = ap_following(user)
784 featured = ap_featured_collection(user)
787 |> put_change(:follower_address, followers)
788 |> put_change(:following_address, following)
789 |> put_change(:featured_address, featured)
792 defp autofollow_users(user) do
793 candidates = Config.get([:instance, :autofollowed_nicknames])
796 User.Query.build(%{nickname: candidates, local: true, is_active: true})
799 follow_all(user, autofollowed_users)
802 defp autofollowing_users(user) do
803 candidates = Config.get([:instance, :autofollowing_nicknames])
805 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
807 |> Enum.each(&follow(&1, user, :follow_accept))
812 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
813 def register(%Ecto.Changeset{} = changeset) do
814 with {:ok, user} <- Repo.insert(changeset) do
815 post_register_action(user)
819 def post_register_action(%User{is_confirmed: false} = user) do
820 with {:ok, _} <- maybe_send_confirmation_email(user) do
825 def post_register_action(%User{is_approved: false} = user) do
826 with {:ok, _} <- send_user_approval_email(user),
827 {:ok, _} <- send_admin_approval_emails(user) do
832 def post_register_action(%User{is_approved: true, is_confirmed: true} = user) do
833 with {:ok, user} <- autofollow_users(user),
834 {:ok, _} <- autofollowing_users(user),
835 {:ok, user} <- set_cache(user),
836 {:ok, _} <- maybe_send_registration_email(user),
837 {:ok, _} <- maybe_send_welcome_email(user),
838 {:ok, _} <- maybe_send_welcome_message(user),
839 {:ok, _} <- maybe_send_welcome_chat_message(user) do
844 defp send_user_approval_email(user) do
846 |> Pleroma.Emails.UserEmail.approval_pending_email()
847 |> Pleroma.Emails.Mailer.deliver_async()
852 defp send_admin_approval_emails(user) do
854 |> Enum.filter(fn user -> not is_nil(user.email) end)
855 |> Enum.each(fn superuser ->
857 |> Pleroma.Emails.AdminEmail.new_unapproved_registration(user)
858 |> Pleroma.Emails.Mailer.deliver_async()
864 defp maybe_send_welcome_message(user) do
865 if User.WelcomeMessage.enabled?() do
866 User.WelcomeMessage.post_message(user)
873 defp maybe_send_welcome_chat_message(user) do
874 if User.WelcomeChatMessage.enabled?() do
875 User.WelcomeChatMessage.post_message(user)
882 defp maybe_send_welcome_email(%User{email: email} = user) when is_binary(email) do
883 if User.WelcomeEmail.enabled?() do
884 User.WelcomeEmail.send_email(user)
891 defp maybe_send_welcome_email(_), do: {:ok, :noop}
893 @spec maybe_send_confirmation_email(User.t()) :: {:ok, :enqueued | :noop}
894 def maybe_send_confirmation_email(%User{is_confirmed: false, email: email} = user)
895 when is_binary(email) do
896 if Config.get([:instance, :account_activation_required]) do
897 send_confirmation_email(user)
904 def maybe_send_confirmation_email(_), do: {:ok, :noop}
906 @spec send_confirmation_email(Uset.t()) :: User.t()
907 def send_confirmation_email(%User{} = user) do
909 |> Pleroma.Emails.UserEmail.account_confirmation_email()
910 |> Pleroma.Emails.Mailer.deliver_async()
915 @spec maybe_send_registration_email(User.t()) :: {:ok, :enqueued | :noop}
916 defp maybe_send_registration_email(%User{email: email} = user) when is_binary(email) do
917 with false <- User.WelcomeEmail.enabled?(),
918 false <- Config.get([:instance, :account_activation_required], false),
919 false <- Config.get([:instance, :account_approval_required], false) do
921 |> Pleroma.Emails.UserEmail.successful_registration_email()
922 |> Pleroma.Emails.Mailer.deliver_async()
931 defp maybe_send_registration_email(_), do: {:ok, :noop}
933 def needs_update?(%User{local: true}), do: false
935 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
937 def needs_update?(%User{local: false} = user) do
938 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
941 def needs_update?(_), do: true
943 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
945 # "Locked" (self-locked) users demand explicit authorization of follow requests
946 def maybe_direct_follow(%User{} = follower, %User{local: true, is_locked: true} = followed) do
947 follow(follower, followed, :follow_pending)
950 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
951 follow(follower, followed)
954 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
955 if not ap_enabled?(followed) do
956 follow(follower, followed)
958 {:ok, follower, followed}
962 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
963 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
964 def follow_all(follower, followeds) do
966 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
967 |> Enum.each(&follow(follower, &1, :follow_accept))
972 def follow(%User{} = follower, %User{} = followed, state \\ :follow_accept) do
973 deny_follow_blocked = Config.get([:user, :deny_follow_blocked])
976 not followed.is_active ->
977 {:error, "Could not follow user: #{followed.nickname} is deactivated."}
979 deny_follow_blocked and blocks?(followed, follower) ->
980 {:error, "Could not follow user: #{followed.nickname} blocked you."}
983 FollowingRelationship.follow(follower, followed, state)
987 def unfollow(%User{ap_id: ap_id}, %User{ap_id: ap_id}) do
988 {:error, "Not subscribed!"}
991 @spec unfollow(User.t(), User.t()) :: {:ok, User.t(), Activity.t()} | {:error, String.t()}
992 def unfollow(%User{} = follower, %User{} = followed) do
993 case do_unfollow(follower, followed) do
994 {:ok, follower, followed} ->
995 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
1002 @spec do_unfollow(User.t(), User.t()) :: {:ok, User.t(), User.t()} | {:error, String.t()}
1003 defp do_unfollow(%User{} = follower, %User{} = followed) do
1004 case get_follow_state(follower, followed) do
1005 state when state in [:follow_pending, :follow_accept] ->
1006 FollowingRelationship.unfollow(follower, followed)
1009 {:error, "Not subscribed!"}
1013 @doc "Returns follow state as Pleroma.FollowingRelationship.State value"
1014 def get_follow_state(%User{} = follower, %User{} = following) do
1015 following_relationship = FollowingRelationship.get(follower, following)
1016 get_follow_state(follower, following, following_relationship)
1019 def get_follow_state(
1021 %User{} = following,
1022 following_relationship
1024 case {following_relationship, following.local} do
1026 case Utils.fetch_latest_follow(follower, following) do
1027 %Activity{data: %{"state" => state}} when state in ["pending", "accept"] ->
1028 FollowingRelationship.state_to_enum(state)
1034 {%{state: state}, _} ->
1042 def locked?(%User{} = user) do
1043 user.is_locked || false
1046 def get_by_id(id) do
1047 Repo.get_by(User, id: id)
1050 def get_by_ap_id(ap_id) do
1051 Repo.get_by(User, ap_id: ap_id)
1054 def get_all_by_ap_id(ap_ids) do
1055 from(u in __MODULE__,
1056 where: u.ap_id in ^ap_ids
1061 def get_all_by_ids(ids) do
1062 from(u in __MODULE__, where: u.id in ^ids)
1066 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
1067 # of the ap_id and the domain and tries to get that user
1068 def get_by_guessed_nickname(ap_id) do
1069 domain = URI.parse(ap_id).host
1070 name = List.last(String.split(ap_id, "/"))
1071 nickname = "#{name}@#{domain}"
1073 get_cached_by_nickname(nickname)
1076 def set_cache({:ok, user}), do: set_cache(user)
1077 def set_cache({:error, err}), do: {:error, err}
1079 def set_cache(%User{} = user) do
1080 @cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
1081 @cachex.put(:user_cache, "nickname:#{user.nickname}", user)
1082 @cachex.put(:user_cache, "friends_ap_ids:#{user.nickname}", get_user_friends_ap_ids(user))
1086 def update_and_set_cache(struct, params) do
1088 |> update_changeset(params)
1089 |> update_and_set_cache()
1092 def update_and_set_cache(%{data: %Pleroma.User{} = user} = changeset) do
1093 was_superuser_before_update = User.superuser?(user)
1095 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
1096 Pleroma.Elasticsearch.maybe_put_into_elasticsearch(user)
1099 |> maybe_remove_report_notifications(was_superuser_before_update)
1102 defp maybe_remove_report_notifications(
1103 {:ok, %Pleroma.User{} = user} = result,
1104 was_superuser_before_update
1106 if was_superuser_before_update and not User.superuser?(user),
1107 do: user |> Notification.destroy_multiple_from_types(["pleroma:report"])
1112 defp maybe_remove_report_notifications(result, _) do
1116 def get_user_friends_ap_ids(user) do
1117 from(u in User.get_friends_query(user), select: u.ap_id)
1121 @spec get_cached_user_friends_ap_ids(User.t()) :: [String.t()]
1122 def get_cached_user_friends_ap_ids(user) do
1123 @cachex.fetch!(:user_cache, "friends_ap_ids:#{user.ap_id}", fn _ ->
1124 get_user_friends_ap_ids(user)
1128 def invalidate_cache(user) do
1129 @cachex.del(:user_cache, "ap_id:#{user.ap_id}")
1130 @cachex.del(:user_cache, "nickname:#{user.nickname}")
1131 @cachex.del(:user_cache, "friends_ap_ids:#{user.ap_id}")
1132 @cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
1133 @cachex.del(:user_cache, "muted_users_ap_ids:#{user.ap_id}")
1136 @spec get_cached_by_ap_id(String.t()) :: User.t() | nil
1137 def get_cached_by_ap_id(ap_id) do
1138 key = "ap_id:#{ap_id}"
1140 with {:ok, nil} <- @cachex.get(:user_cache, key),
1141 user when not is_nil(user) <- get_by_ap_id(ap_id),
1142 {:ok, true} <- @cachex.put(:user_cache, key, user) do
1150 def get_cached_by_id(id) do
1154 @cachex.fetch!(:user_cache, key, fn _ ->
1155 user = get_by_id(id)
1158 @cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
1159 {:commit, user.ap_id}
1165 get_cached_by_ap_id(ap_id)
1168 def get_cached_by_nickname(nickname) do
1169 key = "nickname:#{nickname}"
1171 @cachex.fetch!(:user_cache, key, fn _ ->
1172 case get_or_fetch_by_nickname(nickname) do
1173 {:ok, user} -> {:commit, user}
1174 {:error, _error} -> {:ignore, nil}
1179 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
1180 restrict_to_local = Config.get([:instance, :limit_to_local_content])
1183 is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
1184 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
1186 restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
1187 get_cached_by_nickname(nickname_or_id)
1189 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
1190 get_cached_by_nickname(nickname_or_id)
1197 @spec get_by_nickname(String.t()) :: User.t() | nil
1198 def get_by_nickname(nickname) do
1199 Repo.get_by(User, nickname: nickname) ||
1200 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
1201 Repo.get_by(User, nickname: local_nickname(nickname))
1205 def get_by_email(email), do: Repo.get_by(User, email: email)
1207 def get_by_nickname_or_email(nickname_or_email) do
1208 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
1211 def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname)
1213 def get_or_fetch_by_nickname(nickname) do
1214 with %User{} = user <- get_by_nickname(nickname) do
1218 with [_nick, _domain] <- String.split(nickname, "@"),
1219 {:ok, user} <- fetch_by_nickname(nickname) do
1222 _e -> {:error, "not found " <> nickname}
1227 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
1228 def get_followers_query(%User{} = user, nil) do
1229 User.Query.build(%{followers: user, is_active: true})
1232 def get_followers_query(%User{} = user, page) do
1234 |> get_followers_query(nil)
1235 |> User.Query.paginate(page, 20)
1238 @spec get_followers_query(User.t()) :: Ecto.Query.t()
1239 def get_followers_query(%User{} = user), do: get_followers_query(user, nil)
1241 @spec get_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
1242 def get_followers(%User{} = user, page \\ nil) do
1244 |> get_followers_query(page)
1248 @spec get_external_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
1249 def get_external_followers(%User{} = user, page \\ nil) do
1251 |> get_followers_query(page)
1252 |> User.Query.build(%{external: true})
1256 def get_followers_ids(%User{} = user, page \\ nil) do
1258 |> get_followers_query(page)
1259 |> select([u], u.id)
1263 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
1264 def get_friends_query(%User{} = user, nil) do
1265 User.Query.build(%{friends: user, deactivated: false})
1268 def get_friends_query(%User{} = user, page) do
1270 |> get_friends_query(nil)
1271 |> User.Query.paginate(page, 20)
1274 @spec get_friends_query(User.t()) :: Ecto.Query.t()
1275 def get_friends_query(%User{} = user), do: get_friends_query(user, nil)
1277 def get_friends(%User{} = user, page \\ nil) do
1279 |> get_friends_query(page)
1283 def get_friends_ap_ids(%User{} = user) do
1285 |> get_friends_query(nil)
1286 |> select([u], u.ap_id)
1290 def get_friends_ids(%User{} = user, page \\ nil) do
1292 |> get_friends_query(page)
1293 |> select([u], u.id)
1297 def increase_note_count(%User{} = user) do
1299 |> where(id: ^user.id)
1300 |> update([u], inc: [note_count: 1])
1302 |> Repo.update_all([])
1304 {1, [user]} -> set_cache(user)
1309 def decrease_note_count(%User{} = user) do
1311 |> where(id: ^user.id)
1314 note_count: fragment("greatest(0, note_count - 1)")
1318 |> Repo.update_all([])
1320 {1, [user]} -> set_cache(user)
1325 def update_note_count(%User{} = user, note_count \\ nil) do
1330 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
1336 |> cast(%{note_count: note_count}, [:note_count])
1337 |> update_and_set_cache()
1340 @spec maybe_fetch_follow_information(User.t()) :: User.t()
1341 def maybe_fetch_follow_information(user) do
1342 with {:ok, user} <- fetch_follow_information(user) do
1346 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
1352 def fetch_follow_information(user) do
1353 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
1355 |> follow_information_changeset(info)
1356 |> update_and_set_cache()
1360 defp follow_information_changeset(user, params) do
1367 :hide_followers_count,
1372 @spec update_follower_count(User.t()) :: {:ok, User.t()}
1373 def update_follower_count(%User{} = user) do
1374 if user.local or !Config.get([:instance, :external_user_synchronization]) do
1375 follower_count = FollowingRelationship.follower_count(user)
1378 |> follow_information_changeset(%{follower_count: follower_count})
1379 |> update_and_set_cache
1381 {:ok, maybe_fetch_follow_information(user)}
1385 @spec update_following_count(User.t()) :: {:ok, User.t()}
1386 def update_following_count(%User{local: false} = user) do
1387 if Config.get([:instance, :external_user_synchronization]) do
1388 {:ok, maybe_fetch_follow_information(user)}
1394 def update_following_count(%User{local: true} = user) do
1395 following_count = FollowingRelationship.following_count(user)
1398 |> follow_information_changeset(%{following_count: following_count})
1399 |> update_and_set_cache()
1402 @spec get_users_from_set([String.t()], keyword()) :: [User.t()]
1403 def get_users_from_set(ap_ids, opts \\ []) do
1404 local_only = Keyword.get(opts, :local_only, true)
1405 criteria = %{ap_id: ap_ids, is_active: true}
1406 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
1408 User.Query.build(criteria)
1412 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
1413 def get_recipients_from_activity(%Activity{recipients: to, actor: actor}) do
1416 query = User.Query.build(%{recipients_from_activity: to, local: true, is_active: true})
1422 @spec mute(User.t(), User.t(), map()) ::
1423 {:ok, list(UserRelationship.t())} | {:error, String.t()}
1424 def mute(%User{} = muter, %User{} = mutee, params \\ %{}) do
1425 notifications? = Map.get(params, :notifications, true)
1426 expires_in = Map.get(params, :expires_in, 0)
1428 with {:ok, user_mute} <- UserRelationship.create_mute(muter, mutee),
1429 {:ok, user_notification_mute} <-
1430 (notifications? && UserRelationship.create_notification_mute(muter, mutee)) ||
1432 if expires_in > 0 do
1433 Pleroma.Workers.MuteExpireWorker.enqueue(
1435 %{"muter_id" => muter.id, "mutee_id" => mutee.id},
1436 schedule_in: expires_in
1440 @cachex.del(:user_cache, "muted_users_ap_ids:#{muter.ap_id}")
1442 {:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
1446 def unmute(%User{} = muter, %User{} = mutee) do
1447 with {:ok, user_mute} <- UserRelationship.delete_mute(muter, mutee),
1448 {:ok, user_notification_mute} <-
1449 UserRelationship.delete_notification_mute(muter, mutee) do
1450 @cachex.del(:user_cache, "muted_users_ap_ids:#{muter.ap_id}")
1451 {:ok, [user_mute, user_notification_mute]}
1455 def unmute(muter_id, mutee_id) do
1456 with {:muter, %User{} = muter} <- {:muter, User.get_by_id(muter_id)},
1457 {:mutee, %User{} = mutee} <- {:mutee, User.get_by_id(mutee_id)} do
1458 unmute(muter, mutee)
1460 {who, result} = error ->
1462 "User.unmute/2 failed. #{who}: #{result}, muter_id: #{muter_id}, mutee_id: #{mutee_id}"
1469 def subscribe(%User{} = subscriber, %User{} = target) do
1470 deny_follow_blocked = Config.get([:user, :deny_follow_blocked])
1472 if blocks?(target, subscriber) and deny_follow_blocked do
1473 {:error, "Could not subscribe: #{target.nickname} is blocking you"}
1475 # Note: the relationship is inverse: subscriber acts as relationship target
1476 UserRelationship.create_inverse_subscription(target, subscriber)
1480 def subscribe(%User{} = subscriber, %{ap_id: ap_id}) do
1481 with %User{} = subscribee <- get_cached_by_ap_id(ap_id) do
1482 subscribe(subscriber, subscribee)
1486 def unsubscribe(%User{} = unsubscriber, %User{} = target) do
1487 # Note: the relationship is inverse: subscriber acts as relationship target
1488 UserRelationship.delete_inverse_subscription(target, unsubscriber)
1491 def unsubscribe(%User{} = unsubscriber, %{ap_id: ap_id}) do
1492 with %User{} = user <- get_cached_by_ap_id(ap_id) do
1493 unsubscribe(unsubscriber, user)
1497 def block(%User{} = blocker, %User{} = blocked) do
1498 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
1500 if following?(blocker, blocked) do
1501 {:ok, blocker, _} = unfollow(blocker, blocked)
1507 # clear any requested follows as well
1509 case CommonAPI.reject_follow_request(blocked, blocker) do
1510 {:ok, %User{} = updated_blocked} -> updated_blocked
1514 unsubscribe(blocked, blocker)
1516 unfollowing_blocked = Config.get([:activitypub, :unfollow_blocked], true)
1517 if unfollowing_blocked && following?(blocked, blocker), do: unfollow(blocked, blocker)
1519 {:ok, blocker} = update_follower_count(blocker)
1520 {:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
1521 add_to_block(blocker, blocked)
1524 # helper to handle the block given only an actor's AP id
1525 def block(%User{} = blocker, %{ap_id: ap_id}) do
1526 block(blocker, get_cached_by_ap_id(ap_id))
1529 def unblock(%User{} = blocker, %User{} = blocked) do
1530 remove_from_block(blocker, blocked)
1533 # helper to handle the block given only an actor's AP id
1534 def unblock(%User{} = blocker, %{ap_id: ap_id}) do
1535 unblock(blocker, get_cached_by_ap_id(ap_id))
1538 def mutes?(nil, _), do: false
1539 def mutes?(%User{} = user, %User{} = target), do: mutes_user?(user, target)
1541 def mutes_user?(%User{} = user, %User{} = target) do
1542 UserRelationship.mute_exists?(user, target)
1545 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
1546 def muted_notifications?(nil, _), do: false
1548 def muted_notifications?(%User{} = user, %User{} = target),
1549 do: UserRelationship.notification_mute_exists?(user, target)
1551 def blocks?(nil, _), do: false
1553 def blocks?(%User{} = user, %User{} = target) do
1554 blocks_user?(user, target) ||
1555 (blocks_domain?(user, target) and not User.following?(user, target))
1558 def blocks_user?(%User{} = user, %User{} = target) do
1559 UserRelationship.block_exists?(user, target)
1562 def blocks_user?(_, _), do: false
1564 def blocks_domain?(%User{} = user, %User{} = target) do
1565 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
1566 %{host: host} = URI.parse(target.ap_id)
1567 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1570 def blocks_domain?(_, _), do: false
1572 def subscribed_to?(%User{} = user, %User{} = target) do
1573 # Note: the relationship is inverse: subscriber acts as relationship target
1574 UserRelationship.inverse_subscription_exists?(target, user)
1577 def subscribed_to?(%User{} = user, %{ap_id: ap_id}) do
1578 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1579 subscribed_to?(user, target)
1584 Returns map of outgoing (blocked, muted etc.) relationships' user AP IDs by relation type.
1585 E.g. `outgoing_relationships_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}`
1587 @spec outgoing_relationships_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())}
1588 def outgoing_relationships_ap_ids(_user, []), do: %{}
1590 def outgoing_relationships_ap_ids(nil, _relationship_types), do: %{}
1592 def outgoing_relationships_ap_ids(%User{} = user, relationship_types)
1593 when is_list(relationship_types) do
1596 |> assoc(:outgoing_relationships)
1597 |> join(:inner, [user_rel], u in assoc(user_rel, :target))
1598 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1599 |> select([user_rel, u], [user_rel.relationship_type, fragment("array_agg(?)", u.ap_id)])
1600 |> group_by([user_rel, u], user_rel.relationship_type)
1602 |> Enum.into(%{}, fn [k, v] -> {k, v} end)
1607 fn rel_type -> {rel_type, db_result[rel_type] || []} end
1611 def incoming_relationships_ungrouped_ap_ids(user, relationship_types, ap_ids \\ nil)
1613 def incoming_relationships_ungrouped_ap_ids(_user, [], _ap_ids), do: []
1615 def incoming_relationships_ungrouped_ap_ids(nil, _relationship_types, _ap_ids), do: []
1617 def incoming_relationships_ungrouped_ap_ids(%User{} = user, relationship_types, ap_ids)
1618 when is_list(relationship_types) do
1620 |> assoc(:incoming_relationships)
1621 |> join(:inner, [user_rel], u in assoc(user_rel, :source))
1622 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1623 |> maybe_filter_on_ap_id(ap_ids)
1624 |> select([user_rel, u], u.ap_id)
1629 defp maybe_filter_on_ap_id(query, ap_ids) when is_list(ap_ids) do
1630 where(query, [user_rel, u], u.ap_id in ^ap_ids)
1633 defp maybe_filter_on_ap_id(query, _ap_ids), do: query
1635 def set_activation_async(user, status \\ true) do
1636 BackgroundWorker.enqueue("user_activation", %{"user_id" => user.id, "status" => status})
1639 @spec set_activation([User.t()], boolean()) :: {:ok, User.t()} | {:error, Changeset.t()}
1640 def set_activation(users, status) when is_list(users) do
1641 Repo.transaction(fn ->
1642 for user <- users, do: set_activation(user, status)
1646 @spec set_activation(User.t(), boolean()) :: {:ok, User.t()} | {:error, Changeset.t()}
1647 def set_activation(%User{} = user, status) do
1648 with {:ok, user} <- set_activation_status(user, status) do
1651 |> Enum.filter(& &1.local)
1652 |> Enum.each(&set_cache(update_following_count(&1)))
1654 # Only update local user counts, remote will be update during the next pull.
1657 |> Enum.filter(& &1.local)
1658 |> Enum.each(&do_unfollow(user, &1))
1664 def approve(users) when is_list(users) do
1665 Repo.transaction(fn ->
1666 Enum.map(users, fn user ->
1667 with {:ok, user} <- approve(user), do: user
1672 def approve(%User{is_approved: false} = user) do
1673 with chg <- change(user, is_approved: true),
1674 {:ok, user} <- update_and_set_cache(chg) do
1675 post_register_action(user)
1680 def approve(%User{} = user), do: {:ok, user}
1682 def confirm(users) when is_list(users) do
1683 Repo.transaction(fn ->
1684 Enum.map(users, fn user ->
1685 with {:ok, user} <- confirm(user), do: user
1690 def confirm(%User{is_confirmed: false} = user) do
1691 with chg <- confirmation_changeset(user, set_confirmation: true),
1692 {:ok, user} <- update_and_set_cache(chg) do
1693 post_register_action(user)
1698 def confirm(%User{} = user), do: {:ok, user}
1700 def set_suggestion(users, is_suggested) when is_list(users) do
1701 Repo.transaction(fn ->
1702 Enum.map(users, fn user ->
1703 with {:ok, user} <- set_suggestion(user, is_suggested), do: user
1708 def set_suggestion(%User{is_suggested: is_suggested} = user, is_suggested), do: {:ok, user}
1710 def set_suggestion(%User{} = user, is_suggested) when is_boolean(is_suggested) do
1712 |> change(is_suggested: is_suggested)
1713 |> update_and_set_cache()
1716 def update_notification_settings(%User{} = user, settings) do
1718 |> cast(%{notification_settings: settings}, [])
1719 |> cast_embed(:notification_settings)
1720 |> validate_required([:notification_settings])
1721 |> update_and_set_cache()
1724 @spec purge_user_changeset(User.t()) :: Changeset.t()
1725 def purge_user_changeset(user) do
1726 # "Right to be forgotten"
1727 # https://gdpr.eu/right-to-be-forgotten/
1736 last_refreshed_at: nil,
1737 last_digest_emailed_at: nil,
1744 password_reset_pending: false,
1745 registration_reason: nil,
1746 confirmation_token: nil,
1750 is_moderator: false,
1752 mastofe_settings: nil,
1755 pleroma_settings_store: %{},
1758 is_discoverable: false,
1762 # nickname: preserved
1766 # Purge doesn't delete the user from the database.
1767 # It just nulls all its fields and deactivates it.
1768 # See `User.purge_user_changeset/1` above.
1769 defp purge(%User{} = user) do
1771 |> purge_user_changeset()
1772 |> update_and_set_cache()
1775 def delete(users) when is_list(users) do
1776 for user <- users, do: delete(user)
1779 def delete(%User{} = user) do
1780 # Purge the user immediately
1782 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1785 # *Actually* delete the user from the DB
1786 defp delete_from_db(%User{} = user) do
1787 invalidate_cache(user)
1791 # If the user never finalized their account, it's safe to delete them.
1792 defp maybe_delete_from_db(%User{local: true, is_confirmed: false} = user),
1793 do: delete_from_db(user)
1795 defp maybe_delete_from_db(%User{local: true, is_approved: false} = user),
1796 do: delete_from_db(user)
1798 defp maybe_delete_from_db(user), do: {:ok, user}
1800 def perform(:force_password_reset, user), do: force_password_reset(user)
1802 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1803 def perform(:delete, %User{} = user) do
1804 # Purge the user again, in case perform/2 is called directly
1807 # Remove all relationships
1810 |> Enum.each(fn follower ->
1811 ActivityPub.unfollow(follower, user)
1812 unfollow(follower, user)
1817 |> Enum.each(fn followed ->
1818 ActivityPub.unfollow(user, followed)
1819 unfollow(user, followed)
1822 delete_user_activities(user)
1823 delete_notifications_from_user_activities(user)
1824 delete_outgoing_pending_follow_requests(user)
1826 maybe_delete_from_db(user)
1829 def perform(:set_activation_async, user, status), do: set_activation(user, status)
1831 @spec external_users_query() :: Ecto.Query.t()
1832 def external_users_query do
1840 @spec external_users(keyword()) :: [User.t()]
1841 def external_users(opts \\ []) do
1843 external_users_query()
1844 |> select([u], struct(u, [:id, :ap_id]))
1848 do: where(query, [u], u.id > ^opts[:max_id]),
1853 do: limit(query, ^opts[:limit]),
1859 def delete_notifications_from_user_activities(%User{ap_id: ap_id}) do
1861 |> join(:inner, [n], activity in assoc(n, :activity))
1862 |> where([n, a], fragment("? = ?", a.actor, ^ap_id))
1863 |> Repo.delete_all()
1866 def delete_user_activities(%User{ap_id: ap_id} = user) do
1868 |> Activity.Queries.by_actor()
1869 |> Repo.chunk_stream(50, :batches)
1870 |> Stream.each(fn activities ->
1871 Enum.each(activities, fn activity -> delete_activity(activity, user) end)
1876 defp delete_activity(%{data: %{"type" => "Create", "object" => object}} = activity, user) do
1877 with {_, %Object{}} <- {:find_object, Object.get_by_ap_id(object)},
1878 {:ok, delete_data, _} <- Builder.delete(user, object) do
1879 Pipeline.common_pipeline(delete_data, local: user.local)
1881 {:find_object, nil} ->
1882 # We have the create activity, but not the object, it was probably pruned.
1883 # Insert a tombstone and try again
1884 with {:ok, tombstone_data, _} <- Builder.tombstone(user.ap_id, object),
1885 {:ok, _tombstone} <- Object.create(tombstone_data) do
1886 delete_activity(activity, user)
1890 Logger.error("Could not delete #{object} created by #{activity.data["ap_id"]}")
1891 Logger.error("Error: #{inspect(e)}")
1895 defp delete_activity(%{data: %{"type" => type}} = activity, user)
1896 when type in ["Like", "Announce"] do
1897 {:ok, undo, _} = Builder.undo(user, activity)
1898 Pipeline.common_pipeline(undo, local: user.local)
1901 defp delete_activity(_activity, _user), do: "Doing nothing"
1903 defp delete_outgoing_pending_follow_requests(user) do
1905 |> FollowingRelationship.outgoing_pending_follow_requests_query()
1906 |> Repo.delete_all()
1909 def html_filter_policy(%User{no_rich_text: true}) do
1910 Pleroma.HTML.Scrubber.TwitterText
1913 def html_filter_policy(_), do: Config.get([:markup, :scrub_policy])
1915 def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
1917 def get_or_fetch_by_ap_id(ap_id) do
1918 cached_user = get_cached_by_ap_id(ap_id)
1920 maybe_fetched_user = needs_update?(cached_user) && fetch_by_ap_id(ap_id)
1922 case {cached_user, maybe_fetched_user} do
1923 {_, {:ok, %User{} = user}} ->
1926 {%User{} = user, _} ->
1930 {:error, :not_found}
1935 Creates an internal service actor by URI if missing.
1936 Optionally takes nickname for addressing.
1938 @spec get_or_create_service_actor_by_ap_id(String.t(), String.t()) :: User.t() | nil
1939 def get_or_create_service_actor_by_ap_id(uri, nickname) do
1941 case get_cached_by_ap_id(uri) do
1943 with {:error, %{errors: errors}} <- create_service_actor(uri, nickname) do
1944 Logger.error("Cannot create service actor: #{uri}/.\n#{inspect(errors)}")
1948 %User{invisible: false} = user ->
1958 @spec set_invisible(User.t()) :: {:ok, User.t()}
1959 defp set_invisible(user) do
1961 |> change(%{invisible: true})
1962 |> update_and_set_cache()
1965 @spec create_service_actor(String.t(), String.t()) ::
1966 {:ok, User.t()} | {:error, Ecto.Changeset.t()}
1967 defp create_service_actor(uri, nickname) do
1973 follower_address: uri <> "/followers"
1976 |> unique_constraint(:nickname)
1981 def public_key(%{public_key: public_key_pem}) when is_binary(public_key_pem) do
1984 |> :public_key.pem_decode()
1986 |> :public_key.pem_entry_decode()
1991 def public_key(_), do: {:error, "key not found"}
1993 def get_public_key_for_ap_id(ap_id) do
1994 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1995 {:ok, public_key} <- public_key(user) do
2002 def ap_enabled?(%User{local: true}), do: true
2003 def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled
2004 def ap_enabled?(_), do: false
2006 @doc "Gets or fetch a user by uri or nickname."
2007 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
2008 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
2009 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
2011 # wait a period of time and return newest version of the User structs
2012 # this is because we have synchronous follow APIs and need to simulate them
2013 # with an async handshake
2014 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
2015 with %User{} = a <- get_cached_by_id(a.id),
2016 %User{} = b <- get_cached_by_id(b.id) do
2023 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
2024 with :ok <- :timer.sleep(timeout),
2025 %User{} = a <- get_cached_by_id(a.id),
2026 %User{} = b <- get_cached_by_id(b.id) do
2033 def parse_bio(bio) when is_binary(bio) and bio != "" do
2035 |> CommonUtils.format_input("text/plain", mentions_format: :full)
2039 def parse_bio(_), do: ""
2041 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
2042 # TODO: get profile URLs other than user.ap_id
2043 profile_urls = [user.ap_id]
2046 |> CommonUtils.format_input("text/plain",
2047 mentions_format: :full,
2048 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
2053 def parse_bio(_, _), do: ""
2055 def tag(user_identifiers, tags) when is_list(user_identifiers) do
2056 Repo.transaction(fn ->
2057 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
2061 def tag(nickname, tags) when is_binary(nickname),
2062 do: tag(get_by_nickname(nickname), tags)
2064 def tag(%User{} = user, tags),
2065 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
2067 def untag(user_identifiers, tags) when is_list(user_identifiers) do
2068 Repo.transaction(fn ->
2069 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
2073 def untag(nickname, tags) when is_binary(nickname),
2074 do: untag(get_by_nickname(nickname), tags)
2076 def untag(%User{} = user, tags),
2077 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
2079 defp update_tags(%User{} = user, new_tags) do
2080 {:ok, updated_user} =
2082 |> change(%{tags: new_tags})
2083 |> update_and_set_cache()
2088 defp normalize_tags(tags) do
2091 |> Enum.map(&String.downcase/1)
2094 defp local_nickname_regex do
2095 if Config.get([:instance, :extended_nickname_format]) do
2096 @extended_local_nickname_regex
2098 @strict_local_nickname_regex
2102 def local_nickname(nickname_or_mention) do
2105 |> String.split("@")
2109 def full_nickname(%User{} = user) do
2110 if String.contains?(user.nickname, "@") do
2113 %{host: host} = URI.parse(user.ap_id)
2114 user.nickname <> "@" <> host
2118 def full_nickname(nickname_or_mention),
2119 do: String.trim_leading(nickname_or_mention, "@")
2121 def error_user(ap_id) do
2125 nickname: "erroruser@example.com",
2126 inserted_at: NaiveDateTime.utc_now()
2130 @spec all_superusers() :: [User.t()]
2131 def all_superusers do
2132 User.Query.build(%{super_users: true, local: true, is_active: true})
2136 def muting_reblogs?(%User{} = user, %User{} = target) do
2137 UserRelationship.reblog_mute_exists?(user, target)
2140 def showing_reblogs?(%User{} = user, %User{} = target) do
2141 not muting_reblogs?(user, target)
2145 The function returns a query to get users with no activity for given interval of days.
2146 Inactive users are those who didn't read any notification, or had any activity where
2147 the user is the activity's actor, during `inactivity_threshold` days.
2148 Deactivated users will not appear in this list.
2152 iex> Pleroma.User.list_inactive_users()
2155 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
2156 def list_inactive_users_query(inactivity_threshold \\ 7) do
2157 negative_inactivity_threshold = -inactivity_threshold
2158 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
2159 # Subqueries are not supported in `where` clauses, join gets too complicated.
2160 has_read_notifications =
2161 from(n in Pleroma.Notification,
2162 where: n.seen == true,
2164 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
2167 |> Pleroma.Repo.all()
2169 from(u in Pleroma.User,
2170 left_join: a in Pleroma.Activity,
2171 on: u.ap_id == a.actor,
2172 where: not is_nil(u.nickname),
2173 where: u.is_active == ^true,
2174 where: u.id not in ^has_read_notifications,
2177 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
2178 is_nil(max(a.inserted_at))
2183 Enable or disable email notifications for user
2187 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => false}}, "digest", true)
2188 Pleroma.User{email_notifications: %{"digest" => true}}
2190 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => true}}, "digest", false)
2191 Pleroma.User{email_notifications: %{"digest" => false}}
2193 @spec switch_email_notifications(t(), String.t(), boolean()) ::
2194 {:ok, t()} | {:error, Ecto.Changeset.t()}
2195 def switch_email_notifications(user, type, status) do
2196 User.update_email_notifications(user, %{type => status})
2200 Set `last_digest_emailed_at` value for the user to current time
2202 @spec touch_last_digest_emailed_at(t()) :: t()
2203 def touch_last_digest_emailed_at(user) do
2204 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
2206 {:ok, updated_user} =
2208 |> change(%{last_digest_emailed_at: now})
2209 |> update_and_set_cache()
2214 @spec set_confirmation(User.t(), boolean()) :: {:ok, User.t()} | {:error, Changeset.t()}
2215 def set_confirmation(%User{} = user, bool) do
2217 |> confirmation_changeset(set_confirmation: bool)
2218 |> update_and_set_cache()
2221 def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
2225 def get_mascot(%{mascot: mascot}) when is_nil(mascot) do
2226 # use instance-default
2227 config = Config.get([:assets, :mascots])
2228 default_mascot = Config.get([:assets, :default_mascot])
2229 mascot = Keyword.get(config, default_mascot)
2232 "id" => "default-mascot",
2233 "url" => mascot[:url],
2234 "preview_url" => mascot[:url],
2236 "mime_type" => mascot[:mime_type]
2241 def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
2243 def ensure_keys_present(%User{} = user) do
2244 with {:ok, pem} <- Keys.generate_rsa_pem() do
2246 |> cast(%{keys: pem}, [:keys])
2247 |> validate_required([:keys])
2248 |> update_and_set_cache()
2252 def get_ap_ids_by_nicknames(nicknames) do
2254 where: u.nickname in ^nicknames,
2260 defp put_password_hash(
2261 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
2263 change(changeset, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt(password))
2266 defp put_password_hash(changeset), do: changeset
2268 def is_internal_user?(%User{nickname: nil}), do: true
2269 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
2270 def is_internal_user?(_), do: false
2272 # A hack because user delete activities have a fake id for whatever reason
2273 # TODO: Get rid of this
2274 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
2276 def get_delivered_users_by_object_id(object_id) do
2278 inner_join: delivery in assoc(u, :deliveries),
2279 where: delivery.object_id == ^object_id
2284 def change_email(user, email) do
2286 |> cast(%{email: email}, [:email])
2287 |> maybe_validate_required_email(false)
2288 |> unique_constraint(:email)
2289 |> validate_format(:email, @email_regex)
2290 |> update_and_set_cache()
2293 # Internal function; public one is `deactivate/2`
2294 defp set_activation_status(user, status) do
2296 |> cast(%{is_active: status}, [:is_active])
2297 |> update_and_set_cache()
2300 def update_banner(user, banner) do
2302 |> cast(%{banner: banner}, [:banner])
2303 |> update_and_set_cache()
2306 def update_background(user, background) do
2308 |> cast(%{background: background}, [:background])
2309 |> update_and_set_cache()
2312 def validate_fields(changeset, remote? \\ false) do
2313 limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
2314 limit = Config.get([:instance, limit_name], 0)
2317 |> validate_length(:fields, max: limit)
2318 |> validate_change(:fields, fn :fields, fields ->
2319 if Enum.all?(fields, &valid_field?/1) do
2327 defp valid_field?(%{"name" => name, "value" => value}) do
2328 name_limit = Config.get([:instance, :account_field_name_length], 255)
2329 value_limit = Config.get([:instance, :account_field_value_length], 255)
2331 is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
2332 String.length(value) <= value_limit
2335 defp valid_field?(_), do: false
2337 defp truncate_field(%{"name" => name, "value" => value}) do
2339 String.split_at(name, Config.get([:instance, :account_field_name_length], 255))
2342 String.split_at(value, Config.get([:instance, :account_field_value_length], 255))
2344 %{"name" => name, "value" => value}
2347 def admin_api_update(user, params) do
2354 |> update_and_set_cache()
2357 @doc "Signs user out of all applications"
2358 def global_sign_out(user) do
2359 OAuth.Authorization.delete_user_authorizations(user)
2360 OAuth.Token.delete_user_tokens(user)
2363 def mascot_update(user, url) do
2365 |> cast(%{mascot: url}, [:mascot])
2366 |> validate_required([:mascot])
2367 |> update_and_set_cache()
2370 def mastodon_settings_update(user, settings) do
2372 |> cast(%{mastofe_settings: settings}, [:mastofe_settings])
2373 |> validate_required([:mastofe_settings])
2374 |> update_and_set_cache()
2377 @spec confirmation_changeset(User.t(), keyword()) :: Changeset.t()
2378 def confirmation_changeset(user, set_confirmation: confirmed?) do
2383 confirmation_token: nil
2387 is_confirmed: false,
2388 confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
2392 cast(user, params, [:is_confirmed, :confirmation_token])
2395 @spec approval_changeset(User.t(), keyword()) :: Changeset.t()
2396 def approval_changeset(user, set_approval: approved?) do
2397 cast(user, %{is_approved: approved?}, [:is_approved])
2400 @spec add_pinned_object_id(User.t(), String.t()) :: {:ok, User.t()} | {:error, term()}
2401 def add_pinned_object_id(%User{} = user, object_id) do
2402 if !user.pinned_objects[object_id] do
2403 params = %{pinned_objects: Map.put(user.pinned_objects, object_id, NaiveDateTime.utc_now())}
2406 |> cast(params, [:pinned_objects])
2407 |> validate_change(:pinned_objects, fn :pinned_objects, pinned_objects ->
2408 max_pinned_statuses = Config.get([:instance, :max_pinned_statuses], 0)
2410 if Enum.count(pinned_objects) <= max_pinned_statuses do
2413 [pinned_objects: "You have already pinned the maximum number of statuses"]
2419 |> update_and_set_cache()
2422 @spec remove_pinned_object_id(User.t(), String.t()) :: {:ok, t()} | {:error, term()}
2423 def remove_pinned_object_id(%User{} = user, object_id) do
2426 %{pinned_objects: Map.delete(user.pinned_objects, object_id)},
2429 |> update_and_set_cache()
2432 def update_email_notifications(user, settings) do
2433 email_notifications =
2434 user.email_notifications
2435 |> Map.merge(settings)
2436 |> Map.take(["digest"])
2438 params = %{email_notifications: email_notifications}
2439 fields = [:email_notifications]
2442 |> cast(params, fields)
2443 |> validate_required(fields)
2444 |> update_and_set_cache()
2447 defp set_domain_blocks(user, domain_blocks) do
2448 params = %{domain_blocks: domain_blocks}
2451 |> cast(params, [:domain_blocks])
2452 |> validate_required([:domain_blocks])
2453 |> update_and_set_cache()
2456 def block_domain(user, domain_blocked) do
2457 set_domain_blocks(user, Enum.uniq([domain_blocked | user.domain_blocks]))
2460 def unblock_domain(user, domain_blocked) do
2461 set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked))
2464 @spec add_to_block(User.t(), User.t()) ::
2465 {:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()}
2466 defp add_to_block(%User{} = user, %User{} = blocked) do
2467 with {:ok, relationship} <- UserRelationship.create_block(user, blocked) do
2468 @cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
2473 @spec add_to_block(User.t(), User.t()) ::
2474 {:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()}
2475 defp remove_from_block(%User{} = user, %User{} = blocked) do
2476 with {:ok, relationship} <- UserRelationship.delete_block(user, blocked) do
2477 @cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
2482 def set_invisible(user, invisible) do
2483 params = %{invisible: invisible}
2486 |> cast(params, [:invisible])
2487 |> validate_required([:invisible])
2488 |> update_and_set_cache()
2491 def sanitize_html(%User{} = user) do
2492 sanitize_html(user, nil)
2495 # User data that mastodon isn't filtering (treated as plaintext):
2498 def sanitize_html(%User{} = user, filter) do
2500 Enum.map(user.fields, fn %{"name" => name, "value" => value} ->
2503 "value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
2508 |> Map.put(:bio, HTML.filter_tags(user.bio, filter))
2509 |> Map.put(:fields, fields)
2512 def get_host(%User{ap_id: ap_id} = _user) do
2513 URI.parse(ap_id).host
2516 def update_last_active_at(%__MODULE__{local: true} = user) do
2518 |> cast(%{last_active_at: NaiveDateTime.utc_now()}, [:last_active_at])
2519 |> update_and_set_cache()
2522 def active_user_count(days \\ 30) do
2523 active_after = Timex.shift(NaiveDateTime.utc_now(), days: -days)
2526 |> where([u], u.last_active_at >= ^active_after)
2527 |> where([u], u.local == true)
2528 |> Repo.aggregate(:count)
2531 def update_last_status_at(user) do
2533 |> where(id: ^user.id)
2534 |> update([u], set: [last_status_at: fragment("NOW()")])
2536 |> Repo.update_all([])
2538 {1, [user]} -> set_cache(user)