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)
602 {:error, :file_too_large} ->
603 Ecto.Changeset.validate_change(changeset, map_field, fn map_field, _value ->
604 [{map_field, "file is too large"}]
612 defp put_upload(value, type) do
613 with %Plug.Upload{} <- value,
614 {:ok, object} <- ActivityPub.upload(value, type: type) do
619 def update_as_admin_changeset(struct, params) do
621 |> update_changeset(params)
622 |> cast(params, [:email])
623 |> delete_change(:also_known_as)
624 |> unique_constraint(:email)
625 |> validate_format(:email, @email_regex)
626 |> validate_inclusion(:actor_type, ["Person", "Service"])
629 @spec update_as_admin(User.t(), map()) :: {:ok, User.t()} | {:error, Changeset.t()}
630 def update_as_admin(user, params) do
631 params = Map.put(params, "password_confirmation", params["password"])
632 changeset = update_as_admin_changeset(user, params)
634 if params["password"] do
635 reset_password(user, changeset, params)
637 User.update_and_set_cache(changeset)
641 def password_update_changeset(struct, params) do
643 |> cast(params, [:password, :password_confirmation])
644 |> validate_required([:password, :password_confirmation])
645 |> validate_confirmation(:password)
646 |> put_password_hash()
647 |> put_change(:password_reset_pending, false)
650 @spec reset_password(User.t(), map()) :: {:ok, User.t()} | {:error, Changeset.t()}
651 def reset_password(%User{} = user, params) do
652 reset_password(user, user, params)
655 def reset_password(%User{id: user_id} = user, struct, params) do
658 |> Multi.update(:user, password_update_changeset(struct, params))
659 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
660 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
662 case Repo.transaction(multi) do
663 {:ok, %{user: user} = _} -> set_cache(user)
664 {:error, _, changeset, _} -> {:error, changeset}
668 def update_password_reset_pending(user, value) do
671 |> put_change(:password_reset_pending, value)
672 |> update_and_set_cache()
675 def force_password_reset_async(user) do
676 BackgroundWorker.enqueue("force_password_reset", %{"user_id" => user.id})
679 @spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
680 def force_password_reset(user), do: update_password_reset_pending(user, true)
682 # Used to auto-register LDAP accounts which won't have a password hash stored locally
683 def register_changeset_ldap(struct, params = %{password: password})
684 when is_nil(password) do
686 if Map.has_key?(params, :email) do
687 Map.put_new(params, :email, params[:email])
698 |> validate_required([:name, :nickname])
699 |> unique_constraint(:nickname)
700 |> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames]))
701 |> validate_format(:nickname, local_nickname_regex())
703 |> unique_constraint(:ap_id)
704 |> put_following_and_follower_and_featured_address()
708 def register_changeset(struct, params \\ %{}, opts \\ []) do
709 bio_limit = Config.get([:instance, :user_bio_length], 5000)
710 name_limit = Config.get([:instance, :user_name_length], 100)
711 reason_limit = Config.get([:instance, :registration_reason_length], 500)
714 if is_nil(opts[:confirmed]) do
715 !Config.get([:instance, :account_activation_required])
721 if is_nil(opts[:approved]) do
722 !Config.get([:instance, :account_approval_required])
728 |> confirmation_changeset(set_confirmation: confirmed?)
729 |> approval_changeset(set_approval: approved?)
737 :password_confirmation,
739 :registration_reason,
742 |> validate_required([:name, :nickname, :password, :password_confirmation])
743 |> validate_confirmation(:password)
744 |> unique_constraint(:email)
745 |> validate_format(:email, @email_regex)
746 |> validate_change(:email, fn :email, email ->
748 Config.get([User, :email_blacklist])
749 |> Enum.all?(fn blacklisted_domain ->
750 !String.ends_with?(email, ["@" <> blacklisted_domain, "." <> blacklisted_domain])
753 if valid?, do: [], else: [email: "Invalid email"]
755 |> unique_constraint(:nickname)
756 |> validate_exclusion(:nickname, Config.get([User, :restricted_nicknames]))
757 |> validate_format(:nickname, local_nickname_regex())
758 |> validate_length(:bio, max: bio_limit)
759 |> validate_length(:name, min: 1, max: name_limit)
760 |> validate_length(:registration_reason, max: reason_limit)
761 |> maybe_validate_required_email(opts[:external])
764 |> unique_constraint(:ap_id)
765 |> put_following_and_follower_and_featured_address()
769 def maybe_validate_required_email(changeset, true), do: changeset
771 def maybe_validate_required_email(changeset, _) do
772 if Config.get([:instance, :account_activation_required]) do
773 validate_required(changeset, [:email])
779 def put_ap_id(changeset) do
780 ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
781 put_change(changeset, :ap_id, ap_id)
784 def put_following_and_follower_and_featured_address(changeset) do
785 user = %User{nickname: get_field(changeset, :nickname)}
786 followers = ap_followers(user)
787 following = ap_following(user)
788 featured = ap_featured_collection(user)
791 |> put_change(:follower_address, followers)
792 |> put_change(:following_address, following)
793 |> put_change(:featured_address, featured)
796 defp put_private_key(changeset) do
797 {:ok, pem} = Keys.generate_rsa_pem()
798 put_change(changeset, :keys, pem)
801 defp autofollow_users(user) do
802 candidates = Config.get([:instance, :autofollowed_nicknames])
805 User.Query.build(%{nickname: candidates, local: true, is_active: true})
808 follow_all(user, autofollowed_users)
811 defp autofollowing_users(user) do
812 candidates = Config.get([:instance, :autofollowing_nicknames])
814 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
816 |> Enum.each(&follow(&1, user, :follow_accept))
821 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
822 def register(%Ecto.Changeset{} = changeset) do
823 with {:ok, user} <- Repo.insert(changeset) do
824 post_register_action(user)
828 def post_register_action(%User{is_confirmed: false} = user) do
829 with {:ok, _} <- maybe_send_confirmation_email(user) do
834 def post_register_action(%User{is_approved: false} = user) do
835 with {:ok, _} <- send_user_approval_email(user),
836 {:ok, _} <- send_admin_approval_emails(user) do
841 def post_register_action(%User{is_approved: true, is_confirmed: true} = user) do
842 with {:ok, user} <- autofollow_users(user),
843 {:ok, _} <- autofollowing_users(user),
844 {:ok, user} <- set_cache(user),
845 {:ok, _} <- maybe_send_registration_email(user),
846 {:ok, _} <- maybe_send_welcome_email(user),
847 {:ok, _} <- maybe_send_welcome_message(user) do
852 defp send_user_approval_email(user) do
854 |> Pleroma.Emails.UserEmail.approval_pending_email()
855 |> Pleroma.Emails.Mailer.deliver_async()
860 defp send_admin_approval_emails(user) do
862 |> Enum.filter(fn user -> not is_nil(user.email) end)
863 |> Enum.each(fn superuser ->
865 |> Pleroma.Emails.AdminEmail.new_unapproved_registration(user)
866 |> Pleroma.Emails.Mailer.deliver_async()
872 defp maybe_send_welcome_message(user) do
873 if User.WelcomeMessage.enabled?() do
874 User.WelcomeMessage.post_message(user)
881 defp maybe_send_welcome_email(%User{email: email} = user) when is_binary(email) do
882 if User.WelcomeEmail.enabled?() do
883 User.WelcomeEmail.send_email(user)
890 defp maybe_send_welcome_email(_), do: {:ok, :noop}
892 @spec maybe_send_confirmation_email(User.t()) :: {:ok, :enqueued | :noop}
893 def maybe_send_confirmation_email(%User{is_confirmed: false, email: email} = user)
894 when is_binary(email) do
895 if Config.get([:instance, :account_activation_required]) do
896 send_confirmation_email(user)
903 def maybe_send_confirmation_email(_), do: {:ok, :noop}
905 @spec send_confirmation_email(Uset.t()) :: User.t()
906 def send_confirmation_email(%User{} = user) do
908 |> Pleroma.Emails.UserEmail.account_confirmation_email()
909 |> Pleroma.Emails.Mailer.deliver_async()
914 @spec maybe_send_registration_email(User.t()) :: {:ok, :enqueued | :noop}
915 defp maybe_send_registration_email(%User{email: email} = user) when is_binary(email) do
916 with false <- User.WelcomeEmail.enabled?(),
917 false <- Config.get([:instance, :account_activation_required], false),
918 false <- Config.get([:instance, :account_approval_required], false) do
920 |> Pleroma.Emails.UserEmail.successful_registration_email()
921 |> Pleroma.Emails.Mailer.deliver_async()
930 defp maybe_send_registration_email(_), do: {:ok, :noop}
932 def needs_update?(%User{local: true}), do: false
934 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
936 def needs_update?(%User{local: false} = user) do
937 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
940 def needs_update?(_), do: true
942 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
944 # "Locked" (self-locked) users demand explicit authorization of follow requests
945 def maybe_direct_follow(%User{} = follower, %User{local: true, is_locked: true} = followed) do
946 follow(follower, followed, :follow_pending)
949 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
950 follow(follower, followed)
953 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
954 if not ap_enabled?(followed) do
955 follow(follower, followed)
957 {:ok, follower, followed}
961 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
962 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
963 def follow_all(follower, followeds) do
965 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
966 |> Enum.each(&follow(follower, &1, :follow_accept))
971 def follow(%User{} = follower, %User{} = followed, state \\ :follow_accept) do
972 deny_follow_blocked = Config.get([:user, :deny_follow_blocked])
975 not followed.is_active ->
976 {:error, "Could not follow user: #{followed.nickname} is deactivated."}
978 deny_follow_blocked and blocks?(followed, follower) ->
979 {:error, "Could not follow user: #{followed.nickname} blocked you."}
982 FollowingRelationship.follow(follower, followed, state)
986 def unfollow(%User{ap_id: ap_id}, %User{ap_id: ap_id}) do
987 {:error, "Not subscribed!"}
990 @spec unfollow(User.t(), User.t()) :: {:ok, User.t(), Activity.t()} | {:error, String.t()}
991 def unfollow(%User{} = follower, %User{} = followed) do
992 case do_unfollow(follower, followed) do
993 {:ok, follower, followed} ->
994 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
1001 @spec do_unfollow(User.t(), User.t()) :: {:ok, User.t(), User.t()} | {:error, String.t()}
1002 defp do_unfollow(%User{} = follower, %User{} = followed) do
1003 case get_follow_state(follower, followed) do
1004 state when state in [:follow_pending, :follow_accept] ->
1005 FollowingRelationship.unfollow(follower, followed)
1008 {:error, "Not subscribed!"}
1012 @doc "Returns follow state as Pleroma.FollowingRelationship.State value"
1013 def get_follow_state(%User{} = follower, %User{} = following) do
1014 following_relationship = FollowingRelationship.get(follower, following)
1015 get_follow_state(follower, following, following_relationship)
1018 def get_follow_state(
1020 %User{} = following,
1021 following_relationship
1023 case {following_relationship, following.local} do
1025 case Utils.fetch_latest_follow(follower, following) do
1026 %Activity{data: %{"state" => state}} when state in ["pending", "accept"] ->
1027 FollowingRelationship.state_to_enum(state)
1033 {%{state: state}, _} ->
1041 def locked?(%User{} = user) do
1042 user.is_locked || false
1045 def get_by_id(id) do
1046 Repo.get_by(User, id: id)
1049 def get_by_ap_id(ap_id) do
1050 Repo.get_by(User, ap_id: ap_id)
1053 def get_all_by_ap_id(ap_ids) do
1054 from(u in __MODULE__,
1055 where: u.ap_id in ^ap_ids
1060 def get_all_by_ids(ids) do
1061 from(u in __MODULE__, where: u.id in ^ids)
1065 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
1066 # of the ap_id and the domain and tries to get that user
1067 def get_by_guessed_nickname(ap_id) do
1068 domain = URI.parse(ap_id).host
1069 name = List.last(String.split(ap_id, "/"))
1070 nickname = "#{name}@#{domain}"
1072 get_cached_by_nickname(nickname)
1075 def set_cache({:ok, user}), do: set_cache(user)
1076 def set_cache({:error, err}), do: {:error, err}
1078 def set_cache(%User{} = user) do
1079 @cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
1080 @cachex.put(:user_cache, "nickname:#{user.nickname}", user)
1081 @cachex.put(:user_cache, "friends_ap_ids:#{user.nickname}", get_user_friends_ap_ids(user))
1085 def update_and_set_cache(struct, params) do
1087 |> update_changeset(params)
1088 |> update_and_set_cache()
1091 def update_and_set_cache(%{data: %Pleroma.User{} = user} = changeset) do
1092 was_superuser_before_update = User.superuser?(user)
1094 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
1097 |> maybe_remove_report_notifications(was_superuser_before_update)
1100 defp maybe_remove_report_notifications({:ok, %Pleroma.User{} = user} = result, true) do
1101 if not User.superuser?(user),
1102 do: user |> Notification.destroy_multiple_from_types(["pleroma:report"])
1107 defp maybe_remove_report_notifications(result, _) do
1111 def get_user_friends_ap_ids(user) do
1112 from(u in User.get_friends_query(user), select: u.ap_id)
1116 @spec get_cached_user_friends_ap_ids(User.t()) :: [String.t()]
1117 def get_cached_user_friends_ap_ids(user) do
1118 @cachex.fetch!(:user_cache, "friends_ap_ids:#{user.ap_id}", fn _ ->
1119 get_user_friends_ap_ids(user)
1123 def invalidate_cache(user) do
1124 @cachex.del(:user_cache, "ap_id:#{user.ap_id}")
1125 @cachex.del(:user_cache, "nickname:#{user.nickname}")
1126 @cachex.del(:user_cache, "friends_ap_ids:#{user.ap_id}")
1127 @cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
1128 @cachex.del(:user_cache, "muted_users_ap_ids:#{user.ap_id}")
1131 @spec get_cached_by_ap_id(String.t()) :: User.t() | nil
1132 def get_cached_by_ap_id(ap_id) do
1133 key = "ap_id:#{ap_id}"
1135 with {:ok, nil} <- @cachex.get(:user_cache, key),
1136 user when not is_nil(user) <- get_by_ap_id(ap_id),
1137 {:ok, true} <- @cachex.put(:user_cache, key, user) do
1145 def get_cached_by_id(id) do
1149 @cachex.fetch!(:user_cache, key, fn _ ->
1150 user = get_by_id(id)
1153 @cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
1154 {:commit, user.ap_id}
1160 get_cached_by_ap_id(ap_id)
1163 def get_cached_by_nickname(nickname) do
1164 key = "nickname:#{nickname}"
1166 @cachex.fetch!(:user_cache, key, fn _ ->
1167 case get_or_fetch_by_nickname(nickname) do
1168 {:ok, user} -> {:commit, user}
1169 {:error, _error} -> {:ignore, nil}
1174 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
1175 restrict_to_local = Config.get([:instance, :limit_to_local_content])
1178 is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
1179 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
1181 restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
1182 get_cached_by_nickname(nickname_or_id)
1184 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
1185 get_cached_by_nickname(nickname_or_id)
1192 @spec get_by_nickname(String.t()) :: User.t() | nil
1193 def get_by_nickname(nickname) do
1194 Repo.get_by(User, nickname: nickname) ||
1195 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
1196 Repo.get_by(User, nickname: local_nickname(nickname))
1200 def get_by_email(email), do: Repo.get_by(User, email: email)
1202 def get_by_nickname_or_email(nickname_or_email) do
1203 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
1206 def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname)
1208 def get_or_fetch_by_nickname(nickname) do
1209 with %User{} = user <- get_by_nickname(nickname) do
1213 with [_nick, _domain] <- String.split(nickname, "@"),
1214 {:ok, user} <- fetch_by_nickname(nickname) do
1217 _e -> {:error, "not found " <> nickname}
1222 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
1223 def get_followers_query(%User{} = user, nil) do
1224 User.Query.build(%{followers: user, is_active: true})
1227 def get_followers_query(%User{} = user, page) do
1229 |> get_followers_query(nil)
1230 |> User.Query.paginate(page, 20)
1233 @spec get_followers_query(User.t()) :: Ecto.Query.t()
1234 def get_followers_query(%User{} = user), do: get_followers_query(user, nil)
1236 @spec get_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
1237 def get_followers(%User{} = user, page \\ nil) do
1239 |> get_followers_query(page)
1243 @spec get_external_followers(User.t(), pos_integer() | nil) :: {:ok, list(User.t())}
1244 def get_external_followers(%User{} = user, page \\ nil) do
1246 |> get_followers_query(page)
1247 |> User.Query.build(%{external: true})
1251 def get_followers_ids(%User{} = user, page \\ nil) do
1253 |> get_followers_query(page)
1254 |> select([u], u.id)
1258 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
1259 def get_friends_query(%User{} = user, nil) do
1260 User.Query.build(%{friends: user, deactivated: false})
1263 def get_friends_query(%User{} = user, page) do
1265 |> get_friends_query(nil)
1266 |> User.Query.paginate(page, 20)
1269 @spec get_friends_query(User.t()) :: Ecto.Query.t()
1270 def get_friends_query(%User{} = user), do: get_friends_query(user, nil)
1272 def get_friends(%User{} = user, page \\ nil) do
1274 |> get_friends_query(page)
1278 def get_friends_ap_ids(%User{} = user) do
1280 |> get_friends_query(nil)
1281 |> select([u], u.ap_id)
1285 def get_friends_ids(%User{} = user, page \\ nil) do
1287 |> get_friends_query(page)
1288 |> select([u], u.id)
1292 def increase_note_count(%User{} = user) do
1294 |> where(id: ^user.id)
1295 |> update([u], inc: [note_count: 1])
1297 |> Repo.update_all([])
1299 {1, [user]} -> set_cache(user)
1304 def decrease_note_count(%User{} = user) do
1306 |> where(id: ^user.id)
1309 note_count: fragment("greatest(0, note_count - 1)")
1313 |> Repo.update_all([])
1315 {1, [user]} -> set_cache(user)
1320 def update_note_count(%User{} = user, note_count \\ nil) do
1325 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
1331 |> cast(%{note_count: note_count}, [:note_count])
1332 |> update_and_set_cache()
1335 @spec maybe_fetch_follow_information(User.t()) :: User.t()
1336 def maybe_fetch_follow_information(user) do
1337 with {:ok, user} <- fetch_follow_information(user) do
1341 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
1347 def fetch_follow_information(user) do
1348 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
1350 |> follow_information_changeset(info)
1351 |> update_and_set_cache()
1355 defp follow_information_changeset(user, params) do
1362 :hide_followers_count,
1367 @spec update_follower_count(User.t()) :: {:ok, User.t()}
1368 def update_follower_count(%User{} = user) do
1369 if user.local or !Config.get([:instance, :external_user_synchronization]) do
1370 follower_count = FollowingRelationship.follower_count(user)
1373 |> follow_information_changeset(%{follower_count: follower_count})
1374 |> update_and_set_cache
1376 {:ok, maybe_fetch_follow_information(user)}
1380 @spec update_following_count(User.t()) :: {:ok, User.t()}
1381 def update_following_count(%User{local: false} = user) do
1382 if Config.get([:instance, :external_user_synchronization]) do
1383 {:ok, maybe_fetch_follow_information(user)}
1389 def update_following_count(%User{local: true} = user) do
1390 following_count = FollowingRelationship.following_count(user)
1393 |> follow_information_changeset(%{following_count: following_count})
1394 |> update_and_set_cache()
1397 @spec get_users_from_set([String.t()], keyword()) :: [User.t()]
1398 def get_users_from_set(ap_ids, opts \\ []) do
1399 local_only = Keyword.get(opts, :local_only, true)
1400 criteria = %{ap_id: ap_ids, is_active: true}
1401 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
1403 User.Query.build(criteria)
1407 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
1408 def get_recipients_from_activity(%Activity{recipients: to, actor: actor}) do
1411 query = User.Query.build(%{recipients_from_activity: to, local: true, is_active: true})
1417 @spec mute(User.t(), User.t(), map()) ::
1418 {:ok, list(UserRelationship.t())} | {:error, String.t()}
1419 def mute(%User{} = muter, %User{} = mutee, params \\ %{}) do
1420 notifications? = Map.get(params, :notifications, true)
1421 expires_in = Map.get(params, :expires_in, 0)
1423 with {:ok, user_mute} <- UserRelationship.create_mute(muter, mutee),
1424 {:ok, user_notification_mute} <-
1425 (notifications? && UserRelationship.create_notification_mute(muter, mutee)) ||
1427 if expires_in > 0 do
1428 Pleroma.Workers.MuteExpireWorker.enqueue(
1430 %{"muter_id" => muter.id, "mutee_id" => mutee.id},
1431 schedule_in: expires_in
1435 @cachex.del(:user_cache, "muted_users_ap_ids:#{muter.ap_id}")
1437 {:ok, Enum.filter([user_mute, user_notification_mute], & &1)}
1441 def unmute(%User{} = muter, %User{} = mutee) do
1442 with {:ok, user_mute} <- UserRelationship.delete_mute(muter, mutee),
1443 {:ok, user_notification_mute} <-
1444 UserRelationship.delete_notification_mute(muter, mutee) do
1445 @cachex.del(:user_cache, "muted_users_ap_ids:#{muter.ap_id}")
1446 {:ok, [user_mute, user_notification_mute]}
1450 def unmute(muter_id, mutee_id) do
1451 with {:muter, %User{} = muter} <- {:muter, User.get_by_id(muter_id)},
1452 {:mutee, %User{} = mutee} <- {:mutee, User.get_by_id(mutee_id)} do
1453 unmute(muter, mutee)
1455 {who, result} = error ->
1457 "User.unmute/2 failed. #{who}: #{result}, muter_id: #{muter_id}, mutee_id: #{mutee_id}"
1464 def subscribe(%User{} = subscriber, %User{} = target) do
1465 deny_follow_blocked = Config.get([:user, :deny_follow_blocked])
1467 if blocks?(target, subscriber) and deny_follow_blocked do
1468 {:error, "Could not subscribe: #{target.nickname} is blocking you"}
1470 # Note: the relationship is inverse: subscriber acts as relationship target
1471 UserRelationship.create_inverse_subscription(target, subscriber)
1475 def subscribe(%User{} = subscriber, %{ap_id: ap_id}) do
1476 with %User{} = subscribee <- get_cached_by_ap_id(ap_id) do
1477 subscribe(subscriber, subscribee)
1481 def unsubscribe(%User{} = unsubscriber, %User{} = target) do
1482 # Note: the relationship is inverse: subscriber acts as relationship target
1483 UserRelationship.delete_inverse_subscription(target, unsubscriber)
1486 def unsubscribe(%User{} = unsubscriber, %{ap_id: ap_id}) do
1487 with %User{} = user <- get_cached_by_ap_id(ap_id) do
1488 unsubscribe(unsubscriber, user)
1492 def block(%User{} = blocker, %User{} = blocked) do
1493 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
1495 if following?(blocker, blocked) do
1496 {:ok, blocker, _} = unfollow(blocker, blocked)
1502 # clear any requested follows from both sides as well
1504 case CommonAPI.reject_follow_request(blocked, blocker) do
1505 {:ok, %User{} = updated_blocked} -> updated_blocked
1510 case CommonAPI.reject_follow_request(blocker, blocked) do
1511 {:ok, %User{} = updated_blocker} -> updated_blocker
1515 unsubscribe(blocked, blocker)
1517 unfollowing_blocked = Config.get([:activitypub, :unfollow_blocked], true)
1518 if unfollowing_blocked && following?(blocked, blocker), do: unfollow(blocked, blocker)
1520 {:ok, blocker} = update_follower_count(blocker)
1521 {:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked)
1522 add_to_block(blocker, blocked)
1525 # helper to handle the block given only an actor's AP id
1526 def block(%User{} = blocker, %{ap_id: ap_id}) do
1527 block(blocker, get_cached_by_ap_id(ap_id))
1530 def unblock(%User{} = blocker, %User{} = blocked) do
1531 remove_from_block(blocker, blocked)
1534 # helper to handle the block given only an actor's AP id
1535 def unblock(%User{} = blocker, %{ap_id: ap_id}) do
1536 unblock(blocker, get_cached_by_ap_id(ap_id))
1539 def mutes?(nil, _), do: false
1540 def mutes?(%User{} = user, %User{} = target), do: mutes_user?(user, target)
1542 def mutes_user?(%User{} = user, %User{} = target) do
1543 UserRelationship.mute_exists?(user, target)
1546 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
1547 def muted_notifications?(nil, _), do: false
1549 def muted_notifications?(%User{} = user, %User{} = target),
1550 do: UserRelationship.notification_mute_exists?(user, target)
1552 def blocks?(nil, _), do: false
1554 def blocks?(%User{} = user, %User{} = target) do
1555 blocks_user?(user, target) ||
1556 (blocks_domain?(user, target) and not User.following?(user, target))
1559 def blocks_user?(%User{} = user, %User{} = target) do
1560 UserRelationship.block_exists?(user, target)
1563 def blocks_user?(_, _), do: false
1565 def blocks_domain?(%User{} = user, %User{} = target) do
1566 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)
1567 %{host: host} = URI.parse(target.ap_id)
1568 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1571 def blocks_domain?(_, _), do: false
1573 def subscribed_to?(%User{} = user, %User{} = target) do
1574 # Note: the relationship is inverse: subscriber acts as relationship target
1575 UserRelationship.inverse_subscription_exists?(target, user)
1578 def subscribed_to?(%User{} = user, %{ap_id: ap_id}) do
1579 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1580 subscribed_to?(user, target)
1585 Returns map of outgoing (blocked, muted etc.) relationships' user AP IDs by relation type.
1586 E.g. `outgoing_relationships_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}`
1588 @spec outgoing_relationships_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())}
1589 def outgoing_relationships_ap_ids(_user, []), do: %{}
1591 def outgoing_relationships_ap_ids(nil, _relationship_types), do: %{}
1593 def outgoing_relationships_ap_ids(%User{} = user, relationship_types)
1594 when is_list(relationship_types) do
1597 |> assoc(:outgoing_relationships)
1598 |> join(:inner, [user_rel], u in assoc(user_rel, :target))
1599 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1600 |> select([user_rel, u], [user_rel.relationship_type, fragment("array_agg(?)", u.ap_id)])
1601 |> group_by([user_rel, u], user_rel.relationship_type)
1603 |> Enum.into(%{}, fn [k, v] -> {k, v} end)
1608 fn rel_type -> {rel_type, db_result[rel_type] || []} end
1612 def incoming_relationships_ungrouped_ap_ids(user, relationship_types, ap_ids \\ nil)
1614 def incoming_relationships_ungrouped_ap_ids(_user, [], _ap_ids), do: []
1616 def incoming_relationships_ungrouped_ap_ids(nil, _relationship_types, _ap_ids), do: []
1618 def incoming_relationships_ungrouped_ap_ids(%User{} = user, relationship_types, ap_ids)
1619 when is_list(relationship_types) do
1621 |> assoc(:incoming_relationships)
1622 |> join(:inner, [user_rel], u in assoc(user_rel, :source))
1623 |> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
1624 |> maybe_filter_on_ap_id(ap_ids)
1625 |> select([user_rel, u], u.ap_id)
1630 defp maybe_filter_on_ap_id(query, ap_ids) when is_list(ap_ids) do
1631 where(query, [user_rel, u], u.ap_id in ^ap_ids)
1634 defp maybe_filter_on_ap_id(query, _ap_ids), do: query
1636 def set_activation_async(user, status \\ true) do
1637 BackgroundWorker.enqueue("user_activation", %{"user_id" => user.id, "status" => status})
1640 @spec set_activation([User.t()], boolean()) :: {:ok, User.t()} | {:error, Changeset.t()}
1641 def set_activation(users, status) when is_list(users) do
1642 Repo.transaction(fn ->
1643 for user <- users, do: set_activation(user, status)
1647 @spec set_activation(User.t(), boolean()) :: {:ok, User.t()} | {:error, Changeset.t()}
1648 def set_activation(%User{} = user, status) do
1649 with {:ok, user} <- set_activation_status(user, status) do
1652 |> Enum.filter(& &1.local)
1653 |> Enum.each(&set_cache(update_following_count(&1)))
1655 # Only update local user counts, remote will be update during the next pull.
1658 |> Enum.filter(& &1.local)
1659 |> Enum.each(&do_unfollow(user, &1))
1665 def approve(users) when is_list(users) do
1666 Repo.transaction(fn ->
1667 Enum.map(users, fn user ->
1668 with {:ok, user} <- approve(user), do: user
1673 def approve(%User{is_approved: false} = user) do
1674 with chg <- change(user, is_approved: true),
1675 {:ok, user} <- update_and_set_cache(chg) do
1676 post_register_action(user)
1681 def approve(%User{} = user), do: {:ok, user}
1683 def confirm(users) when is_list(users) do
1684 Repo.transaction(fn ->
1685 Enum.map(users, fn user ->
1686 with {:ok, user} <- confirm(user), do: user
1691 def confirm(%User{is_confirmed: false} = user) do
1692 with chg <- confirmation_changeset(user, set_confirmation: true),
1693 {:ok, user} <- update_and_set_cache(chg) do
1694 post_register_action(user)
1699 def confirm(%User{} = user), do: {:ok, user}
1701 def set_suggestion(users, is_suggested) when is_list(users) do
1702 Repo.transaction(fn ->
1703 Enum.map(users, fn user ->
1704 with {:ok, user} <- set_suggestion(user, is_suggested), do: user
1709 def set_suggestion(%User{is_suggested: is_suggested} = user, is_suggested), do: {:ok, user}
1711 def set_suggestion(%User{} = user, is_suggested) when is_boolean(is_suggested) do
1713 |> change(is_suggested: is_suggested)
1714 |> update_and_set_cache()
1717 def update_notification_settings(%User{} = user, settings) do
1719 |> cast(%{notification_settings: settings}, [])
1720 |> cast_embed(:notification_settings)
1721 |> validate_required([:notification_settings])
1722 |> update_and_set_cache()
1725 @spec purge_user_changeset(User.t()) :: Changeset.t()
1726 def purge_user_changeset(user) do
1727 # "Right to be forgotten"
1728 # https://gdpr.eu/right-to-be-forgotten/
1737 last_refreshed_at: nil,
1738 last_digest_emailed_at: nil,
1745 password_reset_pending: false,
1746 registration_reason: nil,
1747 confirmation_token: nil,
1751 is_moderator: false,
1753 mastofe_settings: nil,
1756 pleroma_settings_store: %{},
1759 is_discoverable: false,
1763 # nickname: preserved
1767 # Purge doesn't delete the user from the database.
1768 # It just nulls all its fields and deactivates it.
1769 # See `User.purge_user_changeset/1` above.
1770 defp purge(%User{} = user) do
1772 |> purge_user_changeset()
1773 |> update_and_set_cache()
1776 def delete(users) when is_list(users) do
1777 for user <- users, do: delete(user)
1780 def delete(%User{} = user) do
1781 # Purge the user immediately
1783 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1786 # *Actually* delete the user from the DB
1787 defp delete_from_db(%User{} = user) do
1788 invalidate_cache(user)
1792 # If the user never finalized their account, it's safe to delete them.
1793 defp maybe_delete_from_db(%User{local: true, is_confirmed: false} = user),
1794 do: delete_from_db(user)
1796 defp maybe_delete_from_db(%User{local: true, is_approved: false} = user),
1797 do: delete_from_db(user)
1799 defp maybe_delete_from_db(user), do: {:ok, user}
1801 def perform(:force_password_reset, user), do: force_password_reset(user)
1803 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1804 def perform(:delete, %User{} = user) do
1805 # Purge the user again, in case perform/2 is called directly
1808 # Remove all relationships
1811 |> Enum.each(fn follower ->
1812 ActivityPub.unfollow(follower, user)
1813 unfollow(follower, user)
1818 |> Enum.each(fn followed ->
1819 ActivityPub.unfollow(user, followed)
1820 unfollow(user, followed)
1823 delete_user_activities(user)
1824 delete_notifications_from_user_activities(user)
1825 delete_outgoing_pending_follow_requests(user)
1827 maybe_delete_from_db(user)
1830 def perform(:set_activation_async, user, status), do: set_activation(user, status)
1832 @spec external_users_query() :: Ecto.Query.t()
1833 def external_users_query do
1841 @spec external_users(keyword()) :: [User.t()]
1842 def external_users(opts \\ []) do
1844 external_users_query()
1845 |> select([u], struct(u, [:id, :ap_id]))
1849 do: where(query, [u], u.id > ^opts[:max_id]),
1854 do: limit(query, ^opts[:limit]),
1860 def delete_notifications_from_user_activities(%User{ap_id: ap_id}) do
1862 |> join(:inner, [n], activity in assoc(n, :activity))
1863 |> where([n, a], fragment("? = ?", a.actor, ^ap_id))
1864 |> Repo.delete_all()
1867 def delete_user_activities(%User{ap_id: ap_id} = user) do
1869 |> Activity.Queries.by_actor()
1870 |> Repo.chunk_stream(50, :batches)
1871 |> Stream.each(fn activities ->
1872 Enum.each(activities, fn activity -> delete_activity(activity, user) end)
1877 defp delete_activity(%{data: %{"type" => "Create", "object" => object}} = activity, user) do
1878 with {_, %Object{}} <- {:find_object, Object.get_by_ap_id(object)},
1879 {:ok, delete_data, _} <- Builder.delete(user, object) do
1880 Pipeline.common_pipeline(delete_data, local: user.local)
1882 {:find_object, nil} ->
1883 # We have the create activity, but not the object, it was probably pruned.
1884 # Insert a tombstone and try again
1885 with {:ok, tombstone_data, _} <- Builder.tombstone(user.ap_id, object),
1886 {:ok, _tombstone} <- Object.create(tombstone_data) do
1887 delete_activity(activity, user)
1891 Logger.error("Could not delete #{object} created by #{activity.data["ap_id"]}")
1892 Logger.error("Error: #{inspect(e)}")
1896 defp delete_activity(%{data: %{"type" => type}} = activity, user)
1897 when type in ["Like", "Announce"] do
1898 {:ok, undo, _} = Builder.undo(user, activity)
1899 Pipeline.common_pipeline(undo, local: user.local)
1902 defp delete_activity(_activity, _user), do: "Doing nothing"
1904 defp delete_outgoing_pending_follow_requests(user) do
1906 |> FollowingRelationship.outgoing_pending_follow_requests_query()
1907 |> Repo.delete_all()
1910 def html_filter_policy(%User{no_rich_text: true}) do
1911 Pleroma.HTML.Scrubber.TwitterText
1914 def html_filter_policy(_), do: Config.get([:markup, :scrub_policy])
1916 def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
1918 def get_or_fetch_by_ap_id(ap_id) do
1919 cached_user = get_cached_by_ap_id(ap_id)
1921 maybe_fetched_user = needs_update?(cached_user) && fetch_by_ap_id(ap_id)
1923 case {cached_user, maybe_fetched_user} do
1924 {_, {:ok, %User{} = user}} ->
1927 {%User{} = user, _} ->
1931 Logger.error("Could not fetch user #{ap_id}, #{inspect(e)}")
1932 {:error, :not_found}
1937 Creates an internal service actor by URI if missing.
1938 Optionally takes nickname for addressing.
1940 @spec get_or_create_service_actor_by_ap_id(String.t(), String.t()) :: User.t() | nil
1941 def get_or_create_service_actor_by_ap_id(uri, nickname) do
1943 case get_cached_by_ap_id(uri) do
1945 with {:error, %{errors: errors}} <- create_service_actor(uri, nickname) do
1946 Logger.error("Cannot create service actor: #{uri}/.\n#{inspect(errors)}")
1950 %User{invisible: false} = user ->
1960 @spec set_invisible(User.t()) :: {:ok, User.t()}
1961 defp set_invisible(user) do
1963 |> change(%{invisible: true})
1964 |> update_and_set_cache()
1967 @spec create_service_actor(String.t(), String.t()) ::
1968 {:ok, User.t()} | {:error, Ecto.Changeset.t()}
1969 defp create_service_actor(uri, nickname) do
1975 follower_address: uri <> "/followers"
1978 |> put_private_key()
1979 |> unique_constraint(:nickname)
1984 def public_key(%{public_key: public_key_pem}) when is_binary(public_key_pem) do
1987 |> :public_key.pem_decode()
1989 |> :public_key.pem_entry_decode()
1994 def public_key(_), do: {:error, "key not found"}
1996 def get_public_key_for_ap_id(ap_id) do
1997 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1998 {:ok, public_key} <- public_key(user) do
2005 def ap_enabled?(%User{local: true}), do: true
2006 def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled
2007 def ap_enabled?(_), do: false
2009 @doc "Gets or fetch a user by uri or nickname."
2010 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
2011 def get_or_fetch("http://" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
2012 def get_or_fetch("https://" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
2013 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
2015 # wait a period of time and return newest version of the User structs
2016 # this is because we have synchronous follow APIs and need to simulate them
2017 # with an async handshake
2018 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
2019 with %User{} = a <- get_cached_by_id(a.id),
2020 %User{} = b <- get_cached_by_id(b.id) do
2027 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
2028 with :ok <- :timer.sleep(timeout),
2029 %User{} = a <- get_cached_by_id(a.id),
2030 %User{} = b <- get_cached_by_id(b.id) do
2037 def parse_bio(bio) when is_binary(bio) and bio != "" do
2039 |> CommonUtils.format_input("text/plain", mentions_format: :full)
2043 def parse_bio(_), do: ""
2045 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
2046 # TODO: get profile URLs other than user.ap_id
2047 profile_urls = [user.ap_id]
2050 |> CommonUtils.format_input("text/plain",
2051 mentions_format: :full,
2052 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
2057 def parse_bio(_, _), do: ""
2059 def tag(user_identifiers, tags) when is_list(user_identifiers) do
2060 Repo.transaction(fn ->
2061 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
2065 def tag(nickname, tags) when is_binary(nickname),
2066 do: tag(get_by_nickname(nickname), tags)
2068 def tag(%User{} = user, tags),
2069 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
2071 def untag(user_identifiers, tags) when is_list(user_identifiers) do
2072 Repo.transaction(fn ->
2073 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
2077 def untag(nickname, tags) when is_binary(nickname),
2078 do: untag(get_by_nickname(nickname), tags)
2080 def untag(%User{} = user, tags),
2081 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
2083 defp update_tags(%User{} = user, new_tags) do
2084 {:ok, updated_user} =
2086 |> change(%{tags: new_tags})
2087 |> update_and_set_cache()
2092 defp normalize_tags(tags) do
2095 |> Enum.map(&String.downcase/1)
2098 def local_nickname_regex do
2099 if Config.get([:instance, :extended_nickname_format]) do
2100 @extended_local_nickname_regex
2102 @strict_local_nickname_regex
2106 def local_nickname(nickname_or_mention) do
2109 |> String.split("@")
2113 def full_nickname(%User{} = user) do
2114 if String.contains?(user.nickname, "@") do
2117 %{host: host} = URI.parse(user.ap_id)
2118 user.nickname <> "@" <> host
2122 def full_nickname(nickname_or_mention),
2123 do: String.trim_leading(nickname_or_mention, "@")
2125 def error_user(ap_id) do
2129 nickname: "erroruser@example.com",
2130 inserted_at: NaiveDateTime.utc_now()
2134 @spec all_superusers() :: [User.t()]
2135 def all_superusers do
2136 User.Query.build(%{super_users: true, local: true, is_active: true})
2140 def muting_reblogs?(%User{} = user, %User{} = target) do
2141 UserRelationship.reblog_mute_exists?(user, target)
2144 def showing_reblogs?(%User{} = user, %User{} = target) do
2145 not muting_reblogs?(user, target)
2149 The function returns a query to get users with no activity for given interval of days.
2150 Inactive users are those who didn't read any notification, or had any activity where
2151 the user is the activity's actor, during `inactivity_threshold` days.
2152 Deactivated users will not appear in this list.
2156 iex> Pleroma.User.list_inactive_users()
2159 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
2160 def list_inactive_users_query(inactivity_threshold \\ 7) do
2161 negative_inactivity_threshold = -inactivity_threshold
2162 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
2163 # Subqueries are not supported in `where` clauses, join gets too complicated.
2164 has_read_notifications =
2165 from(n in Pleroma.Notification,
2166 where: n.seen == true,
2168 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
2171 |> Pleroma.Repo.all()
2173 from(u in Pleroma.User,
2174 left_join: a in Pleroma.Activity,
2175 on: u.ap_id == a.actor,
2176 where: not is_nil(u.nickname),
2177 where: u.is_active == ^true,
2178 where: u.id not in ^has_read_notifications,
2181 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
2182 is_nil(max(a.inserted_at))
2187 Enable or disable email notifications for user
2191 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => false}}, "digest", true)
2192 Pleroma.User{email_notifications: %{"digest" => true}}
2194 iex> Pleroma.User.switch_email_notifications(Pleroma.User{email_notifications: %{"digest" => true}}, "digest", false)
2195 Pleroma.User{email_notifications: %{"digest" => false}}
2197 @spec switch_email_notifications(t(), String.t(), boolean()) ::
2198 {:ok, t()} | {:error, Ecto.Changeset.t()}
2199 def switch_email_notifications(user, type, status) do
2200 User.update_email_notifications(user, %{type => status})
2204 Set `last_digest_emailed_at` value for the user to current time
2206 @spec touch_last_digest_emailed_at(t()) :: t()
2207 def touch_last_digest_emailed_at(user) do
2208 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
2210 {:ok, updated_user} =
2212 |> change(%{last_digest_emailed_at: now})
2213 |> update_and_set_cache()
2218 @spec set_confirmation(User.t(), boolean()) :: {:ok, User.t()} | {:error, Changeset.t()}
2219 def set_confirmation(%User{} = user, bool) do
2221 |> confirmation_changeset(set_confirmation: bool)
2222 |> update_and_set_cache()
2225 def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
2229 def get_mascot(%{mascot: mascot}) when is_nil(mascot) do
2230 # use instance-default
2231 config = Config.get([:assets, :mascots])
2232 default_mascot = Config.get([:assets, :default_mascot])
2233 mascot = Keyword.get(config, default_mascot)
2236 "id" => "default-mascot",
2237 "url" => mascot[:url],
2238 "preview_url" => mascot[:url],
2240 "mime_type" => mascot[:mime_type]
2245 def get_ap_ids_by_nicknames(nicknames) do
2247 where: u.nickname in ^nicknames,
2253 defp put_password_hash(
2254 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
2256 change(changeset, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt(password))
2259 defp put_password_hash(changeset), do: changeset
2261 def is_internal_user?(%User{nickname: nil}), do: true
2262 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
2263 def is_internal_user?(_), do: false
2265 # A hack because user delete activities have a fake id for whatever reason
2266 # TODO: Get rid of this
2267 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
2269 def get_delivered_users_by_object_id(object_id) do
2271 inner_join: delivery in assoc(u, :deliveries),
2272 where: delivery.object_id == ^object_id
2277 def change_email(user, email) do
2279 |> cast(%{email: email}, [:email])
2280 |> maybe_validate_required_email(false)
2281 |> unique_constraint(:email)
2282 |> validate_format(:email, @email_regex)
2283 |> update_and_set_cache()
2286 def alias_users(user) do
2288 |> Enum.map(&User.get_cached_by_ap_id/1)
2289 |> Enum.filter(fn user -> user != nil end)
2292 def add_alias(user, new_alias_user) do
2293 current_aliases = user.also_known_as || []
2294 new_alias_ap_id = new_alias_user.ap_id
2296 if new_alias_ap_id in current_aliases do
2300 |> cast(%{also_known_as: current_aliases ++ [new_alias_ap_id]}, [:also_known_as])
2301 |> update_and_set_cache()
2305 def delete_alias(user, alias_user) do
2306 current_aliases = user.also_known_as || []
2307 alias_ap_id = alias_user.ap_id
2309 if alias_ap_id in current_aliases do
2311 |> cast(%{also_known_as: current_aliases -- [alias_ap_id]}, [:also_known_as])
2312 |> update_and_set_cache()
2314 {:error, :no_such_alias}
2318 # Internal function; public one is `deactivate/2`
2319 defp set_activation_status(user, status) do
2321 |> cast(%{is_active: status}, [:is_active])
2322 |> update_and_set_cache()
2325 def update_banner(user, banner) do
2327 |> cast(%{banner: banner}, [:banner])
2328 |> update_and_set_cache()
2331 def update_background(user, background) do
2333 |> cast(%{background: background}, [:background])
2334 |> update_and_set_cache()
2337 def validate_fields(changeset, remote? \\ false) do
2338 limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
2339 limit = Config.get([:instance, limit_name], 0)
2342 |> validate_length(:fields, max: limit)
2343 |> validate_change(:fields, fn :fields, fields ->
2344 if Enum.all?(fields, &valid_field?/1) do
2352 defp valid_field?(%{"name" => name, "value" => value}) do
2353 name_limit = Config.get([:instance, :account_field_name_length], 255)
2354 value_limit = Config.get([:instance, :account_field_value_length], 255)
2356 is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
2357 String.length(value) <= value_limit
2360 defp valid_field?(_), do: false
2362 defp truncate_field(%{"name" => name, "value" => value}) do
2364 String.split_at(name, Config.get([:instance, :account_field_name_length], 255))
2367 String.split_at(value, Config.get([:instance, :account_field_value_length], 255))
2369 %{"name" => name, "value" => value}
2372 def admin_api_update(user, params) do
2379 |> update_and_set_cache()
2382 @doc "Signs user out of all applications"
2383 def global_sign_out(user) do
2384 OAuth.Authorization.delete_user_authorizations(user)
2385 OAuth.Token.delete_user_tokens(user)
2388 def mascot_update(user, url) do
2390 |> cast(%{mascot: url}, [:mascot])
2391 |> validate_required([:mascot])
2392 |> update_and_set_cache()
2395 def mastodon_settings_update(user, settings) do
2397 |> cast(%{mastofe_settings: settings}, [:mastofe_settings])
2398 |> validate_required([:mastofe_settings])
2399 |> update_and_set_cache()
2402 @spec confirmation_changeset(User.t(), keyword()) :: Changeset.t()
2403 def confirmation_changeset(user, set_confirmation: confirmed?) do
2408 confirmation_token: nil
2412 is_confirmed: false,
2413 confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
2417 cast(user, params, [:is_confirmed, :confirmation_token])
2420 @spec approval_changeset(User.t(), keyword()) :: Changeset.t()
2421 def approval_changeset(user, set_approval: approved?) do
2422 cast(user, %{is_approved: approved?}, [:is_approved])
2425 @spec add_pinned_object_id(User.t(), String.t()) :: {:ok, User.t()} | {:error, term()}
2426 def add_pinned_object_id(%User{} = user, object_id) do
2427 if !user.pinned_objects[object_id] do
2428 params = %{pinned_objects: Map.put(user.pinned_objects, object_id, NaiveDateTime.utc_now())}
2431 |> cast(params, [:pinned_objects])
2432 |> validate_change(:pinned_objects, fn :pinned_objects, pinned_objects ->
2433 max_pinned_statuses = Config.get([:instance, :max_pinned_statuses], 0)
2435 if Enum.count(pinned_objects) <= max_pinned_statuses do
2438 [pinned_objects: "You have already pinned the maximum number of statuses"]
2444 |> update_and_set_cache()
2447 @spec remove_pinned_object_id(User.t(), String.t()) :: {:ok, t()} | {:error, term()}
2448 def remove_pinned_object_id(%User{} = user, object_id) do
2451 %{pinned_objects: Map.delete(user.pinned_objects, object_id)},
2454 |> update_and_set_cache()
2457 def update_email_notifications(user, settings) do
2458 email_notifications =
2459 user.email_notifications
2460 |> Map.merge(settings)
2461 |> Map.take(["digest"])
2463 params = %{email_notifications: email_notifications}
2464 fields = [:email_notifications]
2467 |> cast(params, fields)
2468 |> validate_required(fields)
2469 |> update_and_set_cache()
2472 defp set_domain_blocks(user, domain_blocks) do
2473 params = %{domain_blocks: domain_blocks}
2476 |> cast(params, [:domain_blocks])
2477 |> validate_required([:domain_blocks])
2478 |> update_and_set_cache()
2481 def block_domain(user, domain_blocked) do
2482 set_domain_blocks(user, Enum.uniq([domain_blocked | user.domain_blocks]))
2485 def unblock_domain(user, domain_blocked) do
2486 set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked))
2489 @spec add_to_block(User.t(), User.t()) ::
2490 {:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()}
2491 defp add_to_block(%User{} = user, %User{} = blocked) do
2492 with {:ok, relationship} <- UserRelationship.create_block(user, blocked) do
2493 @cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
2498 @spec add_to_block(User.t(), User.t()) ::
2499 {:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()}
2500 defp remove_from_block(%User{} = user, %User{} = blocked) do
2501 with {:ok, relationship} <- UserRelationship.delete_block(user, blocked) do
2502 @cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
2507 def set_invisible(user, invisible) do
2508 params = %{invisible: invisible}
2511 |> cast(params, [:invisible])
2512 |> validate_required([:invisible])
2513 |> update_and_set_cache()
2516 def sanitize_html(%User{} = user) do
2517 sanitize_html(user, nil)
2520 # User data that mastodon isn't filtering (treated as plaintext):
2523 def sanitize_html(%User{} = user, filter) do
2525 Enum.map(user.fields, fn %{"name" => name, "value" => value} ->
2528 "value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
2533 |> Map.put(:bio, HTML.filter_tags(user.bio, filter))
2534 |> Map.put(:fields, fields)
2537 def get_host(%User{ap_id: ap_id} = _user) do
2538 URI.parse(ap_id).host
2541 def update_last_active_at(%__MODULE__{local: true} = user) do
2543 |> cast(%{last_active_at: NaiveDateTime.utc_now()}, [:last_active_at])
2544 |> update_and_set_cache()
2547 def active_user_count(days \\ 30) do
2548 active_after = Timex.shift(NaiveDateTime.utc_now(), days: -days)
2551 |> where([u], u.last_active_at >= ^active_after)
2552 |> where([u], u.local == true)
2553 |> Repo.aggregate(:count)
2556 def update_last_status_at(user) do
2558 |> where(id: ^user.id)
2559 |> update([u], set: [last_status_at: fragment("NOW()")])
2561 |> Repo.update_all([])
2563 {1, [user]} -> set_cache(user)
2568 defp maybe_load_followed_hashtags(%User{followed_hashtags: follows} = user)
2569 when is_list(follows),
2572 defp maybe_load_followed_hashtags(%User{} = user) do
2573 followed_hashtags = HashtagFollow.get_by_user(user)
2574 %{user | followed_hashtags: followed_hashtags}
2577 def followed_hashtags(%User{followed_hashtags: follows})
2578 when is_list(follows),
2581 def followed_hashtags(%User{} = user) do
2584 |> maybe_load_followed_hashtags()
2587 user.followed_hashtags
2590 def follow_hashtag(%User{} = user, %Hashtag{} = hashtag) do
2591 Logger.debug("Follow hashtag #{hashtag.name} for user #{user.nickname}")
2592 user = maybe_load_followed_hashtags(user)
2594 with {:ok, _} <- HashtagFollow.new(user, hashtag),
2595 follows <- HashtagFollow.get_by_user(user),
2596 %User{} = user <- user |> Map.put(:followed_hashtags, follows) do
2602 def unfollow_hashtag(%User{} = user, %Hashtag{} = hashtag) do
2603 Logger.debug("Unfollow hashtag #{hashtag.name} for user #{user.nickname}")
2604 user = maybe_load_followed_hashtags(user)
2606 with {:ok, _} <- HashtagFollow.delete(user, hashtag),
2607 follows <- HashtagFollow.get_by_user(user),
2608 %User{} = user <- user |> Map.put(:followed_hashtags, follows) do
2614 def following_hashtag?(%User{} = user, %Hashtag{} = hashtag) do
2615 not is_nil(HashtagFollow.get(user, hashtag))