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
22 alias Pleroma.User.HashtagFollow
26 alias Pleroma.Notification
28 alias Pleroma.Registration
31 alias Pleroma.UserRelationship
32 alias Pleroma.Web.ActivityPub.ActivityPub
33 alias Pleroma.Web.ActivityPub.Builder
34 alias Pleroma.Web.ActivityPub.Pipeline
35 alias Pleroma.Web.ActivityPub.Utils
36 alias Pleroma.Web.CommonAPI
37 alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
38 alias Pleroma.Web.Endpoint
39 alias Pleroma.Web.OAuth
40 alias Pleroma.Web.RelMe
41 alias Pleroma.Workers.BackgroundWorker
45 @type t :: %__MODULE__{}
46 @type account_status ::
49 | :password_reset_pending
50 | :confirmation_pending
52 @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
54 # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
55 @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])?)*$/
57 @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
58 @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
60 # AP ID user relationships (blocks, mutes etc.)
61 # Format: [rel_type: [outgoing_rel: :outgoing_rel_target, incoming_rel: :incoming_rel_source]]
62 @user_relationships_config [
64 blocker_blocks: :blocked_users,
65 blockee_blocks: :blocker_users
68 muter_mutes: :muted_users,
69 mutee_mutes: :muter_users
72 reblog_muter_mutes: :reblog_muted_users,
73 reblog_mutee_mutes: :reblog_muter_users
76 notification_muter_mutes: :notification_muted_users,
77 notification_mutee_mutes: :notification_muter_users
79 # Note: `inverse_subscription` relationship is inverse: subscriber acts as relationship target
80 inverse_subscription: [
81 subscribee_subscriptions: :subscriber_users,
82 subscriber_subscriptions: :subscribee_users
86 @cachex Pleroma.Config.get([:cachex, :provider], Cachex)
89 field(:bio, :string, default: "")
90 field(:raw_bio, :string)
91 field(:email, :string)
93 field(:nickname, :string)
94 field(:password_hash, :string)
95 field(:password, :string, virtual: true)
96 field(:password_confirmation, :string, virtual: true)
98 field(:public_key, :string)
99 field(:ap_id, :string)
100 field(:avatar, :map, default: %{})
101 field(:local, :boolean, default: true)
102 field(:follower_address, :string)
103 field(:following_address, :string)
104 field(:featured_address, :string)
105 field(:search_rank, :float, virtual: true)
106 field(:search_type, :integer, virtual: true)
107 field(:tags, {:array, :string}, default: [])
108 field(:last_refreshed_at, :naive_datetime_usec)
109 field(:last_digest_emailed_at, :naive_datetime)
110 field(:banner, :map, default: %{})
111 field(:background, :map, default: %{})
112 field(:note_count, :integer, default: 0)
113 field(:follower_count, :integer, default: 0)
114 field(:following_count, :integer, default: 0)
115 field(:is_locked, :boolean, default: false)
116 field(:is_confirmed, :boolean, default: true)
117 field(:password_reset_pending, :boolean, default: false)
118 field(:is_approved, :boolean, default: true)
119 field(:registration_reason, :string, default: nil)
120 field(:confirmation_token, :string, default: nil)
121 field(:default_scope, :string, default: "public")
122 field(:domain_blocks, {:array, :string}, default: [])
123 field(:is_active, :boolean, default: true)
124 field(:no_rich_text, :boolean, default: false)
125 field(:ap_enabled, :boolean, default: false)
126 field(:is_moderator, :boolean, default: false)
127 field(:is_admin, :boolean, default: false)
128 field(:show_role, :boolean, default: true)
129 field(:mastofe_settings, :map, default: nil)
130 field(:uri, ObjectValidators.Uri, default: nil)
131 field(:hide_followers_count, :boolean, default: false)
132 field(:hide_follows_count, :boolean, default: false)
133 field(:hide_followers, :boolean, default: false)
134 field(:hide_follows, :boolean, default: false)
135 field(:hide_favorites, :boolean, default: true)
136 field(:email_notifications, :map, default: %{"digest" => false})
137 field(:mascot, :map, default: nil)
138 field(:emoji, :map, default: %{})
139 field(:pleroma_settings_store, :map, default: %{})
140 field(:fields, {:array, :map}, default: [])
141 field(:raw_fields, {:array, :map}, default: [])
142 field(:is_discoverable, :boolean, default: false)
143 field(:invisible, :boolean, default: false)
144 field(:allow_following_move, :boolean, default: true)
145 field(:skip_thread_containment, :boolean, default: false)
146 field(:actor_type, :string, default: "Person")
147 field(:also_known_as, {:array, ObjectValidators.ObjectID}, default: [])
148 field(:inbox, :string)
149 field(:shared_inbox, :string)
150 field(:last_active_at, :naive_datetime)
151 field(:disclose_client, :boolean, default: true)
152 field(:pinned_objects, :map, default: %{})
153 field(:is_suggested, :boolean, default: false)
154 field(:last_status_at, :naive_datetime)
155 field(:language, :string)
156 field(:status_ttl_days, :integer, default: nil)
159 :notification_settings,
160 Pleroma.User.NotificationSetting,
164 has_many(:notifications, Notification)
165 has_many(:registrations, Registration)
166 has_many(:deliveries, Delivery)
168 has_many(:outgoing_relationships, UserRelationship, foreign_key: :source_id)
169 has_many(:incoming_relationships, UserRelationship, foreign_key: :target_id)
171 has_many(:frontend_profiles, Pleroma.Akkoma.FrontendSettingsProfile)
173 many_to_many(:followed_hashtags, Hashtag,
175 on_delete: :delete_all,
176 join_through: HashtagFollow
179 for {relationship_type,
181 {outgoing_relation, outgoing_relation_target},
182 {incoming_relation, incoming_relation_source}
183 ]} <- @user_relationships_config do
184 # Definitions of `has_many` relations: :blocker_blocks, :muter_mutes, :reblog_muter_mutes,
185 # :notification_muter_mutes, :subscribee_subscriptions
186 has_many(outgoing_relation, UserRelationship,
187 foreign_key: :source_id,
188 where: [relationship_type: relationship_type]
191 # Definitions of `has_many` relations: :blockee_blocks, :mutee_mutes, :reblog_mutee_mutes,
192 # :notification_mutee_mutes, :subscriber_subscriptions
193 has_many(incoming_relation, UserRelationship,
194 foreign_key: :target_id,
195 where: [relationship_type: relationship_type]
198 # Definitions of `has_many` relations: :blocked_users, :muted_users, :reblog_muted_users,
199 # :notification_muted_users, :subscriber_users
200 has_many(outgoing_relation_target, through: [outgoing_relation, :target])
202 # Definitions of `has_many` relations: :blocker_users, :muter_users, :reblog_muter_users,
203 # :notification_muter_users, :subscribee_users
204 has_many(incoming_relation_source, through: [incoming_relation, :source])
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,
471 |> cast(params, [:name], empty_values: [])
472 |> validate_required([:ap_id])
473 |> validate_required([:name], trim: false)
474 |> unique_constraint(:nickname)
475 |> validate_format(:nickname, @email_regex)
476 |> validate_length(:bio, max: bio_limit)
477 |> validate_length(:name, max: name_limit)
478 |> validate_fields(true)
479 |> validate_non_local()
482 defp validate_non_local(cng) do
483 local? = get_field(cng, :local)
487 |> add_error(:local, "User is local, can't update with this changeset.")
493 def update_changeset(struct, params \\ %{}) do
494 bio_limit = Config.get([:instance, :user_bio_length], 5000)
495 name_limit = Config.get([:instance, :user_name_length], 100)
515 :hide_followers_count,
518 :allow_following_move,
522 :skip_thread_containment,
525 :pleroma_settings_store,
532 |> unique_constraint(:nickname)
533 |> validate_format(:nickname, local_nickname_regex())
534 |> validate_length(:bio, max: bio_limit)
535 |> validate_length(:name, min: 1, max: name_limit)
536 |> validate_inclusion(:actor_type, ["Person", "Service"])
537 |> validate_number(:status_ttl_days, greater_than: 0)
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
680 if Map.has_key?(params, :email) do
681 Map.put_new(params, :email, params[:email])
692 |> validate_required([:name, :nickname])
693 |> unique_constraint(:nickname)
694 |> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames]))
695 |> validate_format(:nickname, local_nickname_regex())
697 |> unique_constraint(:ap_id)
698 |> put_following_and_follower_and_featured_address()
702 def register_changeset(struct, params \\ %{}, opts \\ []) do
703 bio_limit = Config.get([:instance, :user_bio_length], 5000)
704 name_limit = Config.get([:instance, :user_name_length], 100)
705 reason_limit = Config.get([:instance, :registration_reason_length], 500)
708 if is_nil(opts[:confirmed]) do
709 !Config.get([:instance, :account_activation_required])
715 if is_nil(opts[:approved]) do
716 !Config.get([:instance, :account_approval_required])
722 |> confirmation_changeset(set_confirmation: confirmed?)
723 |> approval_changeset(set_approval: approved?)
731 :password_confirmation,
733 :registration_reason,
736 |> validate_required([:name, :nickname, :password, :password_confirmation])
737 |> validate_confirmation(:password)
738 |> unique_constraint(:email)
739 |> validate_format(:email, @email_regex)
740 |> validate_change(:email, fn :email, email ->
742 Config.get([User, :email_blacklist])
743 |> Enum.all?(fn blacklisted_domain ->
744 !String.ends_with?(email, ["@" <> blacklisted_domain, "." <> blacklisted_domain])
747 if valid?, do: [], else: [email: "Invalid email"]
749 |> unique_constraint(:nickname)
750 |> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames]))
751 |> validate_format(:nickname, local_nickname_regex())
752 |> validate_length(:bio, max: bio_limit)
753 |> validate_length(:name, min: 1, max: name_limit)
754 |> validate_length(:registration_reason, max: reason_limit)
755 |> maybe_validate_required_email(opts[:external])
758 |> unique_constraint(:ap_id)
759 |> put_following_and_follower_and_featured_address()
763 def maybe_validate_required_email(changeset, true), do: changeset
765 def maybe_validate_required_email(changeset, _) do
766 if Config.get([:instance, :account_activation_required]) do
767 validate_required(changeset, [:email])
773 def put_ap_id(changeset) do
774 ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
775 put_change(changeset, :ap_id, ap_id)
778 def put_following_and_follower_and_featured_address(changeset) do
779 user = %User{nickname: get_field(changeset, :nickname)}
780 followers = ap_followers(user)
781 following = ap_following(user)
782 featured = ap_featured_collection(user)
785 |> put_change(:follower_address, followers)
786 |> put_change(:following_address, following)
787 |> put_change(:featured_address, featured)
790 defp put_private_key(changeset) do
791 {:ok, pem} = Keys.generate_rsa_pem()
792 put_change(changeset, :keys, pem)
795 defp autofollow_users(user) do
796 candidates = Config.get([:instance, :autofollowed_nicknames])
799 User.Query.build(%{nickname: candidates, local: true, is_active: true})
802 follow_all(user, autofollowed_users)
805 defp autofollowing_users(user) do
806 candidates = Config.get([:instance, :autofollowing_nicknames])
808 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
810 |> Enum.each(&follow(&1, user, :follow_accept))
815 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
816 def register(%Ecto.Changeset{} = changeset) do
817 with {:ok, user} <- Repo.insert(changeset) do
818 post_register_action(user)
822 def post_register_action(%User{is_confirmed: false} = user) do
823 with {:ok, _} <- maybe_send_confirmation_email(user) do
828 def post_register_action(%User{is_approved: false} = user) do
829 with {:ok, _} <- send_user_approval_email(user),
830 {:ok, _} <- send_admin_approval_emails(user) do
835 def post_register_action(%User{is_approved: true, is_confirmed: true} = user) do
836 with {:ok, user} <- autofollow_users(user),
837 {:ok, _} <- autofollowing_users(user),
838 {:ok, user} <- set_cache(user),
839 {:ok, _} <- maybe_send_registration_email(user),
840 {:ok, _} <- maybe_send_welcome_email(user),
841 {:ok, _} <- maybe_send_welcome_message(user) do
846 defp send_user_approval_email(user) do
848 |> Pleroma.Emails.UserEmail.approval_pending_email()
849 |> Pleroma.Emails.Mailer.deliver_async()
854 defp send_admin_approval_emails(user) do
856 |> Enum.filter(fn user -> not is_nil(user.email) end)
857 |> Enum.each(fn superuser ->
859 |> Pleroma.Emails.AdminEmail.new_unapproved_registration(user)
860 |> Pleroma.Emails.Mailer.deliver_async()
866 defp maybe_send_welcome_message(user) do
867 if User.WelcomeMessage.enabled?() do
868 User.WelcomeMessage.post_message(user)
875 defp maybe_send_welcome_email(%User{email: email} = user) when is_binary(email) do
876 if User.WelcomeEmail.enabled?() do
877 User.WelcomeEmail.send_email(user)
884 defp maybe_send_welcome_email(_), do: {:ok, :noop}
886 @spec maybe_send_confirmation_email(User.t()) :: {:ok, :enqueued | :noop}
887 def maybe_send_confirmation_email(%User{is_confirmed: false, email: email} = user)
888 when is_binary(email) do
889 if Config.get([:instance, :account_activation_required]) do
890 send_confirmation_email(user)
897 def maybe_send_confirmation_email(_), do: {:ok, :noop}
899 @spec send_confirmation_email(Uset.t()) :: User.t()
900 def send_confirmation_email(%User{} = user) do
902 |> Pleroma.Emails.UserEmail.account_confirmation_email()
903 |> Pleroma.Emails.Mailer.deliver_async()
908 @spec maybe_send_registration_email(User.t()) :: {:ok, :enqueued | :noop}
909 defp maybe_send_registration_email(%User{email: email} = user) when is_binary(email) do
910 with false <- User.WelcomeEmail.enabled?(),
911 false <- Config.get([:instance, :account_activation_required], false),
912 false <- Config.get([:instance, :account_approval_required], false) do
914 |> Pleroma.Emails.UserEmail.successful_registration_email()
915 |> Pleroma.Emails.Mailer.deliver_async()
924 defp maybe_send_registration_email(_), do: {:ok, :noop}
926 def needs_update?(%User{local: true}), do: false
928 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
930 def needs_update?(%User{local: false} = user) do
931 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
934 def needs_update?(_), do: true
936 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
938 # "Locked" (self-locked) users demand explicit authorization of follow requests
939 def maybe_direct_follow(%User{} = follower, %User{local: true, is_locked: true} = followed) do
940 follow(follower, followed, :follow_pending)
943 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
944 follow(follower, followed)
947 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
948 if not ap_enabled?(followed) do
949 follow(follower, followed)
951 {:ok, follower, followed}
955 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
956 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
957 def follow_all(follower, followeds) do
959 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
960 |> Enum.each(&follow(follower, &1, :follow_accept))
965 def follow(%User{} = follower, %User{} = followed, state \\ :follow_accept) do
966 deny_follow_blocked = Config.get([:user, :deny_follow_blocked])
969 not followed.is_active ->
970 {:error, "Could not follow user: #{followed.nickname} is deactivated."}
972 deny_follow_blocked and blocks?(followed, follower) ->
973 {:error, "Could not follow user: #{followed.nickname} blocked you."}
976 FollowingRelationship.follow(follower, followed, state)
980 def unfollow(%User{ap_id: ap_id}, %User{ap_id: ap_id}) do
981 {:error, "Not subscribed!"}
984 @spec unfollow(User.t(), User.t()) :: {:ok, User.t(), Activity.t()} | {:error, String.t()}
985 def unfollow(%User{} = follower, %User{} = followed) do
986 case do_unfollow(follower, followed) do
987 {:ok, follower, followed} ->
988 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
995 @spec do_unfollow(User.t(), User.t()) :: {:ok, User.t(), User.t()} | {:error, String.t()}
996 defp do_unfollow(%User{} = follower, %User{} = followed) do
997 case get_follow_state(follower, followed) do
998 state when state in [:follow_pending, :follow_accept] ->
999 FollowingRelationship.unfollow(follower, followed)
1002 {:error, "Not subscribed!"}
1006 @doc "Returns follow state as Pleroma.FollowingRelationship.State value"
1007 def get_follow_state(%User{} = follower, %User{} = following) do
1008 following_relationship = FollowingRelationship.get(follower, following)
1009 get_follow_state(follower, following, following_relationship)
1012 def get_follow_state(
1014 %User{} = following,
1015 following_relationship
1017 case {following_relationship, following.local} do
1019 case Utils.fetch_latest_follow(follower, following) do
1020 %Activity{data: %{"state" => state}} when state in ["pending", "accept"] ->
1021 FollowingRelationship.state_to_enum(state)
1027 {%{state: state}, _} ->
1035 def locked?(%User{} = user) do
1036 user.is_locked || false
1039 def get_by_id(id) do
1040 Repo.get_by(User, id: id)
1043 def get_by_ap_id(ap_id) do
1044 Repo.get_by(User, ap_id: ap_id)
1047 def get_all_by_ap_id(ap_ids) do
1048 from(u in __MODULE__,
1049 where: u.ap_id in ^ap_ids
1054 def get_all_by_ids(ids) do
1055 from(u in __MODULE__, where: u.id in ^ids)
1059 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
1060 # of the ap_id and the domain and tries to get that user
1061 def get_by_guessed_nickname(ap_id) do
1062 domain = URI.parse(ap_id).host
1063 name = List.last(String.split(ap_id, "/"))
1064 nickname = "#{name}@#{domain}"
1066 get_cached_by_nickname(nickname)
1069 def set_cache({:ok, user}), do: set_cache(user)
1070 def set_cache({:error, err}), do: {:error, err}
1072 def set_cache(%User{} = user) do
1073 @cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
1074 @cachex.put(:user_cache, "nickname:#{user.nickname}", user)
1075 @cachex.put(:user_cache, "friends_ap_ids:#{user.nickname}", get_user_friends_ap_ids(user))
1079 def update_and_set_cache(struct, params) do
1081 |> update_changeset(params)
1082 |> update_and_set_cache()
1085 def update_and_set_cache(%{data: %Pleroma.User{} = user} = changeset) do
1086 was_superuser_before_update = User.superuser?(user)
1088 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
1091 |> maybe_remove_report_notifications(was_superuser_before_update)
1094 defp maybe_remove_report_notifications({:ok, %Pleroma.User{} = user} = result, true) do
1095 if not User.superuser?(user),
1096 do: user |> Notification.destroy_multiple_from_types(["pleroma:report"])
1101 defp maybe_remove_report_notifications(result, _) do
1105 def get_user_friends_ap_ids(user) do
1106 from(u in User.get_friends_query(user), select: u.ap_id)
1110 @spec get_cached_user_friends_ap_ids(User.t()) :: [String.t()]
1111 def get_cached_user_friends_ap_ids(user) do
1112 @cachex.fetch!(:user_cache, "friends_ap_ids:#{user.ap_id}", fn _ ->
1113 get_user_friends_ap_ids(user)
1117 def invalidate_cache(user) do
1118 @cachex.del(:user_cache, "ap_id:#{user.ap_id}")
1119 @cachex.del(:user_cache, "nickname:#{user.nickname}")
1120 @cachex.del(:user_cache, "friends_ap_ids:#{user.ap_id}")
1121 @cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
1122 @cachex.del(:user_cache, "muted_users_ap_ids:#{user.ap_id}")
1125 @spec get_cached_by_ap_id(String.t()) :: User.t() | nil
1126 def get_cached_by_ap_id(ap_id) do
1127 key = "ap_id:#{ap_id}"
1129 with {:ok, nil} <- @cachex.get(:user_cache, key),
1130 user when not is_nil(user) <- get_by_ap_id(ap_id),
1131 {:ok, true} <- @cachex.put(:user_cache, key, user) do
1139 def get_cached_by_id(id) do
1143 @cachex.fetch!(:user_cache, key, fn _ ->
1144 user = get_by_id(id)
1147 @cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
1148 {:commit, user.ap_id}
1154 get_cached_by_ap_id(ap_id)
1157 def get_cached_by_nickname(nickname) do
1158 key = "nickname:#{nickname}"
1160 @cachex.fetch!(:user_cache, key, fn _ ->
1161 case get_or_fetch_by_nickname(nickname) do
1162 {:ok, user} -> {:commit, user}
1163 {:error, _error} -> {:ignore, nil}
1168 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
1169 restrict_to_local = Config.get([:instance, :limit_to_local_content])
1172 is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
1173 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
1175 restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
1176 get_cached_by_nickname(nickname_or_id)
1178 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
1179 get_cached_by_nickname(nickname_or_id)
1186 @spec get_by_nickname(String.t()) :: User.t() | nil
1187 def get_by_nickname(nickname) do
1188 Repo.get_by(User, nickname: nickname) ||
1189 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
1190 Repo.get_by(User, nickname: local_nickname(nickname))
1194 def get_by_email(email), do: Repo.get_by(User, email: email)
1196 def get_by_nickname_or_email(nickname_or_email) do
1197 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
1200 def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname)
1202 def get_or_fetch_by_nickname(nickname) do
1203 with %User{} = user <- get_by_nickname(nickname) do
1207 with [_nick, _domain] <- String.split(nickname, "@"),
1208 {:ok, user} <- fetch_by_nickname(nickname) do
1211 _e -> {:error, "not found " <> nickname}
1216 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
1217 def get_followers_query(%User{} = user, nil) do
1218 User.Query.build(%{followers: user, is_active: true})
1221 def get_followers_query(%User{} = user, page) do
1223 |> get_followers_query(nil)
1224 |> User.Query.paginate(page, 20)
1227 @spec get_followers_query(User.t()) :: Ecto.Query.t()
1228 def get_followers_query(%User{} = user), do: get_followers_query(user, nil)
1230 @spec get_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
1231 def get_followers(%User{} = user, page \\ nil) do
1233 |> get_followers_query(page)
1237 @spec get_external_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
1238 def get_external_followers(%User{} = user, page \\ nil) do
1240 |> get_followers_query(page)
1241 |> User.Query.build(%{external: true})
1245 def get_followers_ids(%User{} = user, page \\ nil) do
1247 |> get_followers_query(page)
1248 |> select([u], u.id)
1252 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
1253 def get_friends_query(%User{} = user, nil) do
1254 User.Query.build(%{friends: user, deactivated: false})
1257 def get_friends_query(%User{} = user, page) do
1259 |> get_friends_query(nil)
1260 |> User.Query.paginate(page, 20)
1263 @spec get_friends_query(User.t()) :: Ecto.Query.t()
1264 def get_friends_query(%User{} = user), do: get_friends_query(user, nil)
1266 def get_friends(%User{} = user, page \\ nil) do
1268 |> get_friends_query(page)
1272 def get_friends_ap_ids(%User{} = user) do
1274 |> get_friends_query(nil)
1275 |> select([u], u.ap_id)
1279 def get_friends_ids(%User{} = user, page \\ nil) do
1281 |> get_friends_query(page)
1282 |> select([u], u.id)
1286 def increase_note_count(%User{} = user) do
1288 |> where(id: ^user.id)
1289 |> update([u], inc: [note_count: 1])
1291 |> Repo.update_all([])
1293 {1, [user]} -> set_cache(user)
1298 def decrease_note_count(%User{} = user) do
1300 |> where(id: ^user.id)
1303 note_count: fragment("greatest(0, note_count - 1)")
1307 |> Repo.update_all([])
1309 {1, [user]} -> set_cache(user)
1314 def update_note_count(%User{} = user, note_count \\ nil) do
1319 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
1325 |> cast(%{note_count: note_count}, [:note_count])
1326 |> update_and_set_cache()
1329 @spec maybe_fetch_follow_information(User.t()) :: User.t()
1330 def maybe_fetch_follow_information(user) do
1331 with {:ok, user} <- fetch_follow_information(user) do
1335 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
1341 def fetch_follow_information(user) do
1342 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
1344 |> follow_information_changeset(info)
1345 |> update_and_set_cache()
1349 defp follow_information_changeset(user, params) do
1356 :hide_followers_count,
1361 @spec update_follower_count(User.t()) :: {:ok, User.t()}
1362 def update_follower_count(%User{} = user) do
1363 if user.local or !Config.get([:instance, :external_user_synchronization]) do
1364 follower_count = FollowingRelationship.follower_count(user)
1367 |> follow_information_changeset(%{follower_count: follower_count})
1368 |> update_and_set_cache
1370 {:ok, maybe_fetch_follow_information(user)}
1374 @spec update_following_count(User.t()) :: {:ok, User.t()}
1375 def update_following_count(%User{local: false} = user) do
1376 if Config.get([:instance, :external_user_synchronization]) do
1377 {:ok, maybe_fetch_follow_information(user)}
1383 def update_following_count(%User{local: true} = user) do
1384 following_count = FollowingRelationship.following_count(user)
1387 |> follow_information_changeset(%{following_count: following_count})
1388 |> update_and_set_cache()
1391 @spec get_users_from_set([String.t()], keyword()) :: [User.t()]
1392 def get_users_from_set(ap_ids, opts \\ []) do
1393 local_only = Keyword.get(opts, :local_only, true)
1394 criteria = %{ap_id: ap_ids, is_active: true}
1395 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
1397 User.Query.build(criteria)
1401 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
1402 def get_recipients_from_activity(%Activity{recipients: to, actor: actor}) do
1405 query = User.Query.build(%{recipients_from_activity: to, local: true, is_active: true})
1411 @spec mute(User.t(), User.t(), map()) ::
1412 {:ok, list(UserRelationship.t())} | {:error, String.t()}
1413 def mute(%User{} = muter, %User{} = mutee, params \\ %{}) do
1414 notifications? = Map.get(params, :notifications, true)
1415 expires_in = Map.get(params, :expires_in, 0)
1417 with {:ok, user_mute} <- UserRelationship.create_mute(muter, mutee),
1418 {:ok, user_notification_mute} <-
1419 (notifications? && UserRelationship.create_notification_mute(muter, mutee)) ||
1421 if expires_in > 0 do
1422 Pleroma.Workers.MuteExpireWorker.enqueue(
1424 %{"muter_id" => muter.id, "mutee_id" => mutee.id},
1425 schedule_in: expires_in
1429 @cachex.del(:user_cache, "muted_users_ap_ids:#{muter.ap_id}")
1431 {:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
1435 def unmute(%User{} = muter, %User{} = mutee) do
1436 with {:ok, user_mute} <- UserRelationship.delete_mute(muter, mutee),
1437 {:ok, user_notification_mute} <-
1438 UserRelationship.delete_notification_mute(muter, mutee) do
1439 @cachex.del(:user_cache, "muted_users_ap_ids:#{muter.ap_id}")
1440 {:ok, [user_mute, user_notification_mute]}
1444 def unmute(muter_id, mutee_id) do
1445 with {:muter, %User{} = muter} <- {:muter, User.get_by_id(muter_id)},
1446 {:mutee, %User{} = mutee} <- {:mutee, User.get_by_id(mutee_id)} do
1447 unmute(muter, mutee)
1449 {who, result} = error ->
1451 "User.unmute/2 failed. #{who}: #{result}, muter_id: #{muter_id}, mutee_id: #{mutee_id}"
1458 def subscribe(%User{} = subscriber, %User{} = target) do
1459 deny_follow_blocked = Config.get([:user, :deny_follow_blocked])
1461 if blocks?(target, subscriber) and deny_follow_blocked do
1462 {:error, "Could not subscribe: #{target.nickname} is blocking you"}
1464 # Note: the relationship is inverse: subscriber acts as relationship target
1465 UserRelationship.create_inverse_subscription(target, subscriber)
1469 def subscribe(%User{} = subscriber, %{ap_id: ap_id}) do
1470 with %User{} = subscribee <- get_cached_by_ap_id(ap_id) do
1471 subscribe(subscriber, subscribee)
1475 def unsubscribe(%User{} = unsubscriber, %User{} = target) do
1476 # Note: the relationship is inverse: subscriber acts as relationship target
1477 UserRelationship.delete_inverse_subscription(target, unsubscriber)
1480 def unsubscribe(%User{} = unsubscriber, %{ap_id: ap_id}) do
1481 with %User{} = user <- get_cached_by_ap_id(ap_id) do
1482 unsubscribe(unsubscriber, user)
1486 def block(%User{} = blocker, %User{} = blocked) do
1487 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
1489 if following?(blocker, blocked) do
1490 {:ok, blocker, _} = unfollow(blocker, blocked)
1496 # clear any requested follows from both sides as well
1498 case CommonAPI.reject_follow_request(blocked, blocker) do
1499 {:ok, %User{} = updated_blocked} -> updated_blocked
1504 case CommonAPI.reject_follow_request(blocker, blocked) do
1505 {:ok, %User{} = updated_blocker} -> updated_blocker
1509 unsubscribe(blocked, blocker)
1511 unfollowing_blocked = Config.get([:activitypub, :unfollow_blocked], true)
1512 if unfollowing_blocked && following?(blocked, blocker), do: unfollow(blocked, blocker)
1514 {:ok, blocker} = update_follower_count(blocker)
1515 {:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
1516 add_to_block(blocker, blocked)
1519 # helper to handle the block given only an actor's AP id
1520 def block(%User{} = blocker, %{ap_id: ap_id}) do
1521 block(blocker, get_cached_by_ap_id(ap_id))
1524 def unblock(%User{} = blocker, %User{} = blocked) do
1525 remove_from_block(blocker, blocked)
1528 # helper to handle the block given only an actor's AP id
1529 def unblock(%User{} = blocker, %{ap_id: ap_id}) do
1530 unblock(blocker, get_cached_by_ap_id(ap_id))
1533 def mutes?(nil, _), do: false
1534 def mutes?(%User{} = user, %User{} = target), do: mutes_user?(user, target)
1536 def mutes_user?(%User{} = user, %User{} = target) do
1537 UserRelationship.mute_exists?(user, target)
1540 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
1541 def muted_notifications?(nil, _), do: false
1543 def muted_notifications?(%User{} = user, %User{} = target),
1544 do: UserRelationship.notification_mute_exists?(user, target)
1546 def blocks?(nil, _), do: false
1548 def blocks?(%User{} = user, %User{} = target) do
1549 blocks_user?(user, target) ||
1550 (blocks_domain?(user, target) and not User.following?(user, target))
1553 def blocks_user?(%User{} = user, %User{} = target) do
1554 UserRelationship.block_exists?(user, target)
1557 def blocks_user?(_, _), do: false
1559 def blocks_domain?(%User{} = user, %User{} = target) do
1560 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
1561 %{host: host} = URI.parse(target.ap_id)
1562 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1565 def blocks_domain?(_, _), do: false
1567 def subscribed_to?(%User{} = user, %User{} = target) do
1568 # Note: the relationship is inverse: subscriber acts as relationship target
1569 UserRelationship.inverse_subscription_exists?(target, user)
1572 def subscribed_to?(%User{} = user, %{ap_id: ap_id}) do
1573 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1574 subscribed_to?(user, target)
1579 Returns map of outgoing (blocked, muted etc.) relationships' user AP IDs by relation type.
1580 E.g. `outgoing_relationships_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}`
1582 @spec outgoing_relationships_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())}
1583 def outgoing_relationships_ap_ids(_user, []), do: %{}
1585 def outgoing_relationships_ap_ids(nil, _relationship_types), do: %{}
1587 def outgoing_relationships_ap_ids(%User{} = user, relationship_types)
1588 when is_list(relationship_types) do
1591 |> assoc(:outgoing_relationships)
1592 |> join(:inner, [user_rel], u in assoc(user_rel, :target))
1593 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1594 |> select([user_rel, u], [user_rel.relationship_type, fragment("array_agg(?)", u.ap_id)])
1595 |> group_by([user_rel, u], user_rel.relationship_type)
1597 |> Enum.into(%{}, fn [k, v] -> {k, v} end)
1602 fn rel_type -> {rel_type, db_result[rel_type] || []} end
1606 def incoming_relationships_ungrouped_ap_ids(user, relationship_types, ap_ids \\ nil)
1608 def incoming_relationships_ungrouped_ap_ids(_user, [], _ap_ids), do: []
1610 def incoming_relationships_ungrouped_ap_ids(nil, _relationship_types, _ap_ids), do: []
1612 def incoming_relationships_ungrouped_ap_ids(%User{} = user, relationship_types, ap_ids)
1613 when is_list(relationship_types) do
1615 |> assoc(:incoming_relationships)
1616 |> join(:inner, [user_rel], u in assoc(user_rel, :source))
1617 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1618 |> maybe_filter_on_ap_id(ap_ids)
1619 |> select([user_rel, u], u.ap_id)
1624 defp maybe_filter_on_ap_id(query, ap_ids) when is_list(ap_ids) do
1625 where(query, [user_rel, u], u.ap_id in ^ap_ids)
1628 defp maybe_filter_on_ap_id(query, _ap_ids), do: query
1630 def set_activation_async(user, status \\ true) do
1631 BackgroundWorker.enqueue("user_activation", %{"user_id" => user.id, "status" => status})
1634 @spec set_activation([User.t()], boolean()) :: {:ok, User.t()} | {:error, Changeset.t()}
1635 def set_activation(users, status) when is_list(users) do
1636 Repo.transaction(fn ->
1637 for user <- users, do: set_activation(user, status)
1641 @spec set_activation(User.t(), boolean()) :: {:ok, User.t()} | {:error, Changeset.t()}
1642 def set_activation(%User{} = user, status) do
1643 with {:ok, user} <- set_activation_status(user, status) do
1646 |> Enum.filter(& &1.local)
1647 |> Enum.each(&set_cache(update_following_count(&1)))
1649 # Only update local user counts, remote will be update during the next pull.
1652 |> Enum.filter(& &1.local)
1653 |> Enum.each(&do_unfollow(user, &1))
1659 def approve(users) when is_list(users) do
1660 Repo.transaction(fn ->
1661 Enum.map(users, fn user ->
1662 with {:ok, user} <- approve(user), do: user
1667 def approve(%User{is_approved: false} = user) do
1668 with chg <- change(user, is_approved: true),
1669 {:ok, user} <- update_and_set_cache(chg) do
1670 post_register_action(user)
1675 def approve(%User{} = user), do: {:ok, user}
1677 def confirm(users) when is_list(users) do
1678 Repo.transaction(fn ->
1679 Enum.map(users, fn user ->
1680 with {:ok, user} <- confirm(user), do: user
1685 def confirm(%User{is_confirmed: false} = user) do
1686 with chg <- confirmation_changeset(user, set_confirmation: true),
1687 {:ok, user} <- update_and_set_cache(chg) do
1688 post_register_action(user)
1693 def confirm(%User{} = user), do: {:ok, user}
1695 def set_suggestion(users, is_suggested) when is_list(users) do
1696 Repo.transaction(fn ->
1697 Enum.map(users, fn user ->
1698 with {:ok, user} <- set_suggestion(user, is_suggested), do: user
1703 def set_suggestion(%User{is_suggested: is_suggested} = user, is_suggested), do: {:ok, user}
1705 def set_suggestion(%User{} = user, is_suggested) when is_boolean(is_suggested) do
1707 |> change(is_suggested: is_suggested)
1708 |> update_and_set_cache()
1711 def update_notification_settings(%User{} = user, settings) do
1713 |> cast(%{notification_settings: settings}, [])
1714 |> cast_embed(:notification_settings)
1715 |> validate_required([:notification_settings])
1716 |> update_and_set_cache()
1719 @spec purge_user_changeset(User.t()) :: Changeset.t()
1720 def purge_user_changeset(user) do
1721 # "Right to be forgotten"
1722 # https://gdpr.eu/right-to-be-forgotten/
1731 last_refreshed_at: nil,
1732 last_digest_emailed_at: nil,
1739 password_reset_pending: false,
1740 registration_reason: nil,
1741 confirmation_token: nil,
1745 is_moderator: false,
1747 mastofe_settings: nil,
1750 pleroma_settings_store: %{},
1753 is_discoverable: false,
1757 # nickname: preserved
1761 # Purge doesn't delete the user from the database.
1762 # It just nulls all its fields and deactivates it.
1763 # See `User.purge_user_changeset/1` above.
1764 defp purge(%User{} = user) do
1766 |> purge_user_changeset()
1767 |> update_and_set_cache()
1770 def delete(users) when is_list(users) do
1771 for user <- users, do: delete(user)
1774 def delete(%User{} = user) do
1775 # Purge the user immediately
1777 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1780 # *Actually* delete the user from the DB
1781 defp delete_from_db(%User{} = user) do
1782 invalidate_cache(user)
1786 # If the user never finalized their account, it's safe to delete them.
1787 defp maybe_delete_from_db(%User{local: true, is_confirmed: false} = user),
1788 do: delete_from_db(user)
1790 defp maybe_delete_from_db(%User{local: true, is_approved: false} = user),
1791 do: delete_from_db(user)
1793 defp maybe_delete_from_db(user), do: {:ok, user}
1795 def perform(:force_password_reset, user), do: force_password_reset(user)
1797 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1798 def perform(:delete, %User{} = user) do
1799 # Purge the user again, in case perform/2 is called directly
1802 # Remove all relationships
1805 |> Enum.each(fn follower ->
1806 ActivityPub.unfollow(follower, user)
1807 unfollow(follower, user)
1812 |> Enum.each(fn followed ->
1813 ActivityPub.unfollow(user, followed)
1814 unfollow(user, followed)
1817 delete_user_activities(user)
1818 delete_notifications_from_user_activities(user)
1819 delete_outgoing_pending_follow_requests(user)
1821 maybe_delete_from_db(user)
1824 def perform(:set_activation_async, user, status), do: set_activation(user, status)
1826 @spec external_users_query() :: Ecto.Query.t()
1827 def external_users_query do
1835 @spec external_users(keyword()) :: [User.t()]
1836 def external_users(opts \\ []) do
1838 external_users_query()
1839 |> select([u], struct(u, [:id, :ap_id]))
1843 do: where(query, [u], u.id > ^opts[:max_id]),
1848 do: limit(query, ^opts[:limit]),
1854 def delete_notifications_from_user_activities(%User{ap_id: ap_id}) do
1856 |> join(:inner, [n], activity in assoc(n, :activity))
1857 |> where([n, a], fragment("? = ?", a.actor, ^ap_id))
1858 |> Repo.delete_all()
1861 def delete_user_activities(%User{ap_id: ap_id} = user) do
1863 |> Activity.Queries.by_actor()
1864 |> Repo.chunk_stream(50, :batches)
1865 |> Stream.each(fn activities ->
1866 Enum.each(activities, fn activity -> delete_activity(activity, user) end)
1871 defp delete_activity(%{data: %{"type" => "Create", "object" => object}} = activity, user) do
1872 with {_, %Object{}} <- {:find_object, Object.get_by_ap_id(object)},
1873 {:ok, delete_data, _} <- Builder.delete(user, object) do
1874 Pipeline.common_pipeline(delete_data, local: user.local)
1876 {:find_object, nil} ->
1877 # We have the create activity, but not the object, it was probably pruned.
1878 # Insert a tombstone and try again
1879 with {:ok, tombstone_data, _} <- Builder.tombstone(user.ap_id, object),
1880 {:ok, _tombstone} <- Object.create(tombstone_data) do
1881 delete_activity(activity, user)
1885 Logger.error("Could not delete #{object} created by #{activity.data["ap_id"]}")
1886 Logger.error("Error: #{inspect(e)}")
1890 defp delete_activity(%{data: %{"type" => type}} = activity, user)
1891 when type in ["Like", "Announce"] do
1892 {:ok, undo, _} = Builder.undo(user, activity)
1893 Pipeline.common_pipeline(undo, local: user.local)
1896 defp delete_activity(_activity, _user), do: "Doing nothing"
1898 defp delete_outgoing_pending_follow_requests(user) do
1900 |> FollowingRelationship.outgoing_pending_follow_requests_query()
1901 |> Repo.delete_all()
1904 def html_filter_policy(%User{no_rich_text: true}) do
1905 Pleroma.HTML.Scrubber.TwitterText
1908 def html_filter_policy(_), do: Config.get([:markup, :scrub_policy])
1910 def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
1912 def get_or_fetch_by_ap_id(ap_id) do
1913 cached_user = get_cached_by_ap_id(ap_id)
1915 maybe_fetched_user = needs_update?(cached_user) && fetch_by_ap_id(ap_id)
1917 case {cached_user, maybe_fetched_user} do
1918 {_, {:ok, %User{} = user}} ->
1921 {%User{} = user, _} ->
1925 Logger.error("Could not fetch user #{ap_id}, #{inspect(e)}")
1926 {:error, :not_found}
1931 Creates an internal service actor by URI if missing.
1932 Optionally takes nickname for addressing.
1934 @spec get_or_create_service_actor_by_ap_id(String.t(), String.t()) :: User.t() | nil
1935 def get_or_create_service_actor_by_ap_id(uri, nickname) do
1937 case get_cached_by_ap_id(uri) do
1939 with {:error, %{errors: errors}} <- create_service_actor(uri, nickname) do
1940 Logger.error("Cannot create service actor: #{uri}/.\n#{inspect(errors)}")
1944 %User{invisible: false} = user ->
1954 @spec set_invisible(User.t()) :: {:ok, User.t()}
1955 defp set_invisible(user) do
1957 |> change(%{invisible: true})
1958 |> update_and_set_cache()
1961 @spec create_service_actor(String.t(), String.t()) ::
1962 {:ok, User.t()} | {:error, Ecto.Changeset.t()}
1963 defp create_service_actor(uri, nickname) do
1969 follower_address: uri <> "/followers"
1972 |> put_private_key()
1973 |> unique_constraint(:nickname)
1978 def public_key(%{public_key: public_key_pem}) when is_binary(public_key_pem) do
1981 |> :public_key.pem_decode()
1983 |> :public_key.pem_entry_decode()
1988 def public_key(_), do: {:error, "key not found"}
1990 def get_public_key_for_ap_id(ap_id) do
1991 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1992 {:ok, public_key} <- public_key(user) do
1999 def ap_enabled?(%User{local: true}), do: true
2000 def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled
2001 def ap_enabled?(_), do: false
2003 @doc "Gets or fetch a user by uri or nickname."
2004 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
2005 def get_or_fetch("http://" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
2006 def get_or_fetch("https://" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
2007 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
2009 # wait a period of time and return newest version of the User structs
2010 # this is because we have synchronous follow APIs and need to simulate them
2011 # with an async handshake
2012 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
2013 with %User{} = a <- get_cached_by_id(a.id),
2014 %User{} = b <- get_cached_by_id(b.id) do
2021 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
2022 with :ok <- :timer.sleep(timeout),
2023 %User{} = a <- get_cached_by_id(a.id),
2024 %User{} = b <- get_cached_by_id(b.id) do
2031 def parse_bio(bio) when is_binary(bio) and bio != "" do
2033 |> CommonUtils.format_input("text/plain", mentions_format: :full)
2037 def parse_bio(_), do: ""
2039 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
2040 # TODO: get profile URLs other than user.ap_id
2041 profile_urls = [user.ap_id]
2044 |> CommonUtils.format_input("text/plain",
2045 mentions_format: :full,
2046 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
2051 def parse_bio(_, _), do: ""
2053 def tag(user_identifiers, tags) when is_list(user_identifiers) do
2054 Repo.transaction(fn ->
2055 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
2059 def tag(nickname, tags) when is_binary(nickname),
2060 do: tag(get_by_nickname(nickname), tags)
2062 def tag(%User{} = user, tags),
2063 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
2065 def untag(user_identifiers, tags) when is_list(user_identifiers) do
2066 Repo.transaction(fn ->
2067 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
2071 def untag(nickname, tags) when is_binary(nickname),
2072 do: untag(get_by_nickname(nickname), tags)
2074 def untag(%User{} = user, tags),
2075 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
2077 defp update_tags(%User{} = user, new_tags) do
2078 {:ok, updated_user} =
2080 |> change(%{tags: new_tags})
2081 |> update_and_set_cache()
2086 defp normalize_tags(tags) do
2089 |> Enum.map(&String.downcase/1)
2092 def local_nickname_regex do
2093 if Config.get([:instance, :extended_nickname_format]) do
2094 @extended_local_nickname_regex
2096 @strict_local_nickname_regex
2100 def local_nickname(nickname_or_mention) do
2103 |> String.split("@")
2107 def full_nickname(%User{} = user) do
2108 if String.contains?(user.nickname, "@") do
2111 %{host: host} = URI.parse(user.ap_id)
2112 user.nickname <> "@" <> host
2116 def full_nickname(nickname_or_mention),
2117 do: String.trim_leading(nickname_or_mention, "@")
2119 def error_user(ap_id) do
2123 nickname: "erroruser@example.com",
2124 inserted_at: NaiveDateTime.utc_now()
2128 @spec all_superusers() :: [User.t()]
2129 def all_superusers do
2130 User.Query.build(%{super_users: true, local: true, is_active: true})
2134 def muting_reblogs?(%User{} = user, %User{} = target) do
2135 UserRelationship.reblog_mute_exists?(user, target)
2138 def showing_reblogs?(%User{} = user, %User{} = target) do
2139 not muting_reblogs?(user, target)
2143 The function returns a query to get users with no activity for given interval of days.
2144 Inactive users are those who didn't read any notification, or had any activity where
2145 the user is the activity's actor, during `inactivity_threshold` days.
2146 Deactivated users will not appear in this list.
2150 iex> Pleroma.User.list_inactive_users()
2153 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
2154 def list_inactive_users_query(inactivity_threshold \\ 7) do
2155 negative_inactivity_threshold = -inactivity_threshold
2156 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
2157 # Subqueries are not supported in `where` clauses, join gets too complicated.
2158 has_read_notifications =
2159 from(n in Pleroma.Notification,
2160 where: n.seen == true,
2162 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
2165 |> Pleroma.Repo.all()
2167 from(u in Pleroma.User,
2168 left_join: a in Pleroma.Activity,
2169 on: u.ap_id == a.actor,
2170 where: not is_nil(u.nickname),
2171 where: u.is_active == ^true,
2172 where: u.id not in ^has_read_notifications,
2175 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
2176 is_nil(max(a.inserted_at))
2181 Enable or disable email notifications for user
2185 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => false}}, "digest", true)
2186 Pleroma.User{email_notifications: %{"digest" => true}}
2188 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => true}}, "digest", false)
2189 Pleroma.User{email_notifications: %{"digest" => false}}
2191 @spec switch_email_notifications(t(), String.t(), boolean()) ::
2192 {:ok, t()} | {:error, Ecto.Changeset.t()}
2193 def switch_email_notifications(user, type, status) do
2194 User.update_email_notifications(user, %{type => status})
2198 Set `last_digest_emailed_at` value for the user to current time
2200 @spec touch_last_digest_emailed_at(t()) :: t()
2201 def touch_last_digest_emailed_at(user) do
2202 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
2204 {:ok, updated_user} =
2206 |> change(%{last_digest_emailed_at: now})
2207 |> update_and_set_cache()
2212 @spec set_confirmation(User.t(), boolean()) :: {:ok, User.t()} | {:error, Changeset.t()}
2213 def set_confirmation(%User{} = user, bool) do
2215 |> confirmation_changeset(set_confirmation: bool)
2216 |> update_and_set_cache()
2219 def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
2223 def get_mascot(%{mascot: mascot}) when is_nil(mascot) do
2224 # use instance-default
2225 config = Config.get([:assets, :mascots])
2226 default_mascot = Config.get([:assets, :default_mascot])
2227 mascot = Keyword.get(config, default_mascot)
2230 "id" => "default-mascot",
2231 "url" => mascot[:url],
2232 "preview_url" => mascot[:url],
2234 "mime_type" => mascot[:mime_type]
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)
2562 defp maybe_load_followed_hashtags(%User{followed_hashtags: follows} = user)
2563 when is_list(follows),
2566 defp maybe_load_followed_hashtags(%User{} = user) do
2567 followed_hashtags = HashtagFollow.get_by_user(user)
2568 %{user | followed_hashtags: followed_hashtags}
2571 def followed_hashtags(%User{followed_hashtags: follows})
2572 when is_list(follows),
2575 def followed_hashtags(%User{} = user) do
2578 |> maybe_load_followed_hashtags()
2581 user.followed_hashtags
2584 def follow_hashtag(%User{} = user, %Hashtag{} = hashtag) do
2585 Logger.debug("Follow hashtag #{hashtag.name} for user #{user.nickname}")
2586 user = maybe_load_followed_hashtags(user)
2588 with {:ok, _} <- HashtagFollow.new(user, hashtag),
2589 follows <- HashtagFollow.get_by_user(user),
2590 %User{} = user <- user |> Map.put(:followed_hashtags, follows) do
2596 def unfollow_hashtag(%User{} = user, %Hashtag{} = hashtag) do
2597 Logger.debug("Unfollow hashtag #{hashtag.name} for user #{user.nickname}")
2598 user = maybe_load_followed_hashtags(user)
2600 with {:ok, _} <- HashtagFollow.delete(user, hashtag),
2601 follows <- HashtagFollow.get_by_user(user),
2602 %User{} = user <- user |> Map.put(:followed_hashtags, follows) do
2608 def following_hashtag?(%User{} = user, %Hashtag{} = hashtag) do
2609 not is_nil(HashtagFollow.get(user, hashtag))