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
15 alias Pleroma.Notification
17 alias Pleroma.Registration
19 alias Pleroma.RepoStreamer
22 alias Pleroma.Web.ActivityPub.ActivityPub
23 alias Pleroma.Web.ActivityPub.Utils
24 alias Pleroma.Web.CommonAPI
25 alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
26 alias Pleroma.Web.OAuth
27 alias Pleroma.Web.OStatus
28 alias Pleroma.Web.RelMe
29 alias Pleroma.Web.Websub
30 alias Pleroma.Workers.BackgroundWorker
34 @type t :: %__MODULE__{}
36 @primary_key {:id, Pleroma.FlakeId, 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_-]+$/
44 import Pleroma.Workers.WorkerHelper, only: [worker_args: 1]
48 field(:email, :string)
50 field(:nickname, :string)
51 field(:password_hash, :string)
52 field(:password, :string, virtual: true)
53 field(:password_confirmation, :string, virtual: true)
54 field(:following, {:array, :string}, default: [])
55 field(:ap_id, :string)
57 field(:local, :boolean, default: true)
58 field(:follower_address, :string)
59 field(:following_address, :string)
60 field(:search_rank, :float, virtual: true)
61 field(:search_type, :integer, virtual: true)
62 field(:tags, {:array, :string}, default: [])
63 field(:last_refreshed_at, :naive_datetime_usec)
64 field(:last_digest_emailed_at, :naive_datetime)
65 has_many(:notifications, Notification)
66 has_many(:registrations, Registration)
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
110 "#{Web.base_url()}/users/#{nickname}"
113 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
114 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
116 @spec ap_following(User.t()) :: Sring.t()
117 def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
118 def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
120 def user_info(%User{} = user, args \\ %{}) do
122 if args[:following_count],
123 do: args[:following_count],
124 else: user.info.following_count || following_count(user)
127 if args[:follower_count], do: args[:follower_count], else: user.info.follower_count
130 note_count: user.info.note_count,
131 locked: user.info.locked,
132 confirmation_pending: user.info.confirmation_pending,
133 default_scope: user.info.default_scope
135 |> Map.put(:following_count, following_count)
136 |> Map.put(:follower_count, follower_count)
139 def follow_state(%User{} = user, %User{} = target) do
140 follow_activity = Utils.fetch_latest_follow(user, target)
143 do: follow_activity.data["state"],
144 # Ideally this would be nil, but then Cachex does not commit the value
148 def get_cached_follow_state(user, target) do
149 key = "follow_state:#{user.ap_id}|#{target.ap_id}"
150 Cachex.fetch!(:user_cache, key, fn _ -> {:commit, follow_state(user, target)} end)
153 def set_follow_state_cache(user_ap_id, target_ap_id, state) do
156 "follow_state:#{user_ap_id}|#{target_ap_id}",
161 def set_info_cache(user, args) do
162 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user, args))
165 @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
166 def restrict_deactivated(query) do
168 where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info)
172 def following_count(%User{following: []}), do: 0
174 def following_count(%User{} = user) do
176 |> get_friends_query()
177 |> Repo.aggregate(:count, :id)
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)
184 params = Map.put(params, :info, params[:info] || %{})
185 info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])
189 |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
190 |> validate_required([:name, :ap_id])
191 |> unique_constraint(:nickname)
192 |> validate_format(:nickname, @email_regex)
193 |> validate_length(:bio, max: bio_limit)
194 |> validate_length(:name, max: name_limit)
195 |> put_change(:local, false)
196 |> put_embed(:info, info_cng)
199 case info_cng.changes[:source_data] do
200 %{"followers" => followers, "following" => following} ->
202 |> put_change(:follower_address, followers)
203 |> put_change(:following_address, following)
206 followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
209 |> put_change(:follower_address, followers)
216 def update_changeset(struct, params \\ %{}) do
217 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
218 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
221 |> cast(params, [:bio, :name, :avatar, :following])
222 |> unique_constraint(:nickname)
223 |> validate_format(:nickname, local_nickname_regex())
224 |> validate_length(:bio, max: bio_limit)
225 |> validate_length(:name, min: 1, max: name_limit)
228 def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
229 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
230 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
232 params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
233 info_cng = User.Info.user_upgrade(struct.info, params[:info], remote?)
244 |> unique_constraint(:nickname)
245 |> validate_format(:nickname, local_nickname_regex())
246 |> validate_length(:bio, max: bio_limit)
247 |> validate_length(:name, max: name_limit)
248 |> put_embed(:info, info_cng)
251 def password_update_changeset(struct, params) do
253 |> cast(params, [:password, :password_confirmation])
254 |> validate_required([:password, :password_confirmation])
255 |> validate_confirmation(:password)
259 @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
260 def reset_password(%User{id: user_id} = user, data) do
263 |> Multi.update(:user, password_update_changeset(user, data))
264 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
265 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
267 case Repo.transaction(multi) do
268 {:ok, %{user: user} = _} -> set_cache(user)
269 {:error, _, changeset, _} -> {:error, changeset}
273 def register_changeset(struct, params \\ %{}, opts \\ []) do
274 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
275 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
278 if is_nil(opts[:need_confirmation]) do
279 Pleroma.Config.get([:instance, :account_activation_required])
281 opts[:need_confirmation]
285 User.Info.confirmation_changeset(%User.Info{}, need_confirmation: need_confirmation?)
289 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
290 |> validate_required([:name, :nickname, :password, :password_confirmation])
291 |> validate_confirmation(:password)
292 |> unique_constraint(:email)
293 |> unique_constraint(:nickname)
294 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
295 |> validate_format(:nickname, local_nickname_regex())
296 |> validate_format(:email, @email_regex)
297 |> validate_length(:bio, max: bio_limit)
298 |> validate_length(:name, min: 1, max: name_limit)
299 |> put_change(:info, info_change)
302 if opts[:external] do
305 validate_required(changeset, [:email])
308 if changeset.valid? do
309 ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
310 followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
314 |> put_change(:ap_id, ap_id)
315 |> unique_constraint(:ap_id)
316 |> put_change(:following, [followers])
317 |> put_change(:follower_address, followers)
323 defp autofollow_users(user) do
324 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
327 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
330 follow_all(user, autofollowed_users)
333 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
334 def register(%Ecto.Changeset{} = changeset) do
335 with {:ok, user} <- Repo.insert(changeset),
336 {:ok, user} <- post_register_action(user) do
341 def post_register_action(%User{} = user) do
342 with {:ok, user} <- autofollow_users(user),
343 {:ok, user} <- set_cache(user),
344 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
345 {:ok, _} <- try_send_confirmation_email(user) do
350 def try_send_confirmation_email(%User{} = user) do
351 if user.info.confirmation_pending &&
352 Pleroma.Config.get([:instance, :account_activation_required]) do
354 |> Pleroma.Emails.UserEmail.account_confirmation_email()
355 |> Pleroma.Emails.Mailer.deliver_async()
363 def needs_update?(%User{local: true}), do: false
365 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
367 def needs_update?(%User{local: false} = user) do
368 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
371 def needs_update?(_), do: true
373 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
374 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
378 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
379 follow(follower, followed)
382 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
383 if not User.ap_enabled?(followed) do
384 follow(follower, followed)
390 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
391 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
392 def follow_all(follower, followeds) do
395 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
396 |> Enum.map(fn %{follower_address: fa} -> fa end)
400 where: u.id == ^follower.id,
405 "array(select distinct unnest (array_cat(?, ?)))",
414 {1, [follower]} = Repo.update_all(q, [])
416 Enum.each(followeds, fn followed ->
417 update_follower_count(followed)
423 def follow(%User{} = follower, %User{info: info} = followed) do
424 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
425 ap_followers = followed.follower_address
429 {:error, "Could not follow user: You are deactivated."}
431 deny_follow_blocked and blocks?(followed, follower) ->
432 {:error, "Could not follow user: #{followed.nickname} blocked you."}
435 if !followed.local && follower.local && !ap_enabled?(followed) do
436 Websub.subscribe(follower, followed)
441 where: u.id == ^follower.id,
442 update: [push: [following: ^ap_followers]],
446 {1, [follower]} = Repo.update_all(q, [])
448 follower = maybe_update_following_count(follower)
450 {:ok, _} = update_follower_count(followed)
456 def unfollow(%User{} = follower, %User{} = followed) do
457 ap_followers = followed.follower_address
459 if following?(follower, followed) and follower.ap_id != followed.ap_id do
462 where: u.id == ^follower.id,
463 update: [pull: [following: ^ap_followers]],
467 {1, [follower]} = Repo.update_all(q, [])
469 follower = maybe_update_following_count(follower)
471 {:ok, followed} = update_follower_count(followed)
475 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
477 {:error, "Not subscribed!"}
481 @spec following?(User.t(), User.t()) :: boolean
482 def following?(%User{} = follower, %User{} = followed) do
483 Enum.member?(follower.following, followed.follower_address)
486 def locked?(%User{} = user) do
487 user.info.locked || false
491 Repo.get_by(User, id: id)
494 def get_by_ap_id(ap_id) do
495 Repo.get_by(User, ap_id: ap_id)
498 def get_all_by_ap_id(ap_ids) do
499 from(u in __MODULE__,
500 where: u.ap_id in ^ap_ids
505 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
506 # of the ap_id and the domain and tries to get that user
507 def get_by_guessed_nickname(ap_id) do
508 domain = URI.parse(ap_id).host
509 name = List.last(String.split(ap_id, "/"))
510 nickname = "#{name}@#{domain}"
512 get_cached_by_nickname(nickname)
515 def set_cache({:ok, user}), do: set_cache(user)
516 def set_cache({:error, err}), do: {:error, err}
518 def set_cache(%User{} = user) do
519 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
520 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
521 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
525 def update_and_set_cache(changeset) do
526 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
533 def invalidate_cache(user) do
534 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
535 Cachex.del(:user_cache, "nickname:#{user.nickname}")
536 Cachex.del(:user_cache, "user_info:#{user.id}")
539 def get_cached_by_ap_id(ap_id) do
540 key = "ap_id:#{ap_id}"
541 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
544 def get_cached_by_id(id) do
548 Cachex.fetch!(:user_cache, key, fn _ ->
552 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
553 {:commit, user.ap_id}
559 get_cached_by_ap_id(ap_id)
562 def get_cached_by_nickname(nickname) do
563 key = "nickname:#{nickname}"
565 Cachex.fetch!(:user_cache, key, fn ->
566 user_result = get_or_fetch_by_nickname(nickname)
569 {:ok, user} -> {:commit, user}
570 {:error, _error} -> {:ignore, nil}
575 def get_cached_by_nickname_or_id(nickname_or_id) do
576 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
579 def get_by_nickname(nickname) do
580 Repo.get_by(User, nickname: nickname) ||
581 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
582 Repo.get_by(User, nickname: local_nickname(nickname))
586 def get_by_email(email), do: Repo.get_by(User, email: email)
588 def get_by_nickname_or_email(nickname_or_email) do
589 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
592 def get_cached_user_info(user) do
593 key = "user_info:#{user.id}"
594 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
597 def fetch_by_nickname(nickname) do
598 ap_try = ActivityPub.make_user_from_nickname(nickname)
601 {:ok, user} -> {:ok, user}
602 _ -> OStatus.make_user(nickname)
606 def get_or_fetch_by_nickname(nickname) do
607 with %User{} = user <- get_by_nickname(nickname) do
611 with [_nick, _domain] <- String.split(nickname, "@"),
612 {:ok, user} <- fetch_by_nickname(nickname) do
613 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
614 fetch_initial_posts(user)
619 _e -> {:error, "not found " <> nickname}
624 @doc "Fetch some posts when the user has just been federated with"
625 def fetch_initial_posts(user) do
626 %{"op" => "fetch_initial_posts", "user_id" => user.id}
627 |> BackgroundWorker.new(worker_args(:background))
631 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
632 def get_followers_query(%User{} = user, nil) do
633 User.Query.build(%{followers: user, deactivated: false})
636 def get_followers_query(user, page) do
637 from(u in get_followers_query(user, nil))
638 |> User.Query.paginate(page, 20)
641 @spec get_followers_query(User.t()) :: Ecto.Query.t()
642 def get_followers_query(user), do: get_followers_query(user, nil)
644 @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
645 def get_followers(user, page \\ nil) do
646 q = get_followers_query(user, page)
651 @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
652 def get_external_followers(user, page \\ nil) do
655 |> get_followers_query(page)
656 |> User.Query.build(%{external: true})
661 def get_followers_ids(user, page \\ nil) do
662 q = get_followers_query(user, page)
664 Repo.all(from(u in q, select: u.id))
667 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
668 def get_friends_query(%User{} = user, nil) do
669 User.Query.build(%{friends: user, deactivated: false})
672 def get_friends_query(user, page) do
673 from(u in get_friends_query(user, nil))
674 |> User.Query.paginate(page, 20)
677 @spec get_friends_query(User.t()) :: Ecto.Query.t()
678 def get_friends_query(user), do: get_friends_query(user, nil)
680 def get_friends(user, page \\ nil) do
681 q = get_friends_query(user, page)
686 def get_friends_ids(user, page \\ nil) do
687 q = get_friends_query(user, page)
689 Repo.all(from(u in q, select: u.id))
692 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
693 def get_follow_requests(%User{} = user) do
695 Activity.follow_requests_for_actor(user)
696 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
697 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
698 |> group_by([a, u], u.id)
705 def increase_note_count(%User{} = user) do
707 |> where(id: ^user.id)
712 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
719 |> Repo.update_all([])
721 {1, [user]} -> set_cache(user)
726 def decrease_note_count(%User{} = user) do
728 |> where(id: ^user.id)
733 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
740 |> Repo.update_all([])
742 {1, [user]} -> set_cache(user)
747 def update_note_count(%User{} = user) do
751 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
755 note_count = Repo.one(note_count_query)
757 info_cng = User.Info.set_note_count(user.info, note_count)
761 |> put_embed(:info, info_cng)
762 |> update_and_set_cache()
765 @spec maybe_fetch_follow_information(User.t()) :: User.t()
766 def maybe_fetch_follow_information(user) do
767 with {:ok, user} <- fetch_follow_information(user) do
771 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
777 def fetch_follow_information(user) do
778 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
779 info_cng = User.Info.follow_information_update(user.info, info)
784 |> put_embed(:info, info_cng)
786 update_and_set_cache(changeset)
793 def update_follower_count(%User{} = user) do
794 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
795 follower_count_query =
796 User.Query.build(%{followers: user, deactivated: false})
797 |> select([u], %{count: count(u.id)})
800 |> where(id: ^user.id)
801 |> join(:inner, [u], s in subquery(follower_count_query))
806 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
813 |> Repo.update_all([])
815 {1, [user]} -> set_cache(user)
819 {:ok, maybe_fetch_follow_information(user)}
823 @spec maybe_update_following_count(User.t()) :: User.t()
824 def maybe_update_following_count(%User{local: false} = user) do
825 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
826 maybe_fetch_follow_information(user)
832 def maybe_update_following_count(user), do: user
834 def remove_duplicated_following(%User{following: following} = user) do
835 uniq_following = Enum.uniq(following)
837 if length(following) == length(uniq_following) do
841 |> update_changeset(%{following: uniq_following})
842 |> update_and_set_cache()
846 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
847 def get_users_from_set(ap_ids, local_only \\ true) do
848 criteria = %{ap_id: ap_ids, deactivated: false}
849 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
851 User.Query.build(criteria)
855 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
856 def get_recipients_from_activity(%Activity{recipients: to}) do
857 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
861 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
862 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
866 User.Info.add_to_mutes(info, ap_id)
867 |> User.Info.add_to_muted_notifications(info, ap_id, notifications?)
871 |> put_embed(:info, info_cng)
873 update_and_set_cache(cng)
876 def unmute(muter, %{ap_id: ap_id}) do
880 User.Info.remove_from_mutes(info, ap_id)
881 |> User.Info.remove_from_muted_notifications(info, ap_id)
885 |> put_embed(:info, info_cng)
887 update_and_set_cache(cng)
890 def subscribe(subscriber, %{ap_id: ap_id}) do
891 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
893 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
894 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
897 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
901 |> User.Info.add_to_subscribers(subscriber.ap_id)
904 |> put_embed(:info, info_cng)
905 |> update_and_set_cache()
910 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
911 with %User{} = user <- get_cached_by_ap_id(ap_id) do
914 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
917 |> put_embed(:info, info_cng)
918 |> update_and_set_cache()
922 def block(blocker, %User{ap_id: ap_id} = blocked) do
923 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
925 if following?(blocker, blocked) do
926 {:ok, blocker, _} = unfollow(blocker, blocked)
932 # clear any requested follows as well
934 case CommonAPI.reject_follow_request(blocked, blocker) do
935 {:ok, %User{} = updated_blocked} -> updated_blocked
940 if subscribed_to?(blocked, blocker) do
941 {:ok, blocker} = unsubscribe(blocked, blocker)
947 if following?(blocked, blocker) do
948 unfollow(blocked, blocker)
951 {:ok, blocker} = update_follower_count(blocker)
955 |> User.Info.add_to_block(ap_id)
959 |> put_embed(:info, info_cng)
961 update_and_set_cache(cng)
964 # helper to handle the block given only an actor's AP id
965 def block(blocker, %{ap_id: ap_id}) do
966 block(blocker, get_cached_by_ap_id(ap_id))
969 def unblock(blocker, %{ap_id: ap_id}) do
972 |> User.Info.remove_from_block(ap_id)
976 |> put_embed(:info, info_cng)
978 update_and_set_cache(cng)
981 def mutes?(nil, _), do: false
982 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
984 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
985 def muted_notifications?(nil, _), do: false
987 def muted_notifications?(user, %{ap_id: ap_id}),
988 do: Enum.member?(user.info.muted_notifications, ap_id)
990 def blocks?(%User{} = user, %User{} = target) do
991 blocks_ap_id?(user, target) || blocks_domain?(user, target)
994 def blocks?(nil, _), do: false
996 def blocks_ap_id?(%User{} = user, %User{} = target) do
997 Enum.member?(user.info.blocks, target.ap_id)
1000 def blocks_ap_id?(_, _), do: false
1002 def blocks_domain?(%User{} = user, %User{} = target) do
1003 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
1004 %{host: host} = URI.parse(target.ap_id)
1005 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1008 def blocks_domain?(_, _), do: false
1010 def subscribed_to?(user, %{ap_id: ap_id}) do
1011 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1012 Enum.member?(target.info.subscribers, user.ap_id)
1016 @spec muted_users(User.t()) :: [User.t()]
1017 def muted_users(user) do
1018 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
1022 @spec blocked_users(User.t()) :: [User.t()]
1023 def blocked_users(user) do
1024 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
1028 @spec subscribers(User.t()) :: [User.t()]
1029 def subscribers(user) do
1030 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
1034 def block_domain(user, domain) do
1037 |> User.Info.add_to_domain_block(domain)
1041 |> put_embed(:info, info_cng)
1043 update_and_set_cache(cng)
1046 def unblock_domain(user, domain) do
1049 |> User.Info.remove_from_domain_block(domain)
1053 |> put_embed(:info, info_cng)
1055 update_and_set_cache(cng)
1058 def deactivate_async(user, status \\ true) do
1059 %{"op" => "deactivate_user", "user_id" => user.id, "status" => status}
1060 |> BackgroundWorker.new(worker_args(:background))
1064 def deactivate(%User{} = user, status \\ true) do
1065 info_cng = User.Info.set_activation_status(user.info, status)
1067 with {:ok, friends} <- User.get_friends(user),
1068 {:ok, followers} <- User.get_followers(user),
1072 |> put_embed(:info, info_cng)
1073 |> update_and_set_cache() do
1074 Enum.each(followers, &invalidate_cache(&1))
1075 Enum.each(friends, &update_follower_count(&1))
1081 def update_notification_settings(%User{} = user, settings \\ %{}) do
1082 info_changeset = User.Info.update_notification_settings(user.info, settings)
1085 |> put_embed(:info, info_changeset)
1086 |> update_and_set_cache()
1089 def delete(%User{} = user) do
1090 %{"op" => "delete_user", "user_id" => user.id}
1091 |> BackgroundWorker.new(worker_args(:background))
1095 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1096 def perform(:delete, %User{} = user) do
1097 {:ok, _user} = ActivityPub.delete(user)
1099 # Remove all relationships
1100 {:ok, followers} = User.get_followers(user)
1102 Enum.each(followers, fn follower ->
1103 ActivityPub.unfollow(follower, user)
1104 User.unfollow(follower, user)
1107 {:ok, friends} = User.get_friends(user)
1109 Enum.each(friends, fn followed ->
1110 ActivityPub.unfollow(user, followed)
1111 User.unfollow(user, followed)
1114 delete_user_activities(user)
1115 invalidate_cache(user)
1119 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1120 def perform(:fetch_initial_posts, %User{} = user) do
1121 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1124 # Insert all the posts in reverse order, so they're in the right order on the timeline
1125 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
1126 &Pleroma.Web.Federator.incoming_ap_doc/1
1132 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1134 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1135 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1136 when is_list(blocked_identifiers) do
1138 blocked_identifiers,
1139 fn blocked_identifier ->
1140 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1141 {:ok, blocker} <- block(blocker, blocked),
1142 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1146 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1153 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1154 def perform(:follow_import, %User{} = follower, followed_identifiers)
1155 when is_list(followed_identifiers) do
1157 followed_identifiers,
1158 fn followed_identifier ->
1159 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1160 {:ok, follower} <- maybe_direct_follow(follower, followed),
1161 {:ok, _} <- ActivityPub.follow(follower, followed) do
1165 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1172 @spec external_users_query() :: Ecto.Query.t()
1173 def external_users_query do
1181 @spec external_users(keyword()) :: [User.t()]
1182 def external_users(opts \\ []) do
1184 external_users_query()
1185 |> select([u], struct(u, [:id, :ap_id, :info]))
1189 do: where(query, [u], u.id > ^opts[:max_id]),
1194 do: limit(query, ^opts[:limit]),
1200 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1202 "op" => "blocks_import",
1203 "blocker_id" => blocker.id,
1204 "blocked_identifiers" => blocked_identifiers
1206 |> BackgroundWorker.new(worker_args(:background))
1210 def follow_import(%User{} = follower, followed_identifiers)
1211 when is_list(followed_identifiers) do
1213 "op" => "follow_import",
1214 "follower_id" => follower.id,
1215 "followed_identifiers" => followed_identifiers
1217 |> BackgroundWorker.new(worker_args(:background))
1221 def delete_user_activities(%User{ap_id: ap_id} = user) do
1223 |> Activity.query_by_actor()
1224 |> RepoStreamer.chunk_stream(50)
1225 |> Stream.each(fn activities ->
1226 Enum.each(activities, &delete_activity(&1))
1233 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1235 |> Object.normalize()
1236 |> ActivityPub.delete()
1239 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1240 user = get_cached_by_ap_id(activity.actor)
1241 object = Object.normalize(activity)
1243 ActivityPub.unlike(user, object)
1246 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1247 user = get_cached_by_ap_id(activity.actor)
1248 object = Object.normalize(activity)
1250 ActivityPub.unannounce(user, object)
1253 defp delete_activity(_activity), do: "Doing nothing"
1255 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1256 Pleroma.HTML.Scrubber.TwitterText
1259 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1261 def fetch_by_ap_id(ap_id) do
1262 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1269 case OStatus.make_user(ap_id) do
1270 {:ok, user} -> {:ok, user}
1271 _ -> {:error, "Could not fetch by AP id"}
1276 def get_or_fetch_by_ap_id(ap_id) do
1277 user = get_cached_by_ap_id(ap_id)
1279 if !is_nil(user) and !User.needs_update?(user) do
1282 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1283 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1285 resp = fetch_by_ap_id(ap_id)
1287 if should_fetch_initial do
1288 with {:ok, %User{} = user} <- resp do
1289 fetch_initial_posts(user)
1297 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1298 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1299 if user = get_cached_by_ap_id(uri) do
1303 %User{info: %User.Info{}}
1304 |> cast(%{}, [:ap_id, :nickname, :local])
1305 |> put_change(:ap_id, uri)
1306 |> put_change(:nickname, nickname)
1307 |> put_change(:local, true)
1308 |> put_change(:follower_address, uri <> "/followers")
1310 {:ok, user} = Repo.insert(changes)
1316 def public_key_from_info(%{
1317 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1321 |> :public_key.pem_decode()
1323 |> :public_key.pem_entry_decode()
1329 def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
1330 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1333 def public_key_from_info(_), do: {:error, "not found key"}
1335 def get_public_key_for_ap_id(ap_id) do
1336 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1337 {:ok, public_key} <- public_key_from_info(user.info) do
1344 defp blank?(""), do: nil
1345 defp blank?(n), do: n
1347 def insert_or_update_user(data) do
1349 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1350 |> remote_user_creation()
1351 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1355 def ap_enabled?(%User{local: true}), do: true
1356 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1357 def ap_enabled?(_), do: false
1359 @doc "Gets or fetch a user by uri or nickname."
1360 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1361 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1362 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1364 # wait a period of time and return newest version of the User structs
1365 # this is because we have synchronous follow APIs and need to simulate them
1366 # with an async handshake
1367 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1368 with %User{} = a <- User.get_cached_by_id(a.id),
1369 %User{} = b <- User.get_cached_by_id(b.id) do
1377 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1378 with :ok <- :timer.sleep(timeout),
1379 %User{} = a <- User.get_cached_by_id(a.id),
1380 %User{} = b <- User.get_cached_by_id(b.id) do
1388 def parse_bio(bio) when is_binary(bio) and bio != "" do
1390 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1394 def parse_bio(_), do: ""
1396 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1397 # TODO: get profile URLs other than user.ap_id
1398 profile_urls = [user.ap_id]
1401 |> CommonUtils.format_input("text/plain",
1402 mentions_format: :full,
1403 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1408 def parse_bio(_, _), do: ""
1410 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1411 Repo.transaction(fn ->
1412 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1416 def tag(nickname, tags) when is_binary(nickname),
1417 do: tag(get_by_nickname(nickname), tags)
1419 def tag(%User{} = user, tags),
1420 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1422 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1423 Repo.transaction(fn ->
1424 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1428 def untag(nickname, tags) when is_binary(nickname),
1429 do: untag(get_by_nickname(nickname), tags)
1431 def untag(%User{} = user, tags),
1432 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1434 defp update_tags(%User{} = user, new_tags) do
1435 {:ok, updated_user} =
1437 |> change(%{tags: new_tags})
1438 |> update_and_set_cache()
1443 defp normalize_tags(tags) do
1446 |> Enum.map(&String.downcase(&1))
1449 defp local_nickname_regex do
1450 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1451 @extended_local_nickname_regex
1453 @strict_local_nickname_regex
1457 def local_nickname(nickname_or_mention) do
1460 |> String.split("@")
1464 def full_nickname(nickname_or_mention),
1465 do: String.trim_leading(nickname_or_mention, "@")
1467 def error_user(ap_id) do
1472 nickname: "erroruser@example.com",
1473 inserted_at: NaiveDateTime.utc_now()
1477 @spec all_superusers() :: [User.t()]
1478 def all_superusers do
1479 User.Query.build(%{super_users: true, local: true, deactivated: false})
1483 def showing_reblogs?(%User{} = user, %User{} = target) do
1484 target.ap_id not in user.info.muted_reblogs
1488 The function returns a query to get users with no activity for given interval of days.
1489 Inactive users are those who didn't read any notification, or had any activity where
1490 the user is the activity's actor, during `inactivity_threshold` days.
1491 Deactivated users will not appear in this list.
1495 iex> Pleroma.User.list_inactive_users()
1498 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1499 def list_inactive_users_query(inactivity_threshold \\ 7) do
1500 negative_inactivity_threshold = -inactivity_threshold
1501 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1502 # Subqueries are not supported in `where` clauses, join gets too complicated.
1503 has_read_notifications =
1504 from(n in Pleroma.Notification,
1505 where: n.seen == true,
1507 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1510 |> Pleroma.Repo.all()
1512 from(u in Pleroma.User,
1513 left_join: a in Pleroma.Activity,
1514 on: u.ap_id == a.actor,
1515 where: not is_nil(u.nickname),
1516 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1517 where: u.id not in ^has_read_notifications,
1520 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1521 is_nil(max(a.inserted_at))
1526 Enable or disable email notifications for user
1530 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1531 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1533 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1534 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1536 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1537 {:ok, t()} | {:error, Ecto.Changeset.t()}
1538 def switch_email_notifications(user, type, status) do
1539 info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})
1542 |> put_embed(:info, info)
1543 |> update_and_set_cache()
1547 Set `last_digest_emailed_at` value for the user to current time
1549 @spec touch_last_digest_emailed_at(t()) :: t()
1550 def touch_last_digest_emailed_at(user) do
1551 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1553 {:ok, updated_user} =
1555 |> change(%{last_digest_emailed_at: now})
1556 |> update_and_set_cache()
1561 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1562 def toggle_confirmation(%User{} = user) do
1563 need_confirmation? = !user.info.confirmation_pending
1566 User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
1570 |> put_embed(:info, info_changeset)
1571 |> update_and_set_cache()
1574 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1578 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1579 # use instance-default
1580 config = Pleroma.Config.get([:assets, :mascots])
1581 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1582 mascot = Keyword.get(config, default_mascot)
1585 "id" => "default-mascot",
1586 "url" => mascot[:url],
1587 "preview_url" => mascot[:url],
1589 "mime_type" => mascot[:mime_type]
1594 def ensure_keys_present(%User{info: info} = user) do
1598 {:ok, pem} = Keys.generate_rsa_pem()
1601 |> Ecto.Changeset.change()
1602 |> Ecto.Changeset.put_embed(:info, User.Info.set_keys(info, pem))
1603 |> update_and_set_cache()
1607 def get_ap_ids_by_nicknames(nicknames) do
1609 where: u.nickname in ^nicknames,
1615 defdelegate search(query, opts \\ []), to: User.Search
1617 defp put_password_hash(
1618 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1620 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1623 defp put_password_hash(changeset), do: changeset
1625 def is_internal_user?(%User{nickname: nil}), do: true
1626 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1627 def is_internal_user?(_), do: false