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])
198 :multi_factor_authentication_settings,
206 for {_relationship_type, [{_outgoing_relation, outgoing_relation_target}, _]} <-
207 @user_relationships_config do
208 # `def blocked_users_relation/2`, `def muted_users_relation/2`,
209 # `def reblog_muted_users_relation/2`, `def notification_muted_users/2`,
210 # `def subscriber_users/2`
211 def unquote(:"#{outgoing_relation_target}_relation")(user, restrict_deactivated? \\ false) do
212 target_users_query = assoc(user, unquote(outgoing_relation_target))
214 if restrict_deactivated? do
216 |> User.Query.build(%{deactivated: false})
222 # `def blocked_users/2`, `def muted_users/2`, `def reblog_muted_users/2`,
223 # `def notification_muted_users/2`, `def subscriber_users/2`
224 def unquote(outgoing_relation_target)(user, restrict_deactivated? \\ false) do
226 |> apply(unquote(:"#{outgoing_relation_target}_relation"), [
228 restrict_deactivated?
233 # `def blocked_users_ap_ids/2`, `def muted_users_ap_ids/2`, `def reblog_muted_users_ap_ids/2`,
234 # `def notification_muted_users_ap_ids/2`, `def subscriber_users_ap_ids/2`
235 def unquote(:"#{outgoing_relation_target}_ap_ids")(user, restrict_deactivated? \\ false) do
237 |> apply(unquote(:"#{outgoing_relation_target}_relation"), [
239 restrict_deactivated?
241 |> select([u], u.ap_id)
246 def cached_blocked_users_ap_ids(user) do
247 @cachex.fetch!(:user_cache, "blocked_users_ap_ids:#{user.ap_id}", fn _ ->
248 blocked_users_ap_ids(user)
252 def cached_muted_users_ap_ids(user) do
253 @cachex.fetch!(:user_cache, "muted_users_ap_ids:#{user.ap_id}", fn _ ->
254 muted_users_ap_ids(user)
258 defdelegate following_count(user), to: FollowingRelationship
259 defdelegate following(user), to: FollowingRelationship
260 defdelegate following?(follower, followed), to: FollowingRelationship
261 defdelegate following_ap_ids(user), to: FollowingRelationship
262 defdelegate get_follow_requests(user), to: FollowingRelationship
263 defdelegate search(query, opts \\ []), to: User.Search
266 Dumps Flake Id to SQL-compatible format (16-byte UUID).
267 E.g. "9pQtDGXuq4p3VlcJEm" -> <<0, 0, 1, 110, 179, 218, 42, 92, 213, 41, 44, 227, 95, 213, 0, 0>>
269 def binary_id(source_id) when is_binary(source_id) do
270 with {:ok, dumped_id} <- FlakeId.Ecto.CompatType.dump(source_id) do
277 def binary_id(source_ids) when is_list(source_ids) do
278 Enum.map(source_ids, &binary_id/1)
281 def binary_id(%User{} = user), do: binary_id(user.id)
283 @doc "Returns status account"
284 @spec account_status(User.t()) :: account_status()
285 def account_status(%User{is_active: false}), do: :deactivated
286 def account_status(%User{password_reset_pending: true}), do: :password_reset_pending
287 def account_status(%User{local: true, is_approved: false}), do: :approval_pending
288 def account_status(%User{local: true, is_confirmed: false}), do: :confirmation_pending
289 def account_status(%User{}), do: :active
291 @spec visible_for(User.t(), User.t() | nil) ::
294 | :restricted_unauthenticated
296 | :confirmation_pending
297 def visible_for(user, for_user \\ nil)
299 def visible_for(%User{invisible: true}, _), do: :invisible
301 def visible_for(%User{id: user_id}, %User{id: user_id}), do: :visible
303 def visible_for(%User{} = user, nil) do
304 if restrict_unauthenticated?(user) do
305 :restrict_unauthenticated
307 visible_account_status(user)
311 def visible_for(%User{} = user, for_user) do
312 if superuser?(for_user) do
315 visible_account_status(user)
319 def visible_for(_, _), do: :invisible
321 defp restrict_unauthenticated?(%User{local: true}) do
322 Config.restrict_unauthenticated_access?(:profiles, :local)
325 defp restrict_unauthenticated?(%User{local: _}) do
326 Config.restrict_unauthenticated_access?(:profiles, :remote)
329 defp visible_account_status(user) do
330 status = account_status(user)
332 if status in [:active, :password_reset_pending] do
339 @spec superuser?(User.t()) :: boolean()
340 def superuser?(%User{local: true, is_admin: true}), do: true
341 def superuser?(%User{local: true, is_moderator: true}), do: true
342 def superuser?(_), do: false
344 @spec invisible?(User.t()) :: boolean()
345 def invisible?(%User{invisible: true}), do: true
346 def invisible?(_), do: false
348 def avatar_url(user, options \\ []) do
350 %{"url" => [%{"href" => href} | _]} ->
354 unless options[:no_default] do
355 Config.get([:assets, :default_user_avatar], "#{Endpoint.url()}/images/avi.png")
360 def banner_url(user, options \\ []) do
362 %{"url" => [%{"href" => href} | _]} -> href
363 _ -> !options[:no_default] && "#{Endpoint.url()}/images/banner.png"
367 # Should probably be renamed or removed
368 @spec ap_id(User.t()) :: String.t()
369 def ap_id(%User{nickname: nickname}), do: "#{Endpoint.url()}/users/#{nickname}"
371 @spec ap_followers(User.t()) :: String.t()
372 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
373 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
375 @spec ap_following(User.t()) :: String.t()
376 def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
377 def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
379 @spec ap_featured_collection(User.t()) :: String.t()
380 def ap_featured_collection(%User{featured_address: fa}) when is_binary(fa), do: fa
382 def ap_featured_collection(%User{} = user), do: "#{ap_id(user)}/collections/featured"
384 defp truncate_fields_param(params) do
385 if Map.has_key?(params, :fields) do
386 Map.put(params, :fields, Enum.map(params[:fields], &truncate_field/1))
392 defp truncate_if_exists(params, key, max_length) do
393 if Map.has_key?(params, key) and is_binary(params[key]) do
394 {value, _chopped} = String.split_at(params[key], max_length)
395 Map.put(params, key, value)
401 defp fix_follower_address(%{follower_address: _, following_address: _} = params), do: params
403 defp fix_follower_address(%{nickname: nickname} = params),
404 do: Map.put(params, :follower_address, ap_followers(%User{nickname: nickname}))
406 defp fix_follower_address(params), do: params
408 def remote_user_changeset(struct \\ %User{local: false}, params) do
409 bio_limit = Config.get([:instance, :user_bio_length], 5000)
410 name_limit = Config.get([:instance, :user_name_length], 100)
413 case params[:name] do
414 name when is_binary(name) and byte_size(name) > 0 -> name
415 _ -> params[:nickname]
420 |> Map.put(:name, name)
421 |> Map.put_new(:last_refreshed_at, NaiveDateTime.utc_now())
422 |> truncate_if_exists(:name, name_limit)
423 |> truncate_if_exists(:bio, bio_limit)
424 |> truncate_fields_param()
425 |> fix_follower_address()
449 :hide_followers_count,
458 :accepts_chat_messages,
462 |> cast(params, [:name], empty_values: [])
463 |> validate_required([:ap_id])
464 |> validate_required([:name], trim: false)
465 |> unique_constraint(:nickname)
466 |> validate_format(:nickname, @email_regex)
467 |> validate_length(:bio, max: bio_limit)
468 |> validate_length(:name, max: name_limit)
469 |> validate_fields(true)
470 |> validate_non_local()
473 defp validate_non_local(cng) do
474 local? = get_field(cng, :local)
478 |> add_error(:local, "User is local, can't update with this changeset.")
484 def update_changeset(struct, params \\ %{}) do
485 bio_limit = Config.get([:instance, :user_bio_length], 5000)
486 name_limit = Config.get([:instance, :user_name_length], 100)
506 :hide_followers_count,
509 :allow_following_move,
513 :skip_thread_containment,
516 :pleroma_settings_store,
519 :accepts_chat_messages,
523 |> unique_constraint(:nickname)
524 |> validate_format(:nickname, local_nickname_regex())
525 |> validate_length(:bio, max: bio_limit)
526 |> validate_length(:name, min: 1, max: name_limit)
527 |> validate_inclusion(:actor_type, ["Person", "Service"])
530 |> put_change_if_present(:bio, &{:ok, parse_bio(&1, struct)})
531 |> put_change_if_present(:avatar, &put_upload(&1, :avatar))
532 |> put_change_if_present(:banner, &put_upload(&1, :banner))
533 |> put_change_if_present(:background, &put_upload(&1, :background))
534 |> put_change_if_present(
535 :pleroma_settings_store,
536 &{:ok, Map.merge(struct.pleroma_settings_store, &1)}
538 |> validate_fields(false)
541 defp put_fields(changeset) do
542 if raw_fields = get_change(changeset, :raw_fields) do
545 |> Enum.filter(fn %{"name" => n} -> n != "" end)
549 |> Enum.map(fn f -> Map.update!(f, "value", &parse_fields(&1)) end)
552 |> put_change(:raw_fields, raw_fields)
553 |> put_change(:fields, fields)
559 defp parse_fields(value) do
561 |> Formatter.linkify(mentions_format: :full)
565 defp put_emoji(changeset) do
566 emojified_fields = [:bio, :name, :raw_fields]
568 if Enum.any?(changeset.changes, fn {k, _} -> k in emojified_fields end) do
569 bio = Emoji.Formatter.get_emoji_map(get_field(changeset, :bio))
570 name = Emoji.Formatter.get_emoji_map(get_field(changeset, :name))
572 emoji = Map.merge(bio, name)
576 |> get_field(:raw_fields)
577 |> Enum.reduce(emoji, fn x, acc ->
578 Map.merge(acc, Emoji.Formatter.get_emoji_map(x["name"] <> x["value"]))
581 put_change(changeset, :emoji, emoji)
587 defp put_change_if_present(changeset, map_field, value_function) do
588 with {:ok, value} <- fetch_change(changeset, map_field),
589 {:ok, new_value} <- value_function.(value) do
590 put_change(changeset, map_field, new_value)
596 defp put_upload(value, type) do
597 with %Plug.Upload{} <- value,
598 {:ok, object} <- ActivityPub.upload(value, type: type) do
603 def update_as_admin_changeset(struct, params) do
605 |> update_changeset(params)
606 |> cast(params, [:email])
607 |> delete_change(:also_known_as)
608 |> unique_constraint(:email)
609 |> validate_format(:email, @email_regex)
610 |> validate_inclusion(:actor_type, ["Person", "Service"])
613 @spec update_as_admin(User.t(), map()) :: {:ok, User.t()} | {:error, Changeset.t()}
614 def update_as_admin(user, params) do
615 params = Map.put(params, "password_confirmation", params["password"])
616 changeset = update_as_admin_changeset(user, params)
618 if params["password"] do
619 reset_password(user, changeset, params)
621 User.update_and_set_cache(changeset)
625 def password_update_changeset(struct, params) do
627 |> cast(params, [:password, :password_confirmation])
628 |> validate_required([:password, :password_confirmation])
629 |> validate_confirmation(:password)
630 |> put_password_hash()
631 |> put_change(:password_reset_pending, false)
634 @spec reset_password(User.t(), map()) :: {:ok, User.t()} | {:error, Changeset.t()}
635 def reset_password(%User{} = user, params) do
636 reset_password(user, user, params)
639 def reset_password(%User{id: user_id} = user, struct, params) do
642 |> Multi.update(:user, password_update_changeset(struct, params))
643 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
644 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
646 case Repo.transaction(multi) do
647 {:ok, %{user: user} = _} -> set_cache(user)
648 {:error, _, changeset, _} -> {:error, changeset}
652 def update_password_reset_pending(user, value) do
655 |> put_change(:password_reset_pending, value)
656 |> update_and_set_cache()
659 def force_password_reset_async(user) do
660 BackgroundWorker.enqueue("force_password_reset", %{"user_id" => user.id})
663 @spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
664 def force_password_reset(user), do: update_password_reset_pending(user, true)
666 # Used to auto-register LDAP accounts which won't have a password hash stored locally
667 def register_changeset_ldap(struct, params = %{password: password})
668 when is_nil(password) do
669 params = Map.put_new(params, :accepts_chat_messages, true)
672 if Map.has_key?(params, :email) do
673 Map.put_new(params, :email, params[:email])
683 :accepts_chat_messages
685 |> validate_required([:name, :nickname])
686 |> unique_constraint(:nickname)
687 |> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames]))
688 |> validate_format(:nickname, local_nickname_regex())
690 |> unique_constraint(:ap_id)
691 |> put_following_and_follower_and_featured_address()
694 def register_changeset(struct, params \\ %{}, opts \\ []) do
695 bio_limit = Config.get([:instance, :user_bio_length], 5000)
696 name_limit = Config.get([:instance, :user_name_length], 100)
697 reason_limit = Config.get([:instance, :registration_reason_length], 500)
698 params = Map.put_new(params, :accepts_chat_messages, true)
701 if is_nil(opts[:confirmed]) do
702 !Config.get([:instance, :account_activation_required])
708 if is_nil(opts[:approved]) do
709 !Config.get([:instance, :account_approval_required])
715 |> confirmation_changeset(set_confirmation: confirmed?)
716 |> approval_changeset(set_approval: approved?)
724 :password_confirmation,
726 :accepts_chat_messages,
727 :registration_reason,
730 |> validate_required([:name, :nickname, :password, :password_confirmation])
731 |> validate_confirmation(:password)
732 |> unique_constraint(:email)
733 |> validate_format(:email, @email_regex)
734 |> validate_change(:email, fn :email, email ->
736 Config.get([User, :email_blacklist])
737 |> Enum.all?(fn blacklisted_domain ->
738 !String.ends_with?(email, ["@" <> blacklisted_domain, "." <> blacklisted_domain])
741 if valid?, do: [], else: [email: "Invalid email"]
743 |> unique_constraint(:nickname)
744 |> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames]))
745 |> validate_format(:nickname, local_nickname_regex())
746 |> validate_length(:bio, max: bio_limit)
747 |> validate_length(:name, min: 1, max: name_limit)
748 |> validate_length(:registration_reason, max: reason_limit)
749 |> maybe_validate_required_email(opts[:external])
752 |> unique_constraint(:ap_id)
753 |> put_following_and_follower_and_featured_address()
756 def maybe_validate_required_email(changeset, true), do: changeset
758 def maybe_validate_required_email(changeset, _) do
759 if Config.get([:instance, :account_activation_required]) do
760 validate_required(changeset, [:email])
766 defp put_ap_id(changeset) do
767 ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
768 put_change(changeset, :ap_id, ap_id)
771 defp put_following_and_follower_and_featured_address(changeset) do
772 user = %User{nickname: get_field(changeset, :nickname)}
773 followers = ap_followers(user)
774 following = ap_following(user)
775 featured = ap_featured_collection(user)
778 |> put_change(:follower_address, followers)
779 |> put_change(:following_address, following)
780 |> put_change(:featured_address, featured)
783 defp autofollow_users(user) do
784 candidates = Config.get([:instance, :autofollowed_nicknames])
787 User.Query.build(%{nickname: candidates, local: true, is_active: true})
790 follow_all(user, autofollowed_users)
793 defp autofollowing_users(user) do
794 candidates = Config.get([:instance, :autofollowing_nicknames])
796 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
798 |> Enum.each(&follow(&1, user, :follow_accept))
803 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
804 def register(%Ecto.Changeset{} = changeset) do
805 with {:ok, user} <- Repo.insert(changeset) do
806 post_register_action(user)
810 def post_register_action(%User{is_confirmed: false} = user) do
811 with {:ok, _} <- maybe_send_confirmation_email(user) do
816 def post_register_action(%User{is_approved: false} = user) do
817 with {:ok, _} <- send_user_approval_email(user),
818 {:ok, _} <- send_admin_approval_emails(user) do
823 def post_register_action(%User{is_approved: true, is_confirmed: true} = user) do
824 with {:ok, user} <- autofollow_users(user),
825 {:ok, _} <- autofollowing_users(user),
826 {:ok, user} <- set_cache(user),
827 {:ok, _} <- maybe_send_registration_email(user),
828 {:ok, _} <- maybe_send_welcome_email(user),
829 {:ok, _} <- maybe_send_welcome_message(user),
830 {:ok, _} <- maybe_send_welcome_chat_message(user) do
835 defp send_user_approval_email(user) do
837 |> Pleroma.Emails.UserEmail.approval_pending_email()
838 |> Pleroma.Emails.Mailer.deliver_async()
843 defp send_admin_approval_emails(user) do
845 |> Enum.filter(fn user -> not is_nil(user.email) end)
846 |> Enum.each(fn superuser ->
848 |> Pleroma.Emails.AdminEmail.new_unapproved_registration(user)
849 |> Pleroma.Emails.Mailer.deliver_async()
855 defp maybe_send_welcome_message(user) do
856 if User.WelcomeMessage.enabled?() do
857 User.WelcomeMessage.post_message(user)
864 defp maybe_send_welcome_chat_message(user) do
865 if User.WelcomeChatMessage.enabled?() do
866 User.WelcomeChatMessage.post_message(user)
873 defp maybe_send_welcome_email(%User{email: email} = user) when is_binary(email) do
874 if User.WelcomeEmail.enabled?() do
875 User.WelcomeEmail.send_email(user)
882 defp maybe_send_welcome_email(_), do: {:ok, :noop}
884 @spec maybe_send_confirmation_email(User.t()) :: {:ok, :enqueued | :noop}
885 def maybe_send_confirmation_email(%User{is_confirmed: false, email: email} = user)
886 when is_binary(email) do
887 if Config.get([:instance, :account_activation_required]) do
888 send_confirmation_email(user)
895 def maybe_send_confirmation_email(_), do: {:ok, :noop}
897 @spec send_confirmation_email(Uset.t()) :: User.t()
898 def send_confirmation_email(%User{} = user) do
900 |> Pleroma.Emails.UserEmail.account_confirmation_email()
901 |> Pleroma.Emails.Mailer.deliver_async()
906 @spec maybe_send_registration_email(User.t()) :: {:ok, :enqueued | :noop}
907 defp maybe_send_registration_email(%User{email: email} = user) when is_binary(email) do
908 with false <- User.WelcomeEmail.enabled?(),
909 false <- Config.get([:instance, :account_activation_required], false),
910 false <- Config.get([:instance, :account_approval_required], false) do
912 |> Pleroma.Emails.UserEmail.successful_registration_email()
913 |> Pleroma.Emails.Mailer.deliver_async()
922 defp maybe_send_registration_email(_), do: {:ok, :noop}
924 def needs_update?(%User{local: true}), do: false
926 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
928 def needs_update?(%User{local: false} = user) do
929 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
932 def needs_update?(_), do: true
934 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
936 # "Locked" (self-locked) users demand explicit authorization of follow requests
937 def maybe_direct_follow(%User{} = follower, %User{local: true, is_locked: true} = followed) do
938 follow(follower, followed, :follow_pending)
941 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
942 follow(follower, followed)
945 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
946 if not ap_enabled?(followed) do
947 follow(follower, followed)
949 {:ok, follower, followed}
953 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
954 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
955 def follow_all(follower, followeds) do
957 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
958 |> Enum.each(&follow(follower, &1, :follow_accept))
963 def follow(%User{} = follower, %User{} = followed, state \\ :follow_accept) do
964 deny_follow_blocked = Config.get([:user, :deny_follow_blocked])
967 not followed.is_active ->
968 {:error, "Could not follow user: #{followed.nickname} is deactivated."}
970 deny_follow_blocked and blocks?(followed, follower) ->
971 {:error, "Could not follow user: #{followed.nickname} blocked you."}
974 FollowingRelationship.follow(follower, followed, state)
978 def unfollow(%User{ap_id: ap_id}, %User{ap_id: ap_id}) do
979 {:error, "Not subscribed!"}
982 @spec unfollow(User.t(), User.t()) :: {:ok, User.t(), Activity.t()} | {:error, String.t()}
983 def unfollow(%User{} = follower, %User{} = followed) do
984 case do_unfollow(follower, followed) do
985 {:ok, follower, followed} ->
986 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
993 @spec do_unfollow(User.t(), User.t()) :: {:ok, User.t(), User.t()} | {:error, String.t()}
994 defp do_unfollow(%User{} = follower, %User{} = followed) do
995 case get_follow_state(follower, followed) do
996 state when state in [:follow_pending, :follow_accept] ->
997 FollowingRelationship.unfollow(follower, followed)
1000 {:error, "Not subscribed!"}
1004 @doc "Returns follow state as Pleroma.FollowingRelationship.State value"
1005 def get_follow_state(%User{} = follower, %User{} = following) do
1006 following_relationship = FollowingRelationship.get(follower, following)
1007 get_follow_state(follower, following, following_relationship)
1010 def get_follow_state(
1012 %User{} = following,
1013 following_relationship
1015 case {following_relationship, following.local} do
1017 case Utils.fetch_latest_follow(follower, following) do
1018 %Activity{data: %{"state" => state}} when state in ["pending", "accept"] ->
1019 FollowingRelationship.state_to_enum(state)
1025 {%{state: state}, _} ->
1033 def locked?(%User{} = user) do
1034 user.is_locked || false
1037 def get_by_id(id) do
1038 Repo.get_by(User, id: id)
1041 def get_by_ap_id(ap_id) do
1042 Repo.get_by(User, ap_id: ap_id)
1045 def get_all_by_ap_id(ap_ids) do
1046 from(u in __MODULE__,
1047 where: u.ap_id in ^ap_ids
1052 def get_all_by_ids(ids) do
1053 from(u in __MODULE__, where: u.id in ^ids)
1057 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
1058 # of the ap_id and the domain and tries to get that user
1059 def get_by_guessed_nickname(ap_id) do
1060 domain = URI.parse(ap_id).host
1061 name = List.last(String.split(ap_id, "/"))
1062 nickname = "#{name}@#{domain}"
1064 get_cached_by_nickname(nickname)
1067 def set_cache({:ok, user}), do: set_cache(user)
1068 def set_cache({:error, err}), do: {:error, err}
1070 def set_cache(%User{} = user) do
1071 @cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
1072 @cachex.put(:user_cache, "nickname:#{user.nickname}", user)
1073 @cachex.put(:user_cache, "friends_ap_ids:#{user.nickname}", get_user_friends_ap_ids(user))
1077 def update_and_set_cache(struct, params) do
1079 |> update_changeset(params)
1080 |> update_and_set_cache()
1083 def update_and_set_cache(%{data: %Pleroma.User{} = user} = changeset) do
1084 was_superuser_before_update = User.superuser?(user)
1086 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
1089 |> maybe_remove_report_notifications(was_superuser_before_update)
1092 defp maybe_remove_report_notifications({:ok, %Pleroma.User{} = user} = result, true) do
1093 if not User.superuser?(user),
1094 do: user |> Notification.destroy_multiple_from_types(["pleroma:report"])
1099 defp maybe_remove_report_notifications(result, _) do
1103 def get_user_friends_ap_ids(user) do
1104 from(u in User.get_friends_query(user), select: u.ap_id)
1108 @spec get_cached_user_friends_ap_ids(User.t()) :: [String.t()]
1109 def get_cached_user_friends_ap_ids(user) do
1110 @cachex.fetch!(:user_cache, "friends_ap_ids:#{user.ap_id}", fn _ ->
1111 get_user_friends_ap_ids(user)
1115 def invalidate_cache(user) do
1116 @cachex.del(:user_cache, "ap_id:#{user.ap_id}")
1117 @cachex.del(:user_cache, "nickname:#{user.nickname}")
1118 @cachex.del(:user_cache, "friends_ap_ids:#{user.ap_id}")
1119 @cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
1120 @cachex.del(:user_cache, "muted_users_ap_ids:#{user.ap_id}")
1123 @spec get_cached_by_ap_id(String.t()) :: User.t() | nil
1124 def get_cached_by_ap_id(ap_id) do
1125 key = "ap_id:#{ap_id}"
1127 with {:ok, nil} <- @cachex.get(:user_cache, key),
1128 user when not is_nil(user) <- get_by_ap_id(ap_id),
1129 {:ok, true} <- @cachex.put(:user_cache, key, user) do
1137 def get_cached_by_id(id) do
1141 @cachex.fetch!(:user_cache, key, fn _ ->
1142 user = get_by_id(id)
1145 @cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
1146 {:commit, user.ap_id}
1152 get_cached_by_ap_id(ap_id)
1155 def get_cached_by_nickname(nickname) do
1156 key = "nickname:#{nickname}"
1158 @cachex.fetch!(:user_cache, key, fn _ ->
1159 case get_or_fetch_by_nickname(nickname) do
1160 {:ok, user} -> {:commit, user}
1161 {:error, _error} -> {:ignore, nil}
1166 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
1167 restrict_to_local = Config.get([:instance, :limit_to_local_content])
1170 is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
1171 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
1173 restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
1174 get_cached_by_nickname(nickname_or_id)
1176 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
1177 get_cached_by_nickname(nickname_or_id)
1184 @spec get_by_nickname(String.t()) :: User.t() | nil
1185 def get_by_nickname(nickname) do
1186 Repo.get_by(User, nickname: nickname) ||
1187 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
1188 Repo.get_by(User, nickname: local_nickname(nickname))
1192 def get_by_email(email), do: Repo.get_by(User, email: email)
1194 def get_by_nickname_or_email(nickname_or_email) do
1195 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
1198 def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname)
1200 def get_or_fetch_by_nickname(nickname) do
1201 with %User{} = user <- get_by_nickname(nickname) do
1205 with [_nick, _domain] <- String.split(nickname, "@"),
1206 {:ok, user} <- fetch_by_nickname(nickname) do
1209 _e -> {:error, "not found " <> nickname}
1214 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
1215 def get_followers_query(%User{} = user, nil) do
1216 User.Query.build(%{followers: user, is_active: true})
1219 def get_followers_query(%User{} = user, page) do
1221 |> get_followers_query(nil)
1222 |> User.Query.paginate(page, 20)
1225 @spec get_followers_query(User.t()) :: Ecto.Query.t()
1226 def get_followers_query(%User{} = user), do: get_followers_query(user, nil)
1228 @spec get_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
1229 def get_followers(%User{} = user, page \\ nil) do
1231 |> get_followers_query(page)
1235 @spec get_external_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
1236 def get_external_followers(%User{} = user, page \\ nil) do
1238 |> get_followers_query(page)
1239 |> User.Query.build(%{external: true})
1243 def get_followers_ids(%User{} = user, page \\ nil) do
1245 |> get_followers_query(page)
1246 |> select([u], u.id)
1250 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
1251 def get_friends_query(%User{} = user, nil) do
1252 User.Query.build(%{friends: user, deactivated: false})
1255 def get_friends_query(%User{} = user, page) do
1257 |> get_friends_query(nil)
1258 |> User.Query.paginate(page, 20)
1261 @spec get_friends_query(User.t()) :: Ecto.Query.t()
1262 def get_friends_query(%User{} = user), do: get_friends_query(user, nil)
1264 def get_friends(%User{} = user, page \\ nil) do
1266 |> get_friends_query(page)
1270 def get_friends_ap_ids(%User{} = user) do
1272 |> get_friends_query(nil)
1273 |> select([u], u.ap_id)
1277 def get_friends_ids(%User{} = user, page \\ nil) do
1279 |> get_friends_query(page)
1280 |> select([u], u.id)
1284 def increase_note_count(%User{} = user) do
1286 |> where(id: ^user.id)
1287 |> update([u], inc: [note_count: 1])
1289 |> Repo.update_all([])
1291 {1, [user]} -> set_cache(user)
1296 def decrease_note_count(%User{} = user) do
1298 |> where(id: ^user.id)
1301 note_count: fragment("greatest(0, note_count - 1)")
1305 |> Repo.update_all([])
1307 {1, [user]} -> set_cache(user)
1312 def update_note_count(%User{} = user, note_count \\ nil) do
1317 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
1323 |> cast(%{note_count: note_count}, [:note_count])
1324 |> update_and_set_cache()
1327 @spec maybe_fetch_follow_information(User.t()) :: User.t()
1328 def maybe_fetch_follow_information(user) do
1329 with {:ok, user} <- fetch_follow_information(user) do
1333 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
1339 def fetch_follow_information(user) do
1340 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
1342 |> follow_information_changeset(info)
1343 |> update_and_set_cache()
1347 defp follow_information_changeset(user, params) do
1354 :hide_followers_count,
1359 @spec update_follower_count(User.t()) :: {:ok, User.t()}
1360 def update_follower_count(%User{} = user) do
1361 if user.local or !Config.get([:instance, :external_user_synchronization]) do
1362 follower_count = FollowingRelationship.follower_count(user)
1365 |> follow_information_changeset(%{follower_count: follower_count})
1366 |> update_and_set_cache
1368 {:ok, maybe_fetch_follow_information(user)}
1372 @spec update_following_count(User.t()) :: {:ok, User.t()}
1373 def update_following_count(%User{local: false} = user) do
1374 if Config.get([:instance, :external_user_synchronization]) do
1375 {:ok, maybe_fetch_follow_information(user)}
1381 def update_following_count(%User{local: true} = user) do
1382 following_count = FollowingRelationship.following_count(user)
1385 |> follow_information_changeset(%{following_count: following_count})
1386 |> update_and_set_cache()
1389 @spec get_users_from_set([String.t()], keyword()) :: [User.t()]
1390 def get_users_from_set(ap_ids, opts \\ []) do
1391 local_only = Keyword.get(opts, :local_only, true)
1392 criteria = %{ap_id: ap_ids, is_active: true}
1393 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
1395 User.Query.build(criteria)
1399 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
1400 def get_recipients_from_activity(%Activity{recipients: to, actor: actor}) do
1403 query = User.Query.build(%{recipients_from_activity: to, local: true, is_active: true})
1409 @spec mute(User.t(), User.t(), map()) ::
1410 {:ok, list(UserRelationship.t())} | {:error, String.t()}
1411 def mute(%User{} = muter, %User{} = mutee, params \\ %{}) do
1412 notifications? = Map.get(params, :notifications, true)
1413 expires_in = Map.get(params, :expires_in, 0)
1415 with {:ok, user_mute} <- UserRelationship.create_mute(muter, mutee),
1416 {:ok, user_notification_mute} <-
1417 (notifications? && UserRelationship.create_notification_mute(muter, mutee)) ||
1419 if expires_in > 0 do
1420 Pleroma.Workers.MuteExpireWorker.enqueue(
1422 %{"muter_id" => muter.id, "mutee_id" => mutee.id},
1423 schedule_in: expires_in
1427 @cachex.del(:user_cache, "muted_users_ap_ids:#{muter.ap_id}")
1429 {:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
1433 def unmute(%User{} = muter, %User{} = mutee) do
1434 with {:ok, user_mute} <- UserRelationship.delete_mute(muter, mutee),
1435 {:ok, user_notification_mute} <-
1436 UserRelationship.delete_notification_mute(muter, mutee) do
1437 @cachex.del(:user_cache, "muted_users_ap_ids:#{muter.ap_id}")
1438 {:ok, [user_mute, user_notification_mute]}
1442 def unmute(muter_id, mutee_id) do
1443 with {:muter, %User{} = muter} <- {:muter, User.get_by_id(muter_id)},
1444 {:mutee, %User{} = mutee} <- {:mutee, User.get_by_id(mutee_id)} do
1445 unmute(muter, mutee)
1447 {who, result} = error ->
1449 "User.unmute/2 failed. #{who}: #{result}, muter_id: #{muter_id}, mutee_id: #{mutee_id}"
1456 def subscribe(%User{} = subscriber, %User{} = target) do
1457 deny_follow_blocked = Config.get([:user, :deny_follow_blocked])
1459 if blocks?(target, subscriber) and deny_follow_blocked do
1460 {:error, "Could not subscribe: #{target.nickname} is blocking you"}
1462 # Note: the relationship is inverse: subscriber acts as relationship target
1463 UserRelationship.create_inverse_subscription(target, subscriber)
1467 def subscribe(%User{} = subscriber, %{ap_id: ap_id}) do
1468 with %User{} = subscribee <- get_cached_by_ap_id(ap_id) do
1469 subscribe(subscriber, subscribee)
1473 def unsubscribe(%User{} = unsubscriber, %User{} = target) do
1474 # Note: the relationship is inverse: subscriber acts as relationship target
1475 UserRelationship.delete_inverse_subscription(target, unsubscriber)
1478 def unsubscribe(%User{} = unsubscriber, %{ap_id: ap_id}) do
1479 with %User{} = user <- get_cached_by_ap_id(ap_id) do
1480 unsubscribe(unsubscriber, user)
1484 def block(%User{} = blocker, %User{} = blocked) do
1485 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
1487 if following?(blocker, blocked) do
1488 {:ok, blocker, _} = unfollow(blocker, blocked)
1494 # clear any requested follows as well
1496 case CommonAPI.reject_follow_request(blocked, blocker) do
1497 {:ok, %User{} = updated_blocked} -> updated_blocked
1501 unsubscribe(blocked, blocker)
1503 unfollowing_blocked = Config.get([:activitypub, :unfollow_blocked], true)
1504 if unfollowing_blocked && following?(blocked, blocker), do: unfollow(blocked, blocker)
1506 {:ok, blocker} = update_follower_count(blocker)
1507 {:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
1508 add_to_block(blocker, blocked)
1511 # helper to handle the block given only an actor's AP id
1512 def block(%User{} = blocker, %{ap_id: ap_id}) do
1513 block(blocker, get_cached_by_ap_id(ap_id))
1516 def unblock(%User{} = blocker, %User{} = blocked) do
1517 remove_from_block(blocker, blocked)
1520 # helper to handle the block given only an actor's AP id
1521 def unblock(%User{} = blocker, %{ap_id: ap_id}) do
1522 unblock(blocker, get_cached_by_ap_id(ap_id))
1525 def mutes?(nil, _), do: false
1526 def mutes?(%User{} = user, %User{} = target), do: mutes_user?(user, target)
1528 def mutes_user?(%User{} = user, %User{} = target) do
1529 UserRelationship.mute_exists?(user, target)
1532 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
1533 def muted_notifications?(nil, _), do: false
1535 def muted_notifications?(%User{} = user, %User{} = target),
1536 do: UserRelationship.notification_mute_exists?(user, target)
1538 def blocks?(nil, _), do: false
1540 def blocks?(%User{} = user, %User{} = target) do
1541 blocks_user?(user, target) ||
1542 (blocks_domain?(user, target) and not User.following?(user, target))
1545 def blocks_user?(%User{} = user, %User{} = target) do
1546 UserRelationship.block_exists?(user, target)
1549 def blocks_user?(_, _), do: false
1551 def blocks_domain?(%User{} = user, %User{} = target) do
1552 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
1553 %{host: host} = URI.parse(target.ap_id)
1554 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1557 def blocks_domain?(_, _), do: false
1559 def subscribed_to?(%User{} = user, %User{} = target) do
1560 # Note: the relationship is inverse: subscriber acts as relationship target
1561 UserRelationship.inverse_subscription_exists?(target, user)
1564 def subscribed_to?(%User{} = user, %{ap_id: ap_id}) do
1565 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1566 subscribed_to?(user, target)
1571 Returns map of outgoing (blocked, muted etc.) relationships' user AP IDs by relation type.
1572 E.g. `outgoing_relationships_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}`
1574 @spec outgoing_relationships_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())}
1575 def outgoing_relationships_ap_ids(_user, []), do: %{}
1577 def outgoing_relationships_ap_ids(nil, _relationship_types), do: %{}
1579 def outgoing_relationships_ap_ids(%User{} = user, relationship_types)
1580 when is_list(relationship_types) do
1583 |> assoc(:outgoing_relationships)
1584 |> join(:inner, [user_rel], u in assoc(user_rel, :target))
1585 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1586 |> select([user_rel, u], [user_rel.relationship_type, fragment("array_agg(?)", u.ap_id)])
1587 |> group_by([user_rel, u], user_rel.relationship_type)
1589 |> Enum.into(%{}, fn [k, v] -> {k, v} end)
1594 fn rel_type -> {rel_type, db_result[rel_type] || []} end
1598 def incoming_relationships_ungrouped_ap_ids(user, relationship_types, ap_ids \\ nil)
1600 def incoming_relationships_ungrouped_ap_ids(_user, [], _ap_ids), do: []
1602 def incoming_relationships_ungrouped_ap_ids(nil, _relationship_types, _ap_ids), do: []
1604 def incoming_relationships_ungrouped_ap_ids(%User{} = user, relationship_types, ap_ids)
1605 when is_list(relationship_types) do
1607 |> assoc(:incoming_relationships)
1608 |> join(:inner, [user_rel], u in assoc(user_rel, :source))
1609 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1610 |> maybe_filter_on_ap_id(ap_ids)
1611 |> select([user_rel, u], u.ap_id)
1616 defp maybe_filter_on_ap_id(query, ap_ids) when is_list(ap_ids) do
1617 where(query, [user_rel, u], u.ap_id in ^ap_ids)
1620 defp maybe_filter_on_ap_id(query, _ap_ids), do: query
1622 def set_activation_async(user, status \\ true) do
1623 BackgroundWorker.enqueue("user_activation", %{"user_id" => user.id, "status" => status})
1626 @spec set_activation([User.t()], boolean()) :: {:ok, User.t()} | {:error, Changeset.t()}
1627 def set_activation(users, status) when is_list(users) do
1628 Repo.transaction(fn ->
1629 for user <- users, do: set_activation(user, status)
1633 @spec set_activation(User.t(), boolean()) :: {:ok, User.t()} | {:error, Changeset.t()}
1634 def set_activation(%User{} = user, status) do
1635 with {:ok, user} <- set_activation_status(user, status) do
1638 |> Enum.filter(& &1.local)
1639 |> Enum.each(&set_cache(update_following_count(&1)))
1641 # Only update local user counts, remote will be update during the next pull.
1644 |> Enum.filter(& &1.local)
1645 |> Enum.each(&do_unfollow(user, &1))
1651 def approve(users) when is_list(users) do
1652 Repo.transaction(fn ->
1653 Enum.map(users, fn user ->
1654 with {:ok, user} <- approve(user), do: user
1659 def approve(%User{is_approved: false} = user) do
1660 with chg <- change(user, is_approved: true),
1661 {:ok, user} <- update_and_set_cache(chg) do
1662 post_register_action(user)
1667 def approve(%User{} = user), do: {:ok, user}
1669 def confirm(users) when is_list(users) do
1670 Repo.transaction(fn ->
1671 Enum.map(users, fn user ->
1672 with {:ok, user} <- confirm(user), do: user
1677 def confirm(%User{is_confirmed: false} = user) do
1678 with chg <- confirmation_changeset(user, set_confirmation: true),
1679 {:ok, user} <- update_and_set_cache(chg) do
1680 post_register_action(user)
1685 def confirm(%User{} = user), do: {:ok, user}
1687 def set_suggestion(users, is_suggested) when is_list(users) do
1688 Repo.transaction(fn ->
1689 Enum.map(users, fn user ->
1690 with {:ok, user} <- set_suggestion(user, is_suggested), do: user
1695 def set_suggestion(%User{is_suggested: is_suggested} = user, is_suggested), do: {:ok, user}
1697 def set_suggestion(%User{} = user, is_suggested) when is_boolean(is_suggested) do
1699 |> change(is_suggested: is_suggested)
1700 |> update_and_set_cache()
1703 def update_notification_settings(%User{} = user, settings) do
1705 |> cast(%{notification_settings: settings}, [])
1706 |> cast_embed(:notification_settings)
1707 |> validate_required([:notification_settings])
1708 |> update_and_set_cache()
1711 @spec purge_user_changeset(User.t()) :: Changeset.t()
1712 def purge_user_changeset(user) do
1713 # "Right to be forgotten"
1714 # https://gdpr.eu/right-to-be-forgotten/
1723 last_refreshed_at: nil,
1724 last_digest_emailed_at: nil,
1731 password_reset_pending: false,
1732 registration_reason: nil,
1733 confirmation_token: nil,
1737 is_moderator: false,
1739 mastofe_settings: nil,
1742 pleroma_settings_store: %{},
1745 is_discoverable: false,
1749 # nickname: preserved
1753 # Purge doesn't delete the user from the database.
1754 # It just nulls all its fields and deactivates it.
1755 # See `User.purge_user_changeset/1` above.
1756 defp purge(%User{} = user) do
1758 |> purge_user_changeset()
1759 |> update_and_set_cache()
1762 def delete(users) when is_list(users) do
1763 for user <- users, do: delete(user)
1766 def delete(%User{} = user) do
1767 # Purge the user immediately
1769 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1772 # *Actually* delete the user from the DB
1773 defp delete_from_db(%User{} = user) do
1774 invalidate_cache(user)
1778 # If the user never finalized their account, it's safe to delete them.
1779 defp maybe_delete_from_db(%User{local: true, is_confirmed: false} = user),
1780 do: delete_from_db(user)
1782 defp maybe_delete_from_db(%User{local: true, is_approved: false} = user),
1783 do: delete_from_db(user)
1785 defp maybe_delete_from_db(user), do: {:ok, user}
1787 def perform(:force_password_reset, user), do: force_password_reset(user)
1789 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1790 def perform(:delete, %User{} = user) do
1791 # Purge the user again, in case perform/2 is called directly
1794 # Remove all relationships
1797 |> Enum.each(fn follower ->
1798 ActivityPub.unfollow(follower, user)
1799 unfollow(follower, user)
1804 |> Enum.each(fn followed ->
1805 ActivityPub.unfollow(user, followed)
1806 unfollow(user, followed)
1809 delete_user_activities(user)
1810 delete_notifications_from_user_activities(user)
1811 delete_outgoing_pending_follow_requests(user)
1813 maybe_delete_from_db(user)
1816 def perform(:set_activation_async, user, status), do: set_activation(user, status)
1818 @spec external_users_query() :: Ecto.Query.t()
1819 def external_users_query do
1827 @spec external_users(keyword()) :: [User.t()]
1828 def external_users(opts \\ []) do
1830 external_users_query()
1831 |> select([u], struct(u, [:id, :ap_id]))
1835 do: where(query, [u], u.id > ^opts[:max_id]),
1840 do: limit(query, ^opts[:limit]),
1846 def delete_notifications_from_user_activities(%User{ap_id: ap_id}) do
1848 |> join(:inner, [n], activity in assoc(n, :activity))
1849 |> where([n, a], fragment("? = ?", a.actor, ^ap_id))
1850 |> Repo.delete_all()
1853 def delete_user_activities(%User{ap_id: ap_id} = user) do
1855 |> Activity.Queries.by_actor()
1856 |> Repo.chunk_stream(50, :batches)
1857 |> Stream.each(fn activities ->
1858 Enum.each(activities, fn activity -> delete_activity(activity, user) end)
1863 defp delete_activity(%{data: %{"type" => "Create", "object" => object}} = activity, user) do
1864 with {_, %Object{}} <- {:find_object, Object.get_by_ap_id(object)},
1865 {:ok, delete_data, _} <- Builder.delete(user, object) do
1866 Pipeline.common_pipeline(delete_data, local: user.local)
1868 {:find_object, nil} ->
1869 # We have the create activity, but not the object, it was probably pruned.
1870 # Insert a tombstone and try again
1871 with {:ok, tombstone_data, _} <- Builder.tombstone(user.ap_id, object),
1872 {:ok, _tombstone} <- Object.create(tombstone_data) do
1873 delete_activity(activity, user)
1877 Logger.error("Could not delete #{object} created by #{activity.data["ap_id"]}")
1878 Logger.error("Error: #{inspect(e)}")
1882 defp delete_activity(%{data: %{"type" => type}} = activity, user)
1883 when type in ["Like", "Announce"] do
1884 {:ok, undo, _} = Builder.undo(user, activity)
1885 Pipeline.common_pipeline(undo, local: user.local)
1888 defp delete_activity(_activity, _user), do: "Doing nothing"
1890 defp delete_outgoing_pending_follow_requests(user) do
1892 |> FollowingRelationship.outgoing_pending_follow_requests_query()
1893 |> Repo.delete_all()
1896 def html_filter_policy(%User{no_rich_text: true}) do
1897 Pleroma.HTML.Scrubber.TwitterText
1900 def html_filter_policy(_), do: Config.get([:markup, :scrub_policy])
1902 def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
1904 def get_or_fetch_by_ap_id(ap_id) do
1905 cached_user = get_cached_by_ap_id(ap_id)
1907 maybe_fetched_user = needs_update?(cached_user) && fetch_by_ap_id(ap_id)
1909 case {cached_user, maybe_fetched_user} do
1910 {_, {:ok, %User{} = user}} ->
1913 {%User{} = user, _} ->
1917 {:error, :not_found}
1922 Creates an internal service actor by URI if missing.
1923 Optionally takes nickname for addressing.
1925 @spec get_or_create_service_actor_by_ap_id(String.t(), String.t()) :: User.t() | nil
1926 def get_or_create_service_actor_by_ap_id(uri, nickname) do
1928 case get_cached_by_ap_id(uri) do
1930 with {:error, %{errors: errors}} <- create_service_actor(uri, nickname) do
1931 Logger.error("Cannot create service actor: #{uri}/.\n#{inspect(errors)}")
1935 %User{invisible: false} = user ->
1945 @spec set_invisible(User.t()) :: {:ok, User.t()}
1946 defp set_invisible(user) do
1948 |> change(%{invisible: true})
1949 |> update_and_set_cache()
1952 @spec create_service_actor(String.t(), String.t()) ::
1953 {:ok, User.t()} | {:error, Ecto.Changeset.t()}
1954 defp create_service_actor(uri, nickname) do
1960 follower_address: uri <> "/followers"
1963 |> unique_constraint(:nickname)
1968 def public_key(%{public_key: public_key_pem}) when is_binary(public_key_pem) do
1971 |> :public_key.pem_decode()
1973 |> :public_key.pem_entry_decode()
1978 def public_key(_), do: {:error, "key not found"}
1980 def get_public_key_for_ap_id(ap_id) do
1981 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1982 {:ok, public_key} <- public_key(user) do
1989 def ap_enabled?(%User{local: true}), do: true
1990 def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled
1991 def ap_enabled?(_), do: false
1993 @doc "Gets or fetch a user by uri or nickname."
1994 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1995 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1996 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1998 # wait a period of time and return newest version of the User structs
1999 # this is because we have synchronous follow APIs and need to simulate them
2000 # with an async handshake
2001 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
2002 with %User{} = a <- get_cached_by_id(a.id),
2003 %User{} = b <- get_cached_by_id(b.id) do
2010 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
2011 with :ok <- :timer.sleep(timeout),
2012 %User{} = a <- get_cached_by_id(a.id),
2013 %User{} = b <- get_cached_by_id(b.id) do
2020 def parse_bio(bio) when is_binary(bio) and bio != "" do
2022 |> CommonUtils.format_input("text/plain", mentions_format: :full)
2026 def parse_bio(_), do: ""
2028 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
2029 # TODO: get profile URLs other than user.ap_id
2030 profile_urls = [user.ap_id]
2033 |> CommonUtils.format_input("text/plain",
2034 mentions_format: :full,
2035 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
2040 def parse_bio(_, _), do: ""
2042 def tag(user_identifiers, tags) when is_list(user_identifiers) do
2043 Repo.transaction(fn ->
2044 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
2048 def tag(nickname, tags) when is_binary(nickname),
2049 do: tag(get_by_nickname(nickname), tags)
2051 def tag(%User{} = user, tags),
2052 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
2054 def untag(user_identifiers, tags) when is_list(user_identifiers) do
2055 Repo.transaction(fn ->
2056 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
2060 def untag(nickname, tags) when is_binary(nickname),
2061 do: untag(get_by_nickname(nickname), tags)
2063 def untag(%User{} = user, tags),
2064 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
2066 defp update_tags(%User{} = user, new_tags) do
2067 {:ok, updated_user} =
2069 |> change(%{tags: new_tags})
2070 |> update_and_set_cache()
2075 defp normalize_tags(tags) do
2078 |> Enum.map(&String.downcase/1)
2081 defp local_nickname_regex do
2082 if Config.get([:instance, :extended_nickname_format]) do
2083 @extended_local_nickname_regex
2085 @strict_local_nickname_regex
2089 def local_nickname(nickname_or_mention) do
2092 |> String.split("@")
2096 def full_nickname(%User{} = user) do
2097 if String.contains?(user.nickname, "@") do
2100 %{host: host} = URI.parse(user.ap_id)
2101 user.nickname <> "@" <> host
2105 def full_nickname(nickname_or_mention),
2106 do: String.trim_leading(nickname_or_mention, "@")
2108 def error_user(ap_id) do
2112 nickname: "erroruser@example.com",
2113 inserted_at: NaiveDateTime.utc_now()
2117 @spec all_superusers() :: [User.t()]
2118 def all_superusers do
2119 User.Query.build(%{super_users: true, local: true, is_active: true})
2123 def muting_reblogs?(%User{} = user, %User{} = target) do
2124 UserRelationship.reblog_mute_exists?(user, target)
2127 def showing_reblogs?(%User{} = user, %User{} = target) do
2128 not muting_reblogs?(user, target)
2132 The function returns a query to get users with no activity for given interval of days.
2133 Inactive users are those who didn't read any notification, or had any activity where
2134 the user is the activity's actor, during `inactivity_threshold` days.
2135 Deactivated users will not appear in this list.
2139 iex> Pleroma.User.list_inactive_users()
2142 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
2143 def list_inactive_users_query(inactivity_threshold \\ 7) do
2144 negative_inactivity_threshold = -inactivity_threshold
2145 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
2146 # Subqueries are not supported in `where` clauses, join gets too complicated.
2147 has_read_notifications =
2148 from(n in Pleroma.Notification,
2149 where: n.seen == true,
2151 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
2154 |> Pleroma.Repo.all()
2156 from(u in Pleroma.User,
2157 left_join: a in Pleroma.Activity,
2158 on: u.ap_id == a.actor,
2159 where: not is_nil(u.nickname),
2160 where: u.is_active == ^true,
2161 where: u.id not in ^has_read_notifications,
2164 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
2165 is_nil(max(a.inserted_at))
2170 Enable or disable email notifications for user
2174 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => false}}, "digest", true)
2175 Pleroma.User{email_notifications: %{"digest" => true}}
2177 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => true}}, "digest", false)
2178 Pleroma.User{email_notifications: %{"digest" => false}}
2180 @spec switch_email_notifications(t(), String.t(), boolean()) ::
2181 {:ok, t()} | {:error, Ecto.Changeset.t()}
2182 def switch_email_notifications(user, type, status) do
2183 User.update_email_notifications(user, %{type => status})
2187 Set `last_digest_emailed_at` value for the user to current time
2189 @spec touch_last_digest_emailed_at(t()) :: t()
2190 def touch_last_digest_emailed_at(user) do
2191 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
2193 {:ok, updated_user} =
2195 |> change(%{last_digest_emailed_at: now})
2196 |> update_and_set_cache()
2201 @spec set_confirmation(User.t(), boolean()) :: {:ok, User.t()} | {:error, Changeset.t()}
2202 def set_confirmation(%User{} = user, bool) do
2204 |> confirmation_changeset(set_confirmation: bool)
2205 |> update_and_set_cache()
2208 def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
2212 def get_mascot(%{mascot: mascot}) when is_nil(mascot) do
2213 # use instance-default
2214 config = Config.get([:assets, :mascots])
2215 default_mascot = Config.get([:assets, :default_mascot])
2216 mascot = Keyword.get(config, default_mascot)
2219 "id" => "default-mascot",
2220 "url" => mascot[:url],
2221 "preview_url" => mascot[:url],
2223 "mime_type" => mascot[:mime_type]
2228 def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
2230 def ensure_keys_present(%User{} = user) do
2231 with {:ok, pem} <- Keys.generate_rsa_pem() do
2233 |> cast(%{keys: pem}, [:keys])
2234 |> validate_required([:keys])
2235 |> update_and_set_cache()
2239 def get_ap_ids_by_nicknames(nicknames) do
2241 where: u.nickname in ^nicknames,
2247 defp put_password_hash(
2248 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
2250 change(changeset, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt(password))
2253 defp put_password_hash(changeset), do: changeset
2255 def is_internal_user?(%User{nickname: nil}), do: true
2256 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
2257 def is_internal_user?(_), do: false
2259 # A hack because user delete activities have a fake id for whatever reason
2260 # TODO: Get rid of this
2261 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
2263 def get_delivered_users_by_object_id(object_id) do
2265 inner_join: delivery in assoc(u, :deliveries),
2266 where: delivery.object_id == ^object_id
2271 def change_email(user, email) do
2273 |> cast(%{email: email}, [:email])
2274 |> maybe_validate_required_email(false)
2275 |> unique_constraint(:email)
2276 |> validate_format(:email, @email_regex)
2277 |> update_and_set_cache()
2280 def alias_users(user) do
2282 |> Enum.map(&User.get_cached_by_ap_id/1)
2283 |> Enum.filter(fn user -> user != nil end)
2286 def add_alias(user, new_alias_user) do
2287 current_aliases = user.also_known_as || []
2288 new_alias_ap_id = new_alias_user.ap_id
2290 if new_alias_ap_id in current_aliases do
2294 |> cast(%{also_known_as: current_aliases ++ [new_alias_ap_id]}, [:also_known_as])
2295 |> update_and_set_cache()
2299 def delete_alias(user, alias_user) do
2300 current_aliases = user.also_known_as || []
2301 alias_ap_id = alias_user.ap_id
2303 if alias_ap_id in current_aliases do
2305 |> cast(%{also_known_as: current_aliases -- [alias_ap_id]}, [:also_known_as])
2306 |> update_and_set_cache()
2308 {:error, :no_such_alias}
2312 # Internal function; public one is `deactivate/2`
2313 defp set_activation_status(user, status) do
2315 |> cast(%{is_active: status}, [:is_active])
2316 |> update_and_set_cache()
2319 def update_banner(user, banner) do
2321 |> cast(%{banner: banner}, [:banner])
2322 |> update_and_set_cache()
2325 def update_background(user, background) do
2327 |> cast(%{background: background}, [:background])
2328 |> update_and_set_cache()
2331 def validate_fields(changeset, remote? \\ false) do
2332 limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
2333 limit = Config.get([:instance, limit_name], 0)
2336 |> validate_length(:fields, max: limit)
2337 |> validate_change(:fields, fn :fields, fields ->
2338 if Enum.all?(fields, &valid_field?/1) do
2346 defp valid_field?(%{"name" => name, "value" => value}) do
2347 name_limit = Config.get([:instance, :account_field_name_length], 255)
2348 value_limit = Config.get([:instance, :account_field_value_length], 255)
2350 is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
2351 String.length(value) <= value_limit
2354 defp valid_field?(_), do: false
2356 defp truncate_field(%{"name" => name, "value" => value}) do
2358 String.split_at(name, Config.get([:instance, :account_field_name_length], 255))
2361 String.split_at(value, Config.get([:instance, :account_field_value_length], 255))
2363 %{"name" => name, "value" => value}
2366 def admin_api_update(user, params) do
2373 |> update_and_set_cache()
2376 @doc "Signs user out of all applications"
2377 def global_sign_out(user) do
2378 OAuth.Authorization.delete_user_authorizations(user)
2379 OAuth.Token.delete_user_tokens(user)
2382 def mascot_update(user, url) do
2384 |> cast(%{mascot: url}, [:mascot])
2385 |> validate_required([:mascot])
2386 |> update_and_set_cache()
2389 def mastodon_settings_update(user, settings) do
2391 |> cast(%{mastofe_settings: settings}, [:mastofe_settings])
2392 |> validate_required([:mastofe_settings])
2393 |> update_and_set_cache()
2396 @spec confirmation_changeset(User.t(), keyword()) :: Changeset.t()
2397 def confirmation_changeset(user, set_confirmation: confirmed?) do
2402 confirmation_token: nil
2406 is_confirmed: false,
2407 confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
2411 cast(user, params, [:is_confirmed, :confirmation_token])
2414 @spec approval_changeset(User.t(), keyword()) :: Changeset.t()
2415 def approval_changeset(user, set_approval: approved?) do
2416 cast(user, %{is_approved: approved?}, [:is_approved])
2419 @spec add_pinned_object_id(User.t(), String.t()) :: {:ok, User.t()} | {:error, term()}
2420 def add_pinned_object_id(%User{} = user, object_id) do
2421 if !user.pinned_objects[object_id] do
2422 params = %{pinned_objects: Map.put(user.pinned_objects, object_id, NaiveDateTime.utc_now())}
2425 |> cast(params, [:pinned_objects])
2426 |> validate_change(:pinned_objects, fn :pinned_objects, pinned_objects ->
2427 max_pinned_statuses = Config.get([:instance, :max_pinned_statuses], 0)
2429 if Enum.count(pinned_objects) <= max_pinned_statuses do
2432 [pinned_objects: "You have already pinned the maximum number of statuses"]
2438 |> update_and_set_cache()
2441 @spec remove_pinned_object_id(User.t(), String.t()) :: {:ok, t()} | {:error, term()}
2442 def remove_pinned_object_id(%User{} = user, object_id) do
2445 %{pinned_objects: Map.delete(user.pinned_objects, object_id)},
2448 |> update_and_set_cache()
2451 def update_email_notifications(user, settings) do
2452 email_notifications =
2453 user.email_notifications
2454 |> Map.merge(settings)
2455 |> Map.take(["digest"])
2457 params = %{email_notifications: email_notifications}
2458 fields = [:email_notifications]
2461 |> cast(params, fields)
2462 |> validate_required(fields)
2463 |> update_and_set_cache()
2466 defp set_domain_blocks(user, domain_blocks) do
2467 params = %{domain_blocks: domain_blocks}
2470 |> cast(params, [:domain_blocks])
2471 |> validate_required([:domain_blocks])
2472 |> update_and_set_cache()
2475 def block_domain(user, domain_blocked) do
2476 set_domain_blocks(user, Enum.uniq([domain_blocked | user.domain_blocks]))
2479 def unblock_domain(user, domain_blocked) do
2480 set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked))
2483 @spec add_to_block(User.t(), User.t()) ::
2484 {:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()}
2485 defp add_to_block(%User{} = user, %User{} = blocked) do
2486 with {:ok, relationship} <- UserRelationship.create_block(user, blocked) do
2487 @cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
2492 @spec add_to_block(User.t(), User.t()) ::
2493 {:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()}
2494 defp remove_from_block(%User{} = user, %User{} = blocked) do
2495 with {:ok, relationship} <- UserRelationship.delete_block(user, blocked) do
2496 @cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
2501 def set_invisible(user, invisible) do
2502 params = %{invisible: invisible}
2505 |> cast(params, [:invisible])
2506 |> validate_required([:invisible])
2507 |> update_and_set_cache()
2510 def sanitize_html(%User{} = user) do
2511 sanitize_html(user, nil)
2514 # User data that mastodon isn't filtering (treated as plaintext):
2517 def sanitize_html(%User{} = user, filter) do
2519 Enum.map(user.fields, fn %{"name" => name, "value" => value} ->
2522 "value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
2527 |> Map.put(:bio, HTML.filter_tags(user.bio, filter))
2528 |> Map.put(:fields, fields)
2531 def get_host(%User{ap_id: ap_id} = _user) do
2532 URI.parse(ap_id).host
2535 def update_last_active_at(%__MODULE__{local: true} = user) do
2537 |> cast(%{last_active_at: NaiveDateTime.utc_now()}, [:last_active_at])
2538 |> update_and_set_cache()
2541 def active_user_count(days \\ 30) do
2542 active_after = Timex.shift(NaiveDateTime.utc_now(), days: -days)
2545 |> where([u], u.last_active_at >= ^active_after)
2546 |> where([u], u.local == true)
2547 |> Repo.aggregate(:count)
2550 def update_last_status_at(user) do
2552 |> where(id: ^user.id)
2553 |> update([u], set: [last_status_at: fragment("NOW()")])
2555 |> Repo.update_all([])
2557 {1, [user]} -> set_cache(user)