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
33 @type t :: %__MODULE__{}
35 @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
37 # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
38 @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])?)*$/
40 @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
41 @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
45 field(:email, :string)
47 field(:nickname, :string)
48 field(:password_hash, :string)
49 field(:password, :string, virtual: true)
50 field(:password_confirmation, :string, virtual: true)
52 field(:following, {:array, :string}, default: [])
53 field(:ap_id, :string)
55 field(:local, :boolean, default: true)
56 field(:follower_address, :string)
57 field(:following_address, :string)
58 field(:search_rank, :float, virtual: true)
59 field(:search_type, :integer, virtual: true)
60 field(:tags, {:array, :string}, default: [])
61 field(:last_refreshed_at, :naive_datetime_usec)
62 field(:last_digest_emailed_at, :naive_datetime)
63 has_many(:notifications, Notification)
64 has_many(:registrations, Registration)
65 embeds_one(:info, User.Info)
70 def auth_active?(%User{info: %User.Info{confirmation_pending: true}}),
71 do: !Pleroma.Config.get([:instance, :account_activation_required])
73 def auth_active?(%User{info: %User.Info{deactivated: true}}), do: false
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 defp truncate_if_exists(params, key, max_length) do
181 if Map.has_key?(params, key) and is_binary(params[key]) do
182 {value, _chopped} = String.split_at(params[key], max_length)
183 Map.put(params, key, value)
189 def remote_user_creation(params) do
190 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
191 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
195 |> Map.put(:info, params[:info] || %{})
196 |> truncate_if_exists(:name, name_limit)
197 |> truncate_if_exists(:bio, bio_limit)
199 info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])
203 |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
204 |> validate_required([:name, :ap_id])
205 |> unique_constraint(:nickname)
206 |> validate_format(:nickname, @email_regex)
207 |> validate_length(:bio, max: bio_limit)
208 |> validate_length(:name, max: name_limit)
209 |> put_change(:local, false)
210 |> put_embed(:info, info_cng)
213 case info_cng.changes[:source_data] do
214 %{"followers" => followers, "following" => following} ->
216 |> put_change(:follower_address, followers)
217 |> put_change(:following_address, following)
220 followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
223 |> put_change(:follower_address, followers)
230 def update_changeset(struct, params \\ %{}) do
231 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
232 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
235 |> cast(params, [:bio, :name, :avatar, :following])
236 |> unique_constraint(:nickname)
237 |> validate_format(:nickname, local_nickname_regex())
238 |> validate_length(:bio, max: bio_limit)
239 |> validate_length(:name, min: 1, max: name_limit)
242 def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
243 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
244 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
246 params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
247 info_cng = User.Info.user_upgrade(struct.info, params[:info], remote?)
258 |> unique_constraint(:nickname)
259 |> validate_format(:nickname, local_nickname_regex())
260 |> validate_length(:bio, max: bio_limit)
261 |> validate_length(:name, max: name_limit)
262 |> put_embed(:info, info_cng)
265 def password_update_changeset(struct, params) do
267 |> cast(params, [:password, :password_confirmation])
268 |> validate_required([:password, :password_confirmation])
269 |> validate_confirmation(:password)
273 @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
274 def reset_password(%User{id: user_id} = user, data) do
277 |> Multi.update(:user, password_update_changeset(user, data))
278 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
279 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
281 case Repo.transaction(multi) do
282 {:ok, %{user: user} = _} -> set_cache(user)
283 {:error, _, changeset, _} -> {:error, changeset}
287 def register_changeset(struct, params \\ %{}, opts \\ []) do
288 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
289 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
292 if is_nil(opts[:need_confirmation]) do
293 Pleroma.Config.get([:instance, :account_activation_required])
295 opts[:need_confirmation]
299 User.Info.confirmation_changeset(%User.Info{}, need_confirmation: need_confirmation?)
303 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
304 |> validate_required([:name, :nickname, :password, :password_confirmation])
305 |> validate_confirmation(:password)
306 |> unique_constraint(:email)
307 |> unique_constraint(:nickname)
308 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
309 |> validate_format(:nickname, local_nickname_regex())
310 |> validate_format(:email, @email_regex)
311 |> validate_length(:bio, max: bio_limit)
312 |> validate_length(:name, min: 1, max: name_limit)
313 |> put_change(:info, info_change)
316 if opts[:external] do
319 validate_required(changeset, [:email])
322 if changeset.valid? do
323 ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
324 followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
328 |> put_change(:ap_id, ap_id)
329 |> unique_constraint(:ap_id)
330 |> put_change(:following, [followers])
331 |> put_change(:follower_address, followers)
337 defp autofollow_users(user) do
338 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
341 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
344 follow_all(user, autofollowed_users)
347 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
348 def register(%Ecto.Changeset{} = changeset) do
349 with {:ok, user} <- Repo.insert(changeset),
350 {:ok, user} <- post_register_action(user) do
355 def post_register_action(%User{} = user) do
356 with {:ok, user} <- autofollow_users(user),
357 {:ok, user} <- set_cache(user),
358 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
359 {:ok, _} <- try_send_confirmation_email(user) do
364 def try_send_confirmation_email(%User{} = user) do
365 if user.info.confirmation_pending &&
366 Pleroma.Config.get([:instance, :account_activation_required]) do
368 |> Pleroma.Emails.UserEmail.account_confirmation_email()
369 |> Pleroma.Emails.Mailer.deliver_async()
377 def needs_update?(%User{local: true}), do: false
379 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
381 def needs_update?(%User{local: false} = user) do
382 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
385 def needs_update?(_), do: true
387 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
388 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
392 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
393 follow(follower, followed)
396 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
397 if not User.ap_enabled?(followed) do
398 follow(follower, followed)
404 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
405 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
406 def follow_all(follower, followeds) do
409 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
410 |> Enum.map(fn %{follower_address: fa} -> fa end)
414 where: u.id == ^follower.id,
419 "array(select distinct unnest (array_cat(?, ?)))",
428 {1, [follower]} = Repo.update_all(q, [])
430 Enum.each(followeds, fn followed ->
431 update_follower_count(followed)
437 def follow(%User{} = follower, %User{info: info} = followed) do
438 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
439 ap_followers = followed.follower_address
443 {:error, "Could not follow user: You are deactivated."}
445 deny_follow_blocked and blocks?(followed, follower) ->
446 {:error, "Could not follow user: #{followed.nickname} blocked you."}
449 if !followed.local && follower.local && !ap_enabled?(followed) do
450 Websub.subscribe(follower, followed)
455 where: u.id == ^follower.id,
456 update: [push: [following: ^ap_followers]],
460 {1, [follower]} = Repo.update_all(q, [])
462 follower = maybe_update_following_count(follower)
464 {:ok, _} = update_follower_count(followed)
470 def unfollow(%User{} = follower, %User{} = followed) do
471 ap_followers = followed.follower_address
473 if following?(follower, followed) and follower.ap_id != followed.ap_id do
476 where: u.id == ^follower.id,
477 update: [pull: [following: ^ap_followers]],
481 {1, [follower]} = Repo.update_all(q, [])
483 follower = maybe_update_following_count(follower)
485 {:ok, followed} = update_follower_count(followed)
489 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
491 {:error, "Not subscribed!"}
495 @spec following?(User.t(), User.t()) :: boolean
496 def following?(%User{} = follower, %User{} = followed) do
497 Enum.member?(follower.following, followed.follower_address)
500 def locked?(%User{} = user) do
501 user.info.locked || false
505 Repo.get_by(User, id: id)
508 def get_by_ap_id(ap_id) do
509 Repo.get_by(User, ap_id: ap_id)
512 def get_all_by_ap_id(ap_ids) do
513 from(u in __MODULE__,
514 where: u.ap_id in ^ap_ids
519 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
520 # of the ap_id and the domain and tries to get that user
521 def get_by_guessed_nickname(ap_id) do
522 domain = URI.parse(ap_id).host
523 name = List.last(String.split(ap_id, "/"))
524 nickname = "#{name}@#{domain}"
526 get_cached_by_nickname(nickname)
529 def set_cache({:ok, user}), do: set_cache(user)
530 def set_cache({:error, err}), do: {:error, err}
532 def set_cache(%User{} = user) do
533 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
534 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
535 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
539 def update_and_set_cache(changeset) do
540 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
547 def invalidate_cache(user) do
548 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
549 Cachex.del(:user_cache, "nickname:#{user.nickname}")
550 Cachex.del(:user_cache, "user_info:#{user.id}")
553 def get_cached_by_ap_id(ap_id) do
554 key = "ap_id:#{ap_id}"
555 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
558 def get_cached_by_id(id) do
562 Cachex.fetch!(:user_cache, key, fn _ ->
566 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
567 {:commit, user.ap_id}
573 get_cached_by_ap_id(ap_id)
576 def get_cached_by_nickname(nickname) do
577 key = "nickname:#{nickname}"
579 Cachex.fetch!(:user_cache, key, fn ->
580 user_result = get_or_fetch_by_nickname(nickname)
583 {:ok, user} -> {:commit, user}
584 {:error, _error} -> {:ignore, nil}
589 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
590 restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
593 is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
594 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
596 restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
597 get_cached_by_nickname(nickname_or_id)
599 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
600 get_cached_by_nickname(nickname_or_id)
607 def get_by_nickname(nickname) do
608 Repo.get_by(User, nickname: nickname) ||
609 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
610 Repo.get_by(User, nickname: local_nickname(nickname))
614 def get_by_email(email), do: Repo.get_by(User, email: email)
616 def get_by_nickname_or_email(nickname_or_email) do
617 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
620 def get_cached_user_info(user) do
621 key = "user_info:#{user.id}"
622 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
625 def fetch_by_nickname(nickname) do
626 ap_try = ActivityPub.make_user_from_nickname(nickname)
629 {:ok, user} -> {:ok, user}
630 _ -> OStatus.make_user(nickname)
634 def get_or_fetch_by_nickname(nickname) do
635 with %User{} = user <- get_by_nickname(nickname) do
639 with [_nick, _domain] <- String.split(nickname, "@"),
640 {:ok, user} <- fetch_by_nickname(nickname) do
641 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
642 fetch_initial_posts(user)
647 _e -> {:error, "not found " <> nickname}
652 @doc "Fetch some posts when the user has just been federated with"
653 def fetch_initial_posts(user),
654 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:fetch_initial_posts, user])
656 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
657 def get_followers_query(%User{} = user, nil) do
658 User.Query.build(%{followers: user, deactivated: false})
661 def get_followers_query(user, page) do
662 from(u in get_followers_query(user, nil))
663 |> User.Query.paginate(page, 20)
666 @spec get_followers_query(User.t()) :: Ecto.Query.t()
667 def get_followers_query(user), do: get_followers_query(user, nil)
669 @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
670 def get_followers(user, page \\ nil) do
671 q = get_followers_query(user, page)
676 @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
677 def get_external_followers(user, page \\ nil) do
680 |> get_followers_query(page)
681 |> User.Query.build(%{external: true})
686 def get_followers_ids(user, page \\ nil) do
687 q = get_followers_query(user, page)
689 Repo.all(from(u in q, select: u.id))
692 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
693 def get_friends_query(%User{} = user, nil) do
694 User.Query.build(%{friends: user, deactivated: false})
697 def get_friends_query(user, page) do
698 from(u in get_friends_query(user, nil))
699 |> User.Query.paginate(page, 20)
702 @spec get_friends_query(User.t()) :: Ecto.Query.t()
703 def get_friends_query(user), do: get_friends_query(user, nil)
705 def get_friends(user, page \\ nil) do
706 q = get_friends_query(user, page)
711 def get_friends_ids(user, page \\ nil) do
712 q = get_friends_query(user, page)
714 Repo.all(from(u in q, select: u.id))
717 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
718 def get_follow_requests(%User{} = user) do
720 Activity.follow_requests_for_actor(user)
721 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
722 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
723 |> group_by([a, u], u.id)
730 def increase_note_count(%User{} = user) do
732 |> where(id: ^user.id)
737 "safe_jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
744 |> Repo.update_all([])
746 {1, [user]} -> set_cache(user)
751 def decrease_note_count(%User{} = user) do
753 |> where(id: ^user.id)
758 "safe_jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
765 |> Repo.update_all([])
767 {1, [user]} -> set_cache(user)
772 def update_note_count(%User{} = user) do
776 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
780 note_count = Repo.one(note_count_query)
782 info_cng = User.Info.set_note_count(user.info, note_count)
786 |> put_embed(:info, info_cng)
787 |> update_and_set_cache()
790 @spec maybe_fetch_follow_information(User.t()) :: User.t()
791 def maybe_fetch_follow_information(user) do
792 with {:ok, user} <- fetch_follow_information(user) do
796 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
802 def fetch_follow_information(user) do
803 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
804 info_cng = User.Info.follow_information_update(user.info, info)
809 |> put_embed(:info, info_cng)
811 update_and_set_cache(changeset)
818 def update_follower_count(%User{} = user) do
819 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
820 follower_count_query =
821 User.Query.build(%{followers: user, deactivated: false})
822 |> select([u], %{count: count(u.id)})
825 |> where(id: ^user.id)
826 |> join(:inner, [u], s in subquery(follower_count_query))
831 "safe_jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
838 |> Repo.update_all([])
840 {1, [user]} -> set_cache(user)
844 {:ok, maybe_fetch_follow_information(user)}
848 @spec maybe_update_following_count(User.t()) :: User.t()
849 def maybe_update_following_count(%User{local: false} = user) do
850 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
851 maybe_fetch_follow_information(user)
857 def maybe_update_following_count(user), do: user
859 def remove_duplicated_following(%User{following: following} = user) do
860 uniq_following = Enum.uniq(following)
862 if length(following) == length(uniq_following) do
866 |> update_changeset(%{following: uniq_following})
867 |> update_and_set_cache()
871 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
872 def get_users_from_set(ap_ids, local_only \\ true) do
873 criteria = %{ap_id: ap_ids, deactivated: false}
874 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
876 User.Query.build(criteria)
880 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
881 def get_recipients_from_activity(%Activity{recipients: to}) do
882 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
886 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
887 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
891 User.Info.add_to_mutes(info, ap_id)
892 |> User.Info.add_to_muted_notifications(info, ap_id, notifications?)
896 |> put_embed(:info, info_cng)
898 update_and_set_cache(cng)
901 def unmute(muter, %{ap_id: ap_id}) do
905 User.Info.remove_from_mutes(info, ap_id)
906 |> User.Info.remove_from_muted_notifications(info, ap_id)
910 |> put_embed(:info, info_cng)
912 update_and_set_cache(cng)
915 def subscribe(subscriber, %{ap_id: ap_id}) do
916 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
918 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
919 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
922 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
926 |> User.Info.add_to_subscribers(subscriber.ap_id)
929 |> put_embed(:info, info_cng)
930 |> update_and_set_cache()
935 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
936 with %User{} = user <- get_cached_by_ap_id(ap_id) do
939 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
942 |> put_embed(:info, info_cng)
943 |> update_and_set_cache()
947 def block(blocker, %User{ap_id: ap_id} = blocked) do
948 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
950 if following?(blocker, blocked) do
951 {:ok, blocker, _} = unfollow(blocker, blocked)
957 # clear any requested follows as well
959 case CommonAPI.reject_follow_request(blocked, blocker) do
960 {:ok, %User{} = updated_blocked} -> updated_blocked
965 if subscribed_to?(blocked, blocker) do
966 {:ok, blocker} = unsubscribe(blocked, blocker)
972 if following?(blocked, blocker) do
973 unfollow(blocked, blocker)
976 {:ok, blocker} = update_follower_count(blocker)
980 |> User.Info.add_to_block(ap_id)
984 |> put_embed(:info, info_cng)
986 update_and_set_cache(cng)
989 # helper to handle the block given only an actor's AP id
990 def block(blocker, %{ap_id: ap_id}) do
991 block(blocker, get_cached_by_ap_id(ap_id))
994 def unblock(blocker, %{ap_id: ap_id}) do
997 |> User.Info.remove_from_block(ap_id)
1001 |> put_embed(:info, info_cng)
1003 update_and_set_cache(cng)
1006 def mutes?(nil, _), do: false
1007 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
1009 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
1010 def muted_notifications?(nil, _), do: false
1012 def muted_notifications?(user, %{ap_id: ap_id}),
1013 do: Enum.member?(user.info.muted_notifications, ap_id)
1015 def blocks?(%User{} = user, %User{} = target) do
1016 blocks_ap_id?(user, target) || blocks_domain?(user, target)
1019 def blocks?(nil, _), do: false
1021 def blocks_ap_id?(%User{} = user, %User{} = target) do
1022 Enum.member?(user.info.blocks, target.ap_id)
1025 def blocks_ap_id?(_, _), do: false
1027 def blocks_domain?(%User{} = user, %User{} = target) do
1028 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
1029 %{host: host} = URI.parse(target.ap_id)
1030 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1033 def blocks_domain?(_, _), do: false
1035 def subscribed_to?(user, %{ap_id: ap_id}) do
1036 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1037 Enum.member?(target.info.subscribers, user.ap_id)
1041 @spec muted_users(User.t()) :: [User.t()]
1042 def muted_users(user) do
1043 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
1047 @spec blocked_users(User.t()) :: [User.t()]
1048 def blocked_users(user) do
1049 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
1053 @spec subscribers(User.t()) :: [User.t()]
1054 def subscribers(user) do
1055 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
1059 def block_domain(user, domain) do
1062 |> User.Info.add_to_domain_block(domain)
1066 |> put_embed(:info, info_cng)
1068 update_and_set_cache(cng)
1071 def unblock_domain(user, domain) do
1074 |> User.Info.remove_from_domain_block(domain)
1078 |> put_embed(:info, info_cng)
1080 update_and_set_cache(cng)
1083 def deactivate_async(user, status \\ true) do
1084 PleromaJobQueue.enqueue(:background, __MODULE__, [:deactivate_async, user, status])
1087 def deactivate(%User{} = user, status \\ true) do
1088 info_cng = User.Info.set_activation_status(user.info, status)
1090 with {:ok, friends} <- User.get_friends(user),
1091 {:ok, followers} <- User.get_followers(user),
1095 |> put_embed(:info, info_cng)
1096 |> update_and_set_cache() do
1097 Enum.each(followers, &invalidate_cache(&1))
1098 Enum.each(friends, &update_follower_count(&1))
1104 def update_notification_settings(%User{} = user, settings \\ %{}) do
1105 info_changeset = User.Info.update_notification_settings(user.info, settings)
1108 |> put_embed(:info, info_changeset)
1109 |> update_and_set_cache()
1112 @spec delete(User.t()) :: :ok
1113 def delete(%User{} = user),
1114 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:delete, user])
1116 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1117 def perform(:delete, %User{} = user) do
1118 {:ok, _user} = ActivityPub.delete(user)
1120 # Remove all relationships
1121 {:ok, followers} = User.get_followers(user)
1123 Enum.each(followers, fn follower ->
1124 ActivityPub.unfollow(follower, user)
1125 User.unfollow(follower, user)
1128 {:ok, friends} = User.get_friends(user)
1130 Enum.each(friends, fn followed ->
1131 ActivityPub.unfollow(user, followed)
1132 User.unfollow(user, followed)
1135 delete_user_activities(user)
1136 invalidate_cache(user)
1140 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1141 def perform(:fetch_initial_posts, %User{} = user) do
1142 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1145 # Insert all the posts in reverse order, so they're in the right order on the timeline
1146 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
1147 &Pleroma.Web.Federator.incoming_ap_doc/1
1153 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1155 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1156 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1157 when is_list(blocked_identifiers) do
1159 blocked_identifiers,
1160 fn blocked_identifier ->
1161 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1162 {:ok, blocker} <- block(blocker, blocked),
1163 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1167 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1174 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1175 def perform(:follow_import, %User{} = follower, followed_identifiers)
1176 when is_list(followed_identifiers) do
1178 followed_identifiers,
1179 fn followed_identifier ->
1180 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1181 {:ok, follower} <- maybe_direct_follow(follower, followed),
1182 {:ok, _} <- ActivityPub.follow(follower, followed) do
1186 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1193 @spec external_users_query() :: Ecto.Query.t()
1194 def external_users_query do
1202 @spec external_users(keyword()) :: [User.t()]
1203 def external_users(opts \\ []) do
1205 external_users_query()
1206 |> select([u], struct(u, [:id, :ap_id, :info]))
1210 do: where(query, [u], u.id > ^opts[:max_id]),
1215 do: limit(query, ^opts[:limit]),
1221 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers),
1223 PleromaJobQueue.enqueue(:background, __MODULE__, [
1229 def follow_import(%User{} = follower, followed_identifiers) when is_list(followed_identifiers),
1231 PleromaJobQueue.enqueue(:background, __MODULE__, [
1234 followed_identifiers
1237 def delete_user_activities(%User{ap_id: ap_id} = user) do
1239 |> Activity.Queries.by_actor()
1240 |> RepoStreamer.chunk_stream(50)
1241 |> Stream.each(fn activities ->
1242 Enum.each(activities, &delete_activity(&1))
1249 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1251 |> Object.normalize()
1252 |> ActivityPub.delete()
1255 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1256 user = get_cached_by_ap_id(activity.actor)
1257 object = Object.normalize(activity)
1259 ActivityPub.unlike(user, object)
1262 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1263 user = get_cached_by_ap_id(activity.actor)
1264 object = Object.normalize(activity)
1266 ActivityPub.unannounce(user, object)
1269 defp delete_activity(_activity), do: "Doing nothing"
1271 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1272 Pleroma.HTML.Scrubber.TwitterText
1275 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1277 def fetch_by_ap_id(ap_id) do
1278 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1285 case OStatus.make_user(ap_id) do
1286 {:ok, user} -> {:ok, user}
1287 _ -> {:error, "Could not fetch by AP id"}
1292 def get_or_fetch_by_ap_id(ap_id) do
1293 user = get_cached_by_ap_id(ap_id)
1295 if !is_nil(user) and !User.needs_update?(user) do
1298 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1299 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1301 resp = fetch_by_ap_id(ap_id)
1303 if should_fetch_initial do
1304 with {:ok, %User{} = user} <- resp do
1305 fetch_initial_posts(user)
1313 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1314 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1315 if user = get_cached_by_ap_id(uri) do
1319 %User{info: %User.Info{}}
1320 |> cast(%{}, [:ap_id, :nickname, :local])
1321 |> put_change(:ap_id, uri)
1322 |> put_change(:nickname, nickname)
1323 |> put_change(:local, true)
1324 |> put_change(:follower_address, uri <> "/followers")
1326 {:ok, user} = Repo.insert(changes)
1332 def public_key_from_info(%{
1333 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1337 |> :public_key.pem_decode()
1339 |> :public_key.pem_entry_decode()
1345 def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
1346 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1349 def public_key_from_info(_), do: {:error, "not found key"}
1351 def get_public_key_for_ap_id(ap_id) do
1352 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1353 {:ok, public_key} <- public_key_from_info(user.info) do
1360 defp blank?(""), do: nil
1361 defp blank?(n), do: n
1363 def insert_or_update_user(data) do
1365 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1366 |> remote_user_creation()
1367 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1371 def ap_enabled?(%User{local: true}), do: true
1372 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1373 def ap_enabled?(_), do: false
1375 @doc "Gets or fetch a user by uri or nickname."
1376 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1377 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1378 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1380 # wait a period of time and return newest version of the User structs
1381 # this is because we have synchronous follow APIs and need to simulate them
1382 # with an async handshake
1383 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1384 with %User{} = a <- User.get_cached_by_id(a.id),
1385 %User{} = b <- User.get_cached_by_id(b.id) do
1393 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1394 with :ok <- :timer.sleep(timeout),
1395 %User{} = a <- User.get_cached_by_id(a.id),
1396 %User{} = b <- User.get_cached_by_id(b.id) do
1404 def parse_bio(bio) when is_binary(bio) and bio != "" do
1406 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1410 def parse_bio(_), do: ""
1412 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1413 # TODO: get profile URLs other than user.ap_id
1414 profile_urls = [user.ap_id]
1417 |> CommonUtils.format_input("text/plain",
1418 mentions_format: :full,
1419 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1424 def parse_bio(_, _), do: ""
1426 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1427 Repo.transaction(fn ->
1428 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1432 def tag(nickname, tags) when is_binary(nickname),
1433 do: tag(get_by_nickname(nickname), tags)
1435 def tag(%User{} = user, tags),
1436 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1438 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1439 Repo.transaction(fn ->
1440 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1444 def untag(nickname, tags) when is_binary(nickname),
1445 do: untag(get_by_nickname(nickname), tags)
1447 def untag(%User{} = user, tags),
1448 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1450 defp update_tags(%User{} = user, new_tags) do
1451 {:ok, updated_user} =
1453 |> change(%{tags: new_tags})
1454 |> update_and_set_cache()
1459 defp normalize_tags(tags) do
1462 |> Enum.map(&String.downcase(&1))
1465 defp local_nickname_regex do
1466 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1467 @extended_local_nickname_regex
1469 @strict_local_nickname_regex
1473 def local_nickname(nickname_or_mention) do
1476 |> String.split("@")
1480 def full_nickname(nickname_or_mention),
1481 do: String.trim_leading(nickname_or_mention, "@")
1483 def error_user(ap_id) do
1488 nickname: "erroruser@example.com",
1489 inserted_at: NaiveDateTime.utc_now()
1493 @spec all_superusers() :: [User.t()]
1494 def all_superusers do
1495 User.Query.build(%{super_users: true, local: true, deactivated: false})
1499 def showing_reblogs?(%User{} = user, %User{} = target) do
1500 target.ap_id not in user.info.muted_reblogs
1504 The function returns a query to get users with no activity for given interval of days.
1505 Inactive users are those who didn't read any notification, or had any activity where
1506 the user is the activity's actor, during `inactivity_threshold` days.
1507 Deactivated users will not appear in this list.
1511 iex> Pleroma.User.list_inactive_users()
1514 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1515 def list_inactive_users_query(inactivity_threshold \\ 7) do
1516 negative_inactivity_threshold = -inactivity_threshold
1517 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1518 # Subqueries are not supported in `where` clauses, join gets too complicated.
1519 has_read_notifications =
1520 from(n in Pleroma.Notification,
1521 where: n.seen == true,
1523 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1526 |> Pleroma.Repo.all()
1528 from(u in Pleroma.User,
1529 left_join: a in Pleroma.Activity,
1530 on: u.ap_id == a.actor,
1531 where: not is_nil(u.nickname),
1532 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1533 where: u.id not in ^has_read_notifications,
1536 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1537 is_nil(max(a.inserted_at))
1542 Enable or disable email notifications for user
1546 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1547 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1549 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1550 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1552 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1553 {:ok, t()} | {:error, Ecto.Changeset.t()}
1554 def switch_email_notifications(user, type, status) do
1555 info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})
1558 |> put_embed(:info, info)
1559 |> update_and_set_cache()
1563 Set `last_digest_emailed_at` value for the user to current time
1565 @spec touch_last_digest_emailed_at(t()) :: t()
1566 def touch_last_digest_emailed_at(user) do
1567 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1569 {:ok, updated_user} =
1571 |> change(%{last_digest_emailed_at: now})
1572 |> update_and_set_cache()
1577 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1578 def toggle_confirmation(%User{} = user) do
1579 need_confirmation? = !user.info.confirmation_pending
1582 User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
1586 |> put_embed(:info, info_changeset)
1587 |> update_and_set_cache()
1590 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1594 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1595 # use instance-default
1596 config = Pleroma.Config.get([:assets, :mascots])
1597 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1598 mascot = Keyword.get(config, default_mascot)
1601 "id" => "default-mascot",
1602 "url" => mascot[:url],
1603 "preview_url" => mascot[:url],
1605 "mime_type" => mascot[:mime_type]
1610 def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
1612 def ensure_keys_present(%User{} = user) do
1613 with {:ok, pem} <- Keys.generate_rsa_pem() do
1615 |> cast(%{keys: pem}, [:keys])
1616 |> validate_required([:keys])
1617 |> update_and_set_cache()
1621 def get_ap_ids_by_nicknames(nicknames) do
1623 where: u.nickname in ^nicknames,
1629 defdelegate search(query, opts \\ []), to: User.Search
1631 defp put_password_hash(
1632 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1634 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1637 defp put_password_hash(changeset), do: changeset
1639 def is_internal_user?(%User{nickname: nil}), do: true
1640 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1641 def is_internal_user?(_), do: false
1643 def change_email(user, email) do
1645 |> cast(%{email: email}, [:email])
1646 |> validate_required([:email])
1647 |> unique_constraint(:email)
1648 |> validate_format(:email, @email_regex)
1649 |> update_and_set_cache()