1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.User do
13 alias Pleroma.Activity
14 alias Pleroma.Conversation.Participation
15 alias Pleroma.Delivery
17 alias Pleroma.Notification
19 alias Pleroma.Registration
21 alias Pleroma.RepoStreamer
24 alias Pleroma.Web.ActivityPub.ActivityPub
25 alias Pleroma.Web.ActivityPub.Utils
26 alias Pleroma.Web.CommonAPI
27 alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
28 alias Pleroma.Web.OAuth
29 alias Pleroma.Web.RelMe
30 alias Pleroma.Workers.BackgroundWorker
34 @type t :: %__MODULE__{}
36 @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
38 # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
39 @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])?)*$/
41 @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
42 @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
46 field(:email, :string)
48 field(:nickname, :string)
49 field(:password_hash, :string)
50 field(:password, :string, virtual: true)
51 field(:password_confirmation, :string, virtual: true)
53 field(:following, {:array, :string}, default: [])
54 field(:ap_id, :string)
56 field(:local, :boolean, default: true)
57 field(:follower_address, :string)
58 field(:following_address, :string)
59 field(:search_rank, :float, virtual: true)
60 field(:search_type, :integer, virtual: true)
61 field(:tags, {:array, :string}, default: [])
62 field(:last_refreshed_at, :naive_datetime_usec)
63 field(:last_digest_emailed_at, :naive_datetime)
64 has_many(:notifications, Notification)
65 has_many(:registrations, Registration)
66 has_many(:deliveries, Delivery)
67 embeds_one(:info, User.Info)
72 def auth_active?(%User{info: %User.Info{confirmation_pending: true}}),
73 do: !Pleroma.Config.get([:instance, :account_activation_required])
75 def auth_active?(%User{}), do: true
77 def visible_for?(user, for_user \\ nil)
79 def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
81 def visible_for?(%User{} = user, for_user) do
82 auth_active?(user) || superuser?(for_user)
85 def visible_for?(_, _), do: false
87 def superuser?(%User{local: true, info: %User.Info{is_admin: true}}), do: true
88 def superuser?(%User{local: true, info: %User.Info{is_moderator: true}}), do: true
89 def superuser?(_), do: false
91 def avatar_url(user, options \\ []) do
93 %{"url" => [%{"href" => href} | _]} -> href
94 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
98 def banner_url(user, options \\ []) do
99 case user.info.banner do
100 %{"url" => [%{"href" => href} | _]} -> href
101 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
105 def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url
106 def profile_url(%User{ap_id: ap_id}), do: ap_id
107 def profile_url(_), do: nil
109 def ap_id(%User{nickname: nickname}), do: "#{Web.base_url()}/users/#{nickname}"
111 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
112 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
114 @spec ap_following(User.t()) :: Sring.t()
115 def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
116 def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
118 def user_info(%User{} = user, args \\ %{}) do
120 Map.get(args, :following_count, user.info.following_count || following_count(user))
122 follower_count = Map.get(args, :follower_count, user.info.follower_count)
125 note_count: user.info.note_count,
126 locked: user.info.locked,
127 confirmation_pending: user.info.confirmation_pending,
128 default_scope: user.info.default_scope
130 |> Map.put(:following_count, following_count)
131 |> Map.put(:follower_count, follower_count)
134 def follow_state(%User{} = user, %User{} = target) do
135 case Utils.fetch_latest_follow(user, target) do
136 %{data: %{"state" => state}} -> state
137 # Ideally this would be nil, but then Cachex does not commit the value
142 def get_cached_follow_state(user, target) do
143 key = "follow_state:#{user.ap_id}|#{target.ap_id}"
144 Cachex.fetch!(:user_cache, key, fn _ -> {:commit, follow_state(user, target)} end)
147 @spec set_follow_state_cache(String.t(), String.t(), String.t()) :: {:ok | :error, boolean()}
148 def set_follow_state_cache(user_ap_id, target_ap_id, state) do
149 Cachex.put(:user_cache, "follow_state:#{user_ap_id}|#{target_ap_id}", state)
152 def set_info_cache(user, args) do
153 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user, args))
156 @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
157 def restrict_deactivated(query) do
159 where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info)
163 def following_count(%User{following: []}), do: 0
165 def following_count(%User{} = user) do
167 |> get_friends_query()
168 |> Repo.aggregate(:count, :id)
171 defp truncate_if_exists(params, key, max_length) do
172 if Map.has_key?(params, key) and is_binary(params[key]) do
173 {value, _chopped} = String.split_at(params[key], max_length)
174 Map.put(params, key, value)
180 def remote_user_creation(params) do
181 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
182 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
186 |> Map.put(:info, params[:info] || %{})
187 |> truncate_if_exists(:name, name_limit)
188 |> truncate_if_exists(:bio, bio_limit)
192 |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
193 |> validate_required([:name, :ap_id])
194 |> unique_constraint(:nickname)
195 |> validate_format(:nickname, @email_regex)
196 |> validate_length(:bio, max: bio_limit)
197 |> validate_length(:name, max: name_limit)
198 |> change_info(&User.Info.remote_user_creation(&1, params[:info]))
200 case params[:info][:source_data] do
201 %{"followers" => followers, "following" => following} ->
203 |> put_change(:follower_address, followers)
204 |> put_change(:following_address, following)
207 followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
208 put_change(changeset, :follower_address, followers)
212 def update_changeset(struct, params \\ %{}) do
213 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
214 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
217 |> cast(params, [:bio, :name, :avatar, :following])
218 |> unique_constraint(:nickname)
219 |> validate_format(:nickname, local_nickname_regex())
220 |> validate_length(:bio, max: bio_limit)
221 |> validate_length(:name, min: 1, max: name_limit)
224 def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
225 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
226 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
228 params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
239 |> unique_constraint(:nickname)
240 |> validate_format(:nickname, local_nickname_regex())
241 |> validate_length(:bio, max: bio_limit)
242 |> validate_length(:name, max: name_limit)
243 |> change_info(&User.Info.user_upgrade(&1, params[:info], remote?))
246 def password_update_changeset(struct, params) do
248 |> cast(params, [:password, :password_confirmation])
249 |> validate_required([:password, :password_confirmation])
250 |> validate_confirmation(:password)
252 |> put_embed(:info, User.Info.set_password_reset_pending(struct.info, false))
255 @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
256 def reset_password(%User{id: user_id} = user, data) do
259 |> Multi.update(:user, password_update_changeset(user, data))
260 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
261 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
263 case Repo.transaction(multi) do
264 {:ok, %{user: user} = _} -> set_cache(user)
265 {:error, _, changeset, _} -> {:error, changeset}
269 def force_password_reset_async(user) do
270 BackgroundWorker.enqueue("force_password_reset", %{"user_id" => user.id})
273 @spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
274 def force_password_reset(user) do
275 info_cng = User.Info.set_password_reset_pending(user.info, true)
279 |> put_embed(:info, info_cng)
280 |> update_and_set_cache()
283 def register_changeset(struct, params \\ %{}, opts \\ []) do
284 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
285 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
288 if is_nil(opts[:need_confirmation]) do
289 Pleroma.Config.get([:instance, :account_activation_required])
291 opts[:need_confirmation]
295 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
296 |> validate_required([:name, :nickname, :password, :password_confirmation])
297 |> validate_confirmation(:password)
298 |> unique_constraint(:email)
299 |> unique_constraint(:nickname)
300 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
301 |> validate_format(:nickname, local_nickname_regex())
302 |> validate_format(:email, @email_regex)
303 |> validate_length(:bio, max: bio_limit)
304 |> validate_length(:name, min: 1, max: name_limit)
305 |> change_info(&User.Info.confirmation_changeset(&1, need_confirmation: need_confirmation?))
306 |> maybe_validate_required_email(opts[:external])
309 |> unique_constraint(:ap_id)
310 |> put_following_and_follower_address()
313 def maybe_validate_required_email(changeset, true), do: changeset
314 def maybe_validate_required_email(changeset, _), do: validate_required(changeset, [:email])
316 defp put_ap_id(changeset) do
317 ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
318 put_change(changeset, :ap_id, ap_id)
321 defp put_following_and_follower_address(changeset) do
322 followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
325 |> put_change(:following, [followers])
326 |> put_change(:follower_address, followers)
329 defp autofollow_users(user) do
330 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
333 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
336 follow_all(user, autofollowed_users)
339 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
340 def register(%Ecto.Changeset{} = changeset) do
341 with {:ok, user} <- Repo.insert(changeset) do
342 post_register_action(user)
346 def post_register_action(%User{} = user) do
347 with {:ok, user} <- autofollow_users(user),
348 {:ok, user} <- set_cache(user),
349 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
350 {:ok, _} <- try_send_confirmation_email(user) do
355 def try_send_confirmation_email(%User{} = user) do
356 if user.info.confirmation_pending &&
357 Pleroma.Config.get([:instance, :account_activation_required]) do
359 |> Pleroma.Emails.UserEmail.account_confirmation_email()
360 |> Pleroma.Emails.Mailer.deliver_async()
368 def needs_update?(%User{local: true}), do: false
370 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
372 def needs_update?(%User{local: false} = user) do
373 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
376 def needs_update?(_), do: true
378 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
379 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
383 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
384 follow(follower, followed)
387 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
388 if not ap_enabled?(followed) do
389 follow(follower, followed)
395 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
396 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
397 def follow_all(follower, followeds) do
400 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
401 |> Enum.map(fn %{follower_address: fa} -> fa end)
405 where: u.id == ^follower.id,
410 "array(select distinct unnest (array_cat(?, ?)))",
419 {1, [follower]} = Repo.update_all(q, [])
421 Enum.each(followeds, &update_follower_count/1)
426 def follow(%User{} = follower, %User{info: info} = followed) do
427 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
428 ap_followers = followed.follower_address
432 {:error, "Could not follow user: You are deactivated."}
434 deny_follow_blocked and blocks?(followed, follower) ->
435 {:error, "Could not follow user: #{followed.nickname} blocked you."}
440 where: u.id == ^follower.id,
441 update: [push: [following: ^ap_followers]],
445 {1, [follower]} = Repo.update_all(q, [])
447 follower = maybe_update_following_count(follower)
449 {:ok, _} = update_follower_count(followed)
455 def unfollow(%User{} = follower, %User{} = followed) do
456 ap_followers = followed.follower_address
458 if following?(follower, followed) and follower.ap_id != followed.ap_id do
461 where: u.id == ^follower.id,
462 update: [pull: [following: ^ap_followers]],
466 {1, [follower]} = Repo.update_all(q, [])
468 follower = maybe_update_following_count(follower)
470 {:ok, followed} = update_follower_count(followed)
474 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
476 {:error, "Not subscribed!"}
480 @spec following?(User.t(), User.t()) :: boolean
481 def following?(%User{} = follower, %User{} = followed) do
482 Enum.member?(follower.following, followed.follower_address)
485 def locked?(%User{} = user) do
486 user.info.locked || false
490 Repo.get_by(User, id: id)
493 def get_by_ap_id(ap_id) do
494 Repo.get_by(User, ap_id: ap_id)
497 def get_all_by_ap_id(ap_ids) do
498 from(u in __MODULE__,
499 where: u.ap_id in ^ap_ids
504 def get_all_by_ids(ids) do
505 from(u in __MODULE__, where: u.id in ^ids)
509 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
510 # of the ap_id and the domain and tries to get that user
511 def get_by_guessed_nickname(ap_id) do
512 domain = URI.parse(ap_id).host
513 name = List.last(String.split(ap_id, "/"))
514 nickname = "#{name}@#{domain}"
516 get_cached_by_nickname(nickname)
519 def set_cache({:ok, user}), do: set_cache(user)
520 def set_cache({:error, err}), do: {:error, err}
522 def set_cache(%User{} = user) do
523 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
524 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
525 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
529 def update_and_set_cache(changeset) do
530 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
535 def invalidate_cache(user) do
536 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
537 Cachex.del(:user_cache, "nickname:#{user.nickname}")
538 Cachex.del(:user_cache, "user_info:#{user.id}")
541 def get_cached_by_ap_id(ap_id) do
542 key = "ap_id:#{ap_id}"
543 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
546 def get_cached_by_id(id) do
550 Cachex.fetch!(:user_cache, key, fn _ ->
554 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
555 {:commit, user.ap_id}
561 get_cached_by_ap_id(ap_id)
564 def get_cached_by_nickname(nickname) do
565 key = "nickname:#{nickname}"
567 Cachex.fetch!(:user_cache, key, fn ->
568 case get_or_fetch_by_nickname(nickname) do
569 {:ok, user} -> {:commit, user}
570 {:error, _error} -> {:ignore, nil}
575 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
576 restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
579 is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
580 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
582 restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
583 get_cached_by_nickname(nickname_or_id)
585 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
586 get_cached_by_nickname(nickname_or_id)
593 def get_by_nickname(nickname) do
594 Repo.get_by(User, nickname: nickname) ||
595 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
596 Repo.get_by(User, nickname: local_nickname(nickname))
600 def get_by_email(email), do: Repo.get_by(User, email: email)
602 def get_by_nickname_or_email(nickname_or_email) do
603 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
606 def get_cached_user_info(user) do
607 key = "user_info:#{user.id}"
608 Cachex.fetch!(:user_cache, key, fn -> user_info(user) end)
611 def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname)
613 def get_or_fetch_by_nickname(nickname) do
614 with %User{} = user <- get_by_nickname(nickname) do
618 with [_nick, _domain] <- String.split(nickname, "@"),
619 {:ok, user} <- fetch_by_nickname(nickname) do
620 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
621 fetch_initial_posts(user)
626 _e -> {:error, "not found " <> nickname}
631 @doc "Fetch some posts when the user has just been federated with"
632 def fetch_initial_posts(user) do
633 BackgroundWorker.enqueue("fetch_initial_posts", %{"user_id" => user.id})
636 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
637 def get_followers_query(%User{} = user, nil) do
638 User.Query.build(%{followers: user, deactivated: false})
641 def get_followers_query(user, page) do
643 |> get_followers_query(nil)
644 |> User.Query.paginate(page, 20)
647 @spec get_followers_query(User.t()) :: Ecto.Query.t()
648 def get_followers_query(user), do: get_followers_query(user, nil)
650 @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
651 def get_followers(user, page \\ nil) do
653 |> get_followers_query(page)
657 @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
658 def get_external_followers(user, page \\ nil) do
660 |> get_followers_query(page)
661 |> User.Query.build(%{external: true})
665 def get_followers_ids(user, page \\ nil) do
667 |> get_followers_query(page)
672 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
673 def get_friends_query(%User{} = user, nil) do
674 User.Query.build(%{friends: user, deactivated: false})
677 def get_friends_query(user, page) do
679 |> get_friends_query(nil)
680 |> User.Query.paginate(page, 20)
683 @spec get_friends_query(User.t()) :: Ecto.Query.t()
684 def get_friends_query(user), do: get_friends_query(user, nil)
686 def get_friends(user, page \\ nil) do
688 |> get_friends_query(page)
692 def get_friends_ids(user, page \\ nil) do
694 |> get_friends_query(page)
699 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
700 def get_follow_requests(%User{} = user) do
702 |> Activity.follow_requests_for_actor()
703 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
704 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
705 |> group_by([a, u], u.id)
710 def increase_note_count(%User{} = user) do
712 |> where(id: ^user.id)
717 "safe_jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
724 |> Repo.update_all([])
726 {1, [user]} -> set_cache(user)
731 def decrease_note_count(%User{} = user) do
733 |> where(id: ^user.id)
738 "safe_jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
745 |> Repo.update_all([])
747 {1, [user]} -> set_cache(user)
752 def update_note_count(%User{} = user) do
756 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
761 update_info(user, &User.Info.set_note_count(&1, note_count))
764 def update_mascot(user, url) do
766 User.Info.mascot_update(
773 |> put_embed(:info, info_changeset)
774 |> update_and_set_cache()
777 @spec maybe_fetch_follow_information(User.t()) :: User.t()
778 def maybe_fetch_follow_information(user) do
779 with {:ok, user} <- fetch_follow_information(user) do
783 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
789 def fetch_follow_information(user) do
790 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
791 update_info(user, &User.Info.follow_information_update(&1, info))
795 def update_follower_count(%User{} = user) do
796 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
797 follower_count_query =
798 User.Query.build(%{followers: user, deactivated: false})
799 |> select([u], %{count: count(u.id)})
802 |> where(id: ^user.id)
803 |> join(:inner, [u], s in subquery(follower_count_query))
808 "safe_jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
815 |> Repo.update_all([])
817 {1, [user]} -> set_cache(user)
821 {:ok, maybe_fetch_follow_information(user)}
825 @spec maybe_update_following_count(User.t()) :: User.t()
826 def maybe_update_following_count(%User{local: false} = user) do
827 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
828 maybe_fetch_follow_information(user)
834 def maybe_update_following_count(user), do: user
836 def set_unread_conversation_count(%User{local: true} = user) do
837 unread_query = Participation.unread_conversation_count_for_user(user)
840 |> join(:inner, [u], p in subquery(unread_query))
845 "jsonb_set(?, '{unread_conversation_count}', ?::varchar::jsonb, true)",
851 |> where([u], u.id == ^user.id)
853 |> Repo.update_all([])
855 {1, [user]} -> set_cache(user)
860 def set_unread_conversation_count(_), do: :noop
862 def increment_unread_conversation_count(conversation, %User{local: true} = user) do
864 Participation.unread_conversation_count_for_user(user)
865 |> where([p], p.conversation_id == ^conversation.id)
868 |> join(:inner, [u], p in subquery(unread_query))
873 "jsonb_set(?, '{unread_conversation_count}', (coalesce((?->>'unread_conversation_count')::int, 0) + 1)::varchar::jsonb, true)",
879 |> where([u], u.id == ^user.id)
880 |> where([u, p], p.count == 0)
882 |> Repo.update_all([])
884 {1, [user]} -> set_cache(user)
889 def increment_unread_conversation_count(_, _), do: :noop
891 def remove_duplicated_following(%User{following: following} = user) do
892 uniq_following = Enum.uniq(following)
894 if length(following) == length(uniq_following) do
898 |> update_changeset(%{following: uniq_following})
899 |> update_and_set_cache()
903 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
904 def get_users_from_set(ap_ids, local_only \\ true) do
905 criteria = %{ap_id: ap_ids, deactivated: false}
906 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
908 User.Query.build(criteria)
912 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
913 def get_recipients_from_activity(%Activity{recipients: to}) do
914 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
918 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
919 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
920 update_info(muter, &User.Info.add_to_mutes(&1, ap_id, notifications?))
923 def unmute(muter, %{ap_id: ap_id}) do
924 update_info(muter, &User.Info.remove_from_mutes(&1, ap_id))
927 def subscribe(subscriber, %{ap_id: ap_id}) do
928 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
929 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
931 if blocks?(subscribed, subscriber) and deny_follow_blocked do
932 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
934 update_info(subscribed, &User.Info.add_to_subscribers(&1, subscriber.ap_id))
939 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
940 with %User{} = user <- get_cached_by_ap_id(ap_id) do
941 update_info(user, &User.Info.remove_from_subscribers(&1, unsubscriber.ap_id))
945 def block(blocker, %User{ap_id: ap_id} = blocked) do
946 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
948 if following?(blocker, blocked) do
949 {:ok, blocker, _} = unfollow(blocker, blocked)
955 # clear any requested follows as well
957 case CommonAPI.reject_follow_request(blocked, blocker) do
958 {:ok, %User{} = updated_blocked} -> updated_blocked
963 if subscribed_to?(blocked, blocker) do
964 {:ok, blocker} = unsubscribe(blocked, blocker)
970 if following?(blocked, blocker), do: unfollow(blocked, blocker)
972 {:ok, blocker} = update_follower_count(blocker)
974 update_info(blocker, &User.Info.add_to_block(&1, ap_id))
977 # helper to handle the block given only an actor's AP id
978 def block(blocker, %{ap_id: ap_id}) do
979 block(blocker, get_cached_by_ap_id(ap_id))
982 def unblock(blocker, %{ap_id: ap_id}) do
983 update_info(blocker, &User.Info.remove_from_block(&1, ap_id))
986 def mutes?(nil, _), do: false
987 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
989 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
990 def muted_notifications?(nil, _), do: false
992 def muted_notifications?(user, %{ap_id: ap_id}),
993 do: Enum.member?(user.info.muted_notifications, ap_id)
995 def blocks?(%User{} = user, %User{} = target) do
996 blocks_ap_id?(user, target) || blocks_domain?(user, target)
999 def blocks?(nil, _), do: false
1001 def blocks_ap_id?(%User{} = user, %User{} = target) do
1002 Enum.member?(user.info.blocks, target.ap_id)
1005 def blocks_ap_id?(_, _), do: false
1007 def blocks_domain?(%User{} = user, %User{} = target) do
1008 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
1009 %{host: host} = URI.parse(target.ap_id)
1010 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1013 def blocks_domain?(_, _), do: false
1015 def subscribed_to?(user, %{ap_id: ap_id}) do
1016 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1017 Enum.member?(target.info.subscribers, user.ap_id)
1021 @spec muted_users(User.t()) :: [User.t()]
1022 def muted_users(user) do
1023 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
1027 @spec blocked_users(User.t()) :: [User.t()]
1028 def blocked_users(user) do
1029 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
1033 @spec subscribers(User.t()) :: [User.t()]
1034 def subscribers(user) do
1035 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
1039 def block_domain(user, domain) do
1040 update_info(user, &User.Info.add_to_domain_block(&1, domain))
1043 def unblock_domain(user, domain) do
1044 update_info(user, &User.Info.remove_from_domain_block(&1, domain))
1047 def deactivate_async(user, status \\ true) do
1048 BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
1051 def deactivate(user, status \\ true)
1053 def deactivate(users, status) when is_list(users) do
1054 Repo.transaction(fn ->
1055 for user <- users, do: deactivate(user, status)
1059 def deactivate(%User{} = user, status) do
1060 with {:ok, user} <- update_info(user, &User.Info.set_activation_status(&1, status)) do
1061 Enum.each(get_followers(user), &invalidate_cache/1)
1062 Enum.each(get_friends(user), &update_follower_count/1)
1068 def update_notification_settings(%User{} = user, settings \\ %{}) do
1069 update_info(user, &User.Info.update_notification_settings(&1, settings))
1072 def delete(users) when is_list(users) do
1073 for user <- users, do: delete(user)
1076 def delete(%User{} = user) do
1077 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1080 def perform(:force_password_reset, user), do: force_password_reset(user)
1082 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1083 def perform(:delete, %User{} = user) do
1084 {:ok, _user} = ActivityPub.delete(user)
1086 # Remove all relationships
1089 |> Enum.each(fn follower ->
1090 ActivityPub.unfollow(follower, user)
1091 unfollow(follower, user)
1096 |> Enum.each(fn followed ->
1097 ActivityPub.unfollow(user, followed)
1098 unfollow(user, followed)
1101 delete_user_activities(user)
1102 invalidate_cache(user)
1106 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1107 def perform(:fetch_initial_posts, %User{} = user) do
1108 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1110 # Insert all the posts in reverse order, so they're in the right order on the timeline
1111 user.info.source_data["outbox"]
1112 |> Utils.fetch_ordered_collection(pages)
1114 |> Enum.each(&Pleroma.Web.Federator.incoming_ap_doc/1)
1117 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1119 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1120 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1121 when is_list(blocked_identifiers) do
1123 blocked_identifiers,
1124 fn blocked_identifier ->
1125 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1126 {:ok, blocker} <- block(blocker, blocked),
1127 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1131 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1138 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1139 def perform(:follow_import, %User{} = follower, followed_identifiers)
1140 when is_list(followed_identifiers) do
1142 followed_identifiers,
1143 fn followed_identifier ->
1144 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1145 {:ok, follower} <- maybe_direct_follow(follower, followed),
1146 {:ok, _} <- ActivityPub.follow(follower, followed) do
1150 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1157 @spec external_users_query() :: Ecto.Query.t()
1158 def external_users_query do
1166 @spec external_users(keyword()) :: [User.t()]
1167 def external_users(opts \\ []) do
1169 external_users_query()
1170 |> select([u], struct(u, [:id, :ap_id, :info]))
1174 do: where(query, [u], u.id > ^opts[:max_id]),
1179 do: limit(query, ^opts[:limit]),
1185 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1186 BackgroundWorker.enqueue("blocks_import", %{
1187 "blocker_id" => blocker.id,
1188 "blocked_identifiers" => blocked_identifiers
1192 def follow_import(%User{} = follower, followed_identifiers)
1193 when is_list(followed_identifiers) do
1194 BackgroundWorker.enqueue("follow_import", %{
1195 "follower_id" => follower.id,
1196 "followed_identifiers" => followed_identifiers
1200 def delete_user_activities(%User{ap_id: ap_id}) do
1202 |> Activity.Queries.by_actor()
1203 |> RepoStreamer.chunk_stream(50)
1204 |> Stream.each(fn activities -> Enum.each(activities, &delete_activity/1) end)
1208 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1210 |> Object.normalize()
1211 |> ActivityPub.delete()
1214 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1215 object = Object.normalize(activity)
1218 |> get_cached_by_ap_id()
1219 |> ActivityPub.unlike(object)
1222 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1223 object = Object.normalize(activity)
1226 |> get_cached_by_ap_id()
1227 |> ActivityPub.unannounce(object)
1230 defp delete_activity(_activity), do: "Doing nothing"
1232 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1233 Pleroma.HTML.Scrubber.TwitterText
1236 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1238 def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
1240 def get_or_fetch_by_ap_id(ap_id) do
1241 user = get_cached_by_ap_id(ap_id)
1243 if !is_nil(user) and !needs_update?(user) do
1246 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1247 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1249 resp = fetch_by_ap_id(ap_id)
1251 if should_fetch_initial do
1252 with {:ok, %User{} = user} <- resp do
1253 fetch_initial_posts(user)
1261 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1262 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1263 with %User{} = user <- get_cached_by_ap_id(uri) do
1268 %User{info: %User.Info{}}
1269 |> cast(%{}, [:ap_id, :nickname, :local])
1270 |> put_change(:ap_id, uri)
1271 |> put_change(:nickname, nickname)
1272 |> put_change(:local, true)
1273 |> put_change(:follower_address, uri <> "/followers")
1281 def public_key_from_info(%{
1282 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1286 |> :public_key.pem_decode()
1288 |> :public_key.pem_entry_decode()
1293 def public_key_from_info(_), do: {:error, "not found key"}
1295 def get_public_key_for_ap_id(ap_id) do
1296 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1297 {:ok, public_key} <- public_key_from_info(user.info) do
1304 defp blank?(""), do: nil
1305 defp blank?(n), do: n
1307 def insert_or_update_user(data) do
1309 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1310 |> remote_user_creation()
1311 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1315 def ap_enabled?(%User{local: true}), do: true
1316 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1317 def ap_enabled?(_), do: false
1319 @doc "Gets or fetch a user by uri or nickname."
1320 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1321 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1322 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1324 # wait a period of time and return newest version of the User structs
1325 # this is because we have synchronous follow APIs and need to simulate them
1326 # with an async handshake
1327 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1328 with %User{} = a <- get_cached_by_id(a.id),
1329 %User{} = b <- get_cached_by_id(b.id) do
1336 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1337 with :ok <- :timer.sleep(timeout),
1338 %User{} = a <- get_cached_by_id(a.id),
1339 %User{} = b <- get_cached_by_id(b.id) do
1346 def parse_bio(bio) when is_binary(bio) and bio != "" do
1348 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1352 def parse_bio(_), do: ""
1354 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1355 # TODO: get profile URLs other than user.ap_id
1356 profile_urls = [user.ap_id]
1359 |> CommonUtils.format_input("text/plain",
1360 mentions_format: :full,
1361 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1366 def parse_bio(_, _), do: ""
1368 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1369 Repo.transaction(fn ->
1370 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1374 def tag(nickname, tags) when is_binary(nickname),
1375 do: tag(get_by_nickname(nickname), tags)
1377 def tag(%User{} = user, tags),
1378 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1380 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1381 Repo.transaction(fn ->
1382 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1386 def untag(nickname, tags) when is_binary(nickname),
1387 do: untag(get_by_nickname(nickname), tags)
1389 def untag(%User{} = user, tags),
1390 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1392 defp update_tags(%User{} = user, new_tags) do
1393 {:ok, updated_user} =
1395 |> change(%{tags: new_tags})
1396 |> update_and_set_cache()
1401 defp normalize_tags(tags) do
1404 |> Enum.map(&String.downcase/1)
1407 defp local_nickname_regex do
1408 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1409 @extended_local_nickname_regex
1411 @strict_local_nickname_regex
1415 def local_nickname(nickname_or_mention) do
1418 |> String.split("@")
1422 def full_nickname(nickname_or_mention),
1423 do: String.trim_leading(nickname_or_mention, "@")
1425 def error_user(ap_id) do
1430 nickname: "erroruser@example.com",
1431 inserted_at: NaiveDateTime.utc_now()
1435 @spec all_superusers() :: [User.t()]
1436 def all_superusers do
1437 User.Query.build(%{super_users: true, local: true, deactivated: false})
1441 def showing_reblogs?(%User{} = user, %User{} = target) do
1442 target.ap_id not in user.info.muted_reblogs
1446 The function returns a query to get users with no activity for given interval of days.
1447 Inactive users are those who didn't read any notification, or had any activity where
1448 the user is the activity's actor, during `inactivity_threshold` days.
1449 Deactivated users will not appear in this list.
1453 iex> Pleroma.User.list_inactive_users()
1456 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1457 def list_inactive_users_query(inactivity_threshold \\ 7) do
1458 negative_inactivity_threshold = -inactivity_threshold
1459 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1460 # Subqueries are not supported in `where` clauses, join gets too complicated.
1461 has_read_notifications =
1462 from(n in Pleroma.Notification,
1463 where: n.seen == true,
1465 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1468 |> Pleroma.Repo.all()
1470 from(u in Pleroma.User,
1471 left_join: a in Pleroma.Activity,
1472 on: u.ap_id == a.actor,
1473 where: not is_nil(u.nickname),
1474 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1475 where: u.id not in ^has_read_notifications,
1478 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1479 is_nil(max(a.inserted_at))
1484 Enable or disable email notifications for user
1488 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1489 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1491 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1492 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1494 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1495 {:ok, t()} | {:error, Ecto.Changeset.t()}
1496 def switch_email_notifications(user, type, status) do
1497 update_info(user, &User.Info.update_email_notifications(&1, %{type => status}))
1501 Set `last_digest_emailed_at` value for the user to current time
1503 @spec touch_last_digest_emailed_at(t()) :: t()
1504 def touch_last_digest_emailed_at(user) do
1505 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1507 {:ok, updated_user} =
1509 |> change(%{last_digest_emailed_at: now})
1510 |> update_and_set_cache()
1515 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1516 def toggle_confirmation(%User{} = user) do
1517 need_confirmation? = !user.info.confirmation_pending
1520 |> update_info(&User.Info.confirmation_changeset(&1, need_confirmation: need_confirmation?))
1523 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1527 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1528 # use instance-default
1529 config = Pleroma.Config.get([:assets, :mascots])
1530 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1531 mascot = Keyword.get(config, default_mascot)
1534 "id" => "default-mascot",
1535 "url" => mascot[:url],
1536 "preview_url" => mascot[:url],
1538 "mime_type" => mascot[:mime_type]
1543 def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
1545 def ensure_keys_present(%User{} = user) do
1546 with {:ok, pem} <- Keys.generate_rsa_pem() do
1548 |> cast(%{keys: pem}, [:keys])
1549 |> validate_required([:keys])
1550 |> update_and_set_cache()
1554 def get_ap_ids_by_nicknames(nicknames) do
1556 where: u.nickname in ^nicknames,
1562 defdelegate search(query, opts \\ []), to: User.Search
1564 defp put_password_hash(
1565 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1567 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1570 defp put_password_hash(changeset), do: changeset
1572 def is_internal_user?(%User{nickname: nil}), do: true
1573 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1574 def is_internal_user?(_), do: false
1576 # A hack because user delete activities have a fake id for whatever reason
1577 # TODO: Get rid of this
1578 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
1580 def get_delivered_users_by_object_id(object_id) do
1582 inner_join: delivery in assoc(u, :deliveries),
1583 where: delivery.object_id == ^object_id
1588 def change_email(user, email) do
1590 |> cast(%{email: email}, [:email])
1591 |> validate_required([:email])
1592 |> unique_constraint(:email)
1593 |> validate_format(:email, @email_regex)
1594 |> update_and_set_cache()
1598 Changes `user.info` and returns the user changeset.
1600 `fun` is called with the `user.info`.
1602 def change_info(user, fun) do
1603 changeset = change(user)
1604 info = get_field(changeset, :info) || %User.Info{}
1605 put_embed(changeset, :info, fun.(info))
1609 Updates `user.info` and sets cache.
1611 `fun` is called with the `user.info`.
1613 def update_info(users, fun) when is_list(users) do
1614 Repo.transaction(fn ->
1615 for user <- users, do: update_info(user, fun)
1619 def update_info(user, fun) do
1622 |> update_and_set_cache()