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 defdelegate worker_args(queue), to: Pleroma.Workers.Helper
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} <- autofollow_users(user),
337 {:ok, user} <- set_cache(user),
338 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
339 {:ok, _} <- try_send_confirmation_email(user) do
344 def try_send_confirmation_email(%User{} = user) do
345 if user.info.confirmation_pending &&
346 Pleroma.Config.get([:instance, :account_activation_required]) do
348 |> Pleroma.Emails.UserEmail.account_confirmation_email()
349 |> Pleroma.Emails.Mailer.deliver_async()
357 def needs_update?(%User{local: true}), do: false
359 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
361 def needs_update?(%User{local: false} = user) do
362 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
365 def needs_update?(_), do: true
367 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
368 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
372 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
373 follow(follower, followed)
376 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
377 if not User.ap_enabled?(followed) do
378 follow(follower, followed)
384 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
385 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
386 def follow_all(follower, followeds) do
389 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
390 |> Enum.map(fn %{follower_address: fa} -> fa end)
394 where: u.id == ^follower.id,
399 "array(select distinct unnest (array_cat(?, ?)))",
408 {1, [follower]} = Repo.update_all(q, [])
410 Enum.each(followeds, fn followed ->
411 update_follower_count(followed)
417 def follow(%User{} = follower, %User{info: info} = followed) do
418 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
419 ap_followers = followed.follower_address
423 {:error, "Could not follow user: You are deactivated."}
425 deny_follow_blocked and blocks?(followed, follower) ->
426 {:error, "Could not follow user: #{followed.nickname} blocked you."}
429 if !followed.local && follower.local && !ap_enabled?(followed) do
430 Websub.subscribe(follower, followed)
435 where: u.id == ^follower.id,
436 update: [push: [following: ^ap_followers]],
440 {1, [follower]} = Repo.update_all(q, [])
442 follower = maybe_update_following_count(follower)
444 {:ok, _} = update_follower_count(followed)
450 def unfollow(%User{} = follower, %User{} = followed) do
451 ap_followers = followed.follower_address
453 if following?(follower, followed) and follower.ap_id != followed.ap_id do
456 where: u.id == ^follower.id,
457 update: [pull: [following: ^ap_followers]],
461 {1, [follower]} = Repo.update_all(q, [])
463 follower = maybe_update_following_count(follower)
465 {:ok, followed} = update_follower_count(followed)
469 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
471 {:error, "Not subscribed!"}
475 @spec following?(User.t(), User.t()) :: boolean
476 def following?(%User{} = follower, %User{} = followed) do
477 Enum.member?(follower.following, followed.follower_address)
480 def locked?(%User{} = user) do
481 user.info.locked || false
485 Repo.get_by(User, id: id)
488 def get_by_ap_id(ap_id) do
489 Repo.get_by(User, ap_id: ap_id)
492 def get_all_by_ap_id(ap_ids) do
493 from(u in __MODULE__,
494 where: u.ap_id in ^ap_ids
499 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
500 # of the ap_id and the domain and tries to get that user
501 def get_by_guessed_nickname(ap_id) do
502 domain = URI.parse(ap_id).host
503 name = List.last(String.split(ap_id, "/"))
504 nickname = "#{name}@#{domain}"
506 get_cached_by_nickname(nickname)
509 def set_cache({:ok, user}), do: set_cache(user)
510 def set_cache({:error, err}), do: {:error, err}
512 def set_cache(%User{} = user) do
513 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
514 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
515 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
519 def update_and_set_cache(changeset) do
520 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
527 def invalidate_cache(user) do
528 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
529 Cachex.del(:user_cache, "nickname:#{user.nickname}")
530 Cachex.del(:user_cache, "user_info:#{user.id}")
533 def get_cached_by_ap_id(ap_id) do
534 key = "ap_id:#{ap_id}"
535 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
538 def get_cached_by_id(id) do
542 Cachex.fetch!(:user_cache, key, fn _ ->
546 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
547 {:commit, user.ap_id}
553 get_cached_by_ap_id(ap_id)
556 def get_cached_by_nickname(nickname) do
557 key = "nickname:#{nickname}"
559 Cachex.fetch!(:user_cache, key, fn ->
560 user_result = get_or_fetch_by_nickname(nickname)
563 {:ok, user} -> {:commit, user}
564 {:error, _error} -> {:ignore, nil}
569 def get_cached_by_nickname_or_id(nickname_or_id) do
570 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
573 def get_by_nickname(nickname) do
574 Repo.get_by(User, nickname: nickname) ||
575 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
576 Repo.get_by(User, nickname: local_nickname(nickname))
580 def get_by_email(email), do: Repo.get_by(User, email: email)
582 def get_by_nickname_or_email(nickname_or_email) do
583 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
586 def get_cached_user_info(user) do
587 key = "user_info:#{user.id}"
588 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
591 def fetch_by_nickname(nickname) do
592 ap_try = ActivityPub.make_user_from_nickname(nickname)
595 {:ok, user} -> {:ok, user}
596 _ -> OStatus.make_user(nickname)
600 def get_or_fetch_by_nickname(nickname) do
601 with %User{} = user <- get_by_nickname(nickname) do
605 with [_nick, _domain] <- String.split(nickname, "@"),
606 {:ok, user} <- fetch_by_nickname(nickname) do
607 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
608 fetch_initial_posts(user)
613 _e -> {:error, "not found " <> nickname}
618 @doc "Fetch some posts when the user has just been federated with"
619 def fetch_initial_posts(user) do
620 %{"op" => "fetch_initial_posts", "user_id" => user.id}
621 |> BackgroundWorker.new(worker_args(:background))
625 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
626 def get_followers_query(%User{} = user, nil) do
627 User.Query.build(%{followers: user, deactivated: false})
630 def get_followers_query(user, page) do
631 from(u in get_followers_query(user, nil))
632 |> User.Query.paginate(page, 20)
635 @spec get_followers_query(User.t()) :: Ecto.Query.t()
636 def get_followers_query(user), do: get_followers_query(user, nil)
638 @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
639 def get_followers(user, page \\ nil) do
640 q = get_followers_query(user, page)
645 @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
646 def get_external_followers(user, page \\ nil) do
649 |> get_followers_query(page)
650 |> User.Query.build(%{external: true})
655 def get_followers_ids(user, page \\ nil) do
656 q = get_followers_query(user, page)
658 Repo.all(from(u in q, select: u.id))
661 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
662 def get_friends_query(%User{} = user, nil) do
663 User.Query.build(%{friends: user, deactivated: false})
666 def get_friends_query(user, page) do
667 from(u in get_friends_query(user, nil))
668 |> User.Query.paginate(page, 20)
671 @spec get_friends_query(User.t()) :: Ecto.Query.t()
672 def get_friends_query(user), do: get_friends_query(user, nil)
674 def get_friends(user, page \\ nil) do
675 q = get_friends_query(user, page)
680 def get_friends_ids(user, page \\ nil) do
681 q = get_friends_query(user, page)
683 Repo.all(from(u in q, select: u.id))
686 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
687 def get_follow_requests(%User{} = user) do
689 Activity.follow_requests_for_actor(user)
690 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
691 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
692 |> group_by([a, u], u.id)
699 def increase_note_count(%User{} = user) do
701 |> where(id: ^user.id)
706 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
713 |> Repo.update_all([])
715 {1, [user]} -> set_cache(user)
720 def decrease_note_count(%User{} = user) do
722 |> where(id: ^user.id)
727 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
734 |> Repo.update_all([])
736 {1, [user]} -> set_cache(user)
741 def update_note_count(%User{} = user) do
745 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
749 note_count = Repo.one(note_count_query)
751 info_cng = User.Info.set_note_count(user.info, note_count)
755 |> put_embed(:info, info_cng)
756 |> update_and_set_cache()
759 @spec maybe_fetch_follow_information(User.t()) :: User.t()
760 def maybe_fetch_follow_information(user) do
761 with {:ok, user} <- fetch_follow_information(user) do
765 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
771 def fetch_follow_information(user) do
772 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
773 info_cng = User.Info.follow_information_update(user.info, info)
778 |> put_embed(:info, info_cng)
780 update_and_set_cache(changeset)
787 def update_follower_count(%User{} = user) do
788 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
789 follower_count_query =
790 User.Query.build(%{followers: user, deactivated: false})
791 |> select([u], %{count: count(u.id)})
794 |> where(id: ^user.id)
795 |> join(:inner, [u], s in subquery(follower_count_query))
800 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
807 |> Repo.update_all([])
809 {1, [user]} -> set_cache(user)
813 {:ok, maybe_fetch_follow_information(user)}
817 @spec maybe_update_following_count(User.t()) :: User.t()
818 def maybe_update_following_count(%User{local: false} = user) do
819 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
820 maybe_fetch_follow_information(user)
826 def maybe_update_following_count(user), do: user
828 def remove_duplicated_following(%User{following: following} = user) do
829 uniq_following = Enum.uniq(following)
831 if length(following) == length(uniq_following) do
835 |> update_changeset(%{following: uniq_following})
836 |> update_and_set_cache()
840 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
841 def get_users_from_set(ap_ids, local_only \\ true) do
842 criteria = %{ap_id: ap_ids, deactivated: false}
843 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
845 User.Query.build(criteria)
849 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
850 def get_recipients_from_activity(%Activity{recipients: to}) do
851 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
855 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
856 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
860 User.Info.add_to_mutes(info, ap_id)
861 |> User.Info.add_to_muted_notifications(info, ap_id, notifications?)
865 |> put_embed(:info, info_cng)
867 update_and_set_cache(cng)
870 def unmute(muter, %{ap_id: ap_id}) do
874 User.Info.remove_from_mutes(info, ap_id)
875 |> User.Info.remove_from_muted_notifications(info, ap_id)
879 |> put_embed(:info, info_cng)
881 update_and_set_cache(cng)
884 def subscribe(subscriber, %{ap_id: ap_id}) do
885 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
887 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
888 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
891 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
895 |> User.Info.add_to_subscribers(subscriber.ap_id)
898 |> put_embed(:info, info_cng)
899 |> update_and_set_cache()
904 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
905 with %User{} = user <- get_cached_by_ap_id(ap_id) do
908 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
911 |> put_embed(:info, info_cng)
912 |> update_and_set_cache()
916 def block(blocker, %User{ap_id: ap_id} = blocked) do
917 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
919 if following?(blocker, blocked) do
920 {:ok, blocker, _} = unfollow(blocker, blocked)
926 # clear any requested follows as well
928 case CommonAPI.reject_follow_request(blocked, blocker) do
929 {:ok, %User{} = updated_blocked} -> updated_blocked
934 if subscribed_to?(blocked, blocker) do
935 {:ok, blocker} = unsubscribe(blocked, blocker)
941 if following?(blocked, blocker) do
942 unfollow(blocked, blocker)
945 {:ok, blocker} = update_follower_count(blocker)
949 |> User.Info.add_to_block(ap_id)
953 |> put_embed(:info, info_cng)
955 update_and_set_cache(cng)
958 # helper to handle the block given only an actor's AP id
959 def block(blocker, %{ap_id: ap_id}) do
960 block(blocker, get_cached_by_ap_id(ap_id))
963 def unblock(blocker, %{ap_id: ap_id}) do
966 |> User.Info.remove_from_block(ap_id)
970 |> put_embed(:info, info_cng)
972 update_and_set_cache(cng)
975 def mutes?(nil, _), do: false
976 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
978 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
979 def muted_notifications?(nil, _), do: false
981 def muted_notifications?(user, %{ap_id: ap_id}),
982 do: Enum.member?(user.info.muted_notifications, ap_id)
984 def blocks?(%User{} = user, %User{} = target) do
985 blocks_ap_id?(user, target) || blocks_domain?(user, target)
988 def blocks?(nil, _), do: false
990 def blocks_ap_id?(%User{} = user, %User{} = target) do
991 Enum.member?(user.info.blocks, target.ap_id)
994 def blocks_ap_id?(_, _), do: false
996 def blocks_domain?(%User{} = user, %User{} = target) do
997 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
998 %{host: host} = URI.parse(target.ap_id)
999 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1002 def blocks_domain?(_, _), do: false
1004 def subscribed_to?(user, %{ap_id: ap_id}) do
1005 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1006 Enum.member?(target.info.subscribers, user.ap_id)
1010 @spec muted_users(User.t()) :: [User.t()]
1011 def muted_users(user) do
1012 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
1016 @spec blocked_users(User.t()) :: [User.t()]
1017 def blocked_users(user) do
1018 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
1022 @spec subscribers(User.t()) :: [User.t()]
1023 def subscribers(user) do
1024 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
1028 def block_domain(user, domain) do
1031 |> User.Info.add_to_domain_block(domain)
1035 |> put_embed(:info, info_cng)
1037 update_and_set_cache(cng)
1040 def unblock_domain(user, domain) do
1043 |> User.Info.remove_from_domain_block(domain)
1047 |> put_embed(:info, info_cng)
1049 update_and_set_cache(cng)
1052 def deactivate_async(user, status \\ true) do
1053 %{"op" => "deactivate_user", "user_id" => user.id, "status" => status}
1054 |> BackgroundWorker.new(worker_args(:background))
1058 def deactivate(%User{} = user, status \\ true) do
1059 info_cng = User.Info.set_activation_status(user.info, status)
1061 with {:ok, friends} <- User.get_friends(user),
1062 {:ok, followers} <- User.get_followers(user),
1066 |> put_embed(:info, info_cng)
1067 |> update_and_set_cache() do
1068 Enum.each(followers, &invalidate_cache(&1))
1069 Enum.each(friends, &update_follower_count(&1))
1075 def update_notification_settings(%User{} = user, settings \\ %{}) do
1076 info_changeset = User.Info.update_notification_settings(user.info, settings)
1079 |> put_embed(:info, info_changeset)
1080 |> update_and_set_cache()
1083 def delete(%User{} = user) do
1084 %{"op" => "delete_user", "user_id" => user.id}
1085 |> BackgroundWorker.new(worker_args(:background))
1089 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1090 def perform(:delete, %User{} = user) do
1091 {:ok, _user} = ActivityPub.delete(user)
1093 # Remove all relationships
1094 {:ok, followers} = User.get_followers(user)
1096 Enum.each(followers, fn follower ->
1097 ActivityPub.unfollow(follower, user)
1098 User.unfollow(follower, user)
1101 {:ok, friends} = User.get_friends(user)
1103 Enum.each(friends, fn followed ->
1104 ActivityPub.unfollow(user, followed)
1105 User.unfollow(user, followed)
1108 delete_user_activities(user)
1109 invalidate_cache(user)
1113 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1114 def perform(:fetch_initial_posts, %User{} = user) do
1115 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1118 # Insert all the posts in reverse order, so they're in the right order on the timeline
1119 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
1120 &Pleroma.Web.Federator.incoming_ap_doc/1
1126 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1128 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1129 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1130 when is_list(blocked_identifiers) do
1132 blocked_identifiers,
1133 fn blocked_identifier ->
1134 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1135 {:ok, blocker} <- block(blocker, blocked),
1136 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1140 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1147 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1148 def perform(:follow_import, %User{} = follower, followed_identifiers)
1149 when is_list(followed_identifiers) do
1151 followed_identifiers,
1152 fn followed_identifier ->
1153 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1154 {:ok, follower} <- maybe_direct_follow(follower, followed),
1155 {:ok, _} <- ActivityPub.follow(follower, followed) do
1159 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1166 @spec external_users_query() :: Ecto.Query.t()
1167 def external_users_query do
1175 @spec external_users(keyword()) :: [User.t()]
1176 def external_users(opts \\ []) do
1178 external_users_query()
1179 |> select([u], struct(u, [:id, :ap_id, :info]))
1183 do: where(query, [u], u.id > ^opts[:max_id]),
1188 do: limit(query, ^opts[:limit]),
1194 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1196 "op" => "blocks_import",
1197 "blocker_id" => blocker.id,
1198 "blocked_identifiers" => blocked_identifiers
1200 |> BackgroundWorker.new(worker_args(:background))
1204 def follow_import(%User{} = follower, followed_identifiers)
1205 when is_list(followed_identifiers) do
1207 "op" => "follow_import",
1208 "follower_id" => follower.id,
1209 "followed_identifiers" => followed_identifiers
1211 |> BackgroundWorker.new(worker_args(:background))
1215 def delete_user_activities(%User{ap_id: ap_id} = user) do
1217 |> Activity.query_by_actor()
1218 |> RepoStreamer.chunk_stream(50)
1219 |> Stream.each(fn activities ->
1220 Enum.each(activities, &delete_activity(&1))
1227 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1229 |> Object.normalize()
1230 |> ActivityPub.delete()
1233 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1234 user = get_cached_by_ap_id(activity.actor)
1235 object = Object.normalize(activity)
1237 ActivityPub.unlike(user, object)
1240 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1241 user = get_cached_by_ap_id(activity.actor)
1242 object = Object.normalize(activity)
1244 ActivityPub.unannounce(user, object)
1247 defp delete_activity(_activity), do: "Doing nothing"
1249 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1250 Pleroma.HTML.Scrubber.TwitterText
1253 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1255 def fetch_by_ap_id(ap_id) do
1256 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1263 case OStatus.make_user(ap_id) do
1264 {:ok, user} -> {:ok, user}
1265 _ -> {:error, "Could not fetch by AP id"}
1270 def get_or_fetch_by_ap_id(ap_id) do
1271 user = get_cached_by_ap_id(ap_id)
1273 if !is_nil(user) and !User.needs_update?(user) do
1276 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1277 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1279 resp = fetch_by_ap_id(ap_id)
1281 if should_fetch_initial do
1282 with {:ok, %User{} = user} <- resp do
1283 fetch_initial_posts(user)
1291 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1292 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1293 if user = get_cached_by_ap_id(uri) do
1297 %User{info: %User.Info{}}
1298 |> cast(%{}, [:ap_id, :nickname, :local])
1299 |> put_change(:ap_id, uri)
1300 |> put_change(:nickname, nickname)
1301 |> put_change(:local, true)
1302 |> put_change(:follower_address, uri <> "/followers")
1304 {:ok, user} = Repo.insert(changes)
1310 def public_key_from_info(%{
1311 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1315 |> :public_key.pem_decode()
1317 |> :public_key.pem_entry_decode()
1323 def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
1324 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1327 def public_key_from_info(_), do: {:error, "not found key"}
1329 def get_public_key_for_ap_id(ap_id) do
1330 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1331 {:ok, public_key} <- public_key_from_info(user.info) do
1338 defp blank?(""), do: nil
1339 defp blank?(n), do: n
1341 def insert_or_update_user(data) do
1343 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1344 |> remote_user_creation()
1345 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1349 def ap_enabled?(%User{local: true}), do: true
1350 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1351 def ap_enabled?(_), do: false
1353 @doc "Gets or fetch a user by uri or nickname."
1354 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1355 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1356 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1358 # wait a period of time and return newest version of the User structs
1359 # this is because we have synchronous follow APIs and need to simulate them
1360 # with an async handshake
1361 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1362 with %User{} = a <- User.get_cached_by_id(a.id),
1363 %User{} = b <- User.get_cached_by_id(b.id) do
1371 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1372 with :ok <- :timer.sleep(timeout),
1373 %User{} = a <- User.get_cached_by_id(a.id),
1374 %User{} = b <- User.get_cached_by_id(b.id) do
1382 def parse_bio(bio) when is_binary(bio) and bio != "" do
1384 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1388 def parse_bio(_), do: ""
1390 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1391 # TODO: get profile URLs other than user.ap_id
1392 profile_urls = [user.ap_id]
1395 |> CommonUtils.format_input("text/plain",
1396 mentions_format: :full,
1397 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1402 def parse_bio(_, _), do: ""
1404 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1405 Repo.transaction(fn ->
1406 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1410 def tag(nickname, tags) when is_binary(nickname),
1411 do: tag(get_by_nickname(nickname), tags)
1413 def tag(%User{} = user, tags),
1414 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1416 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1417 Repo.transaction(fn ->
1418 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1422 def untag(nickname, tags) when is_binary(nickname),
1423 do: untag(get_by_nickname(nickname), tags)
1425 def untag(%User{} = user, tags),
1426 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1428 defp update_tags(%User{} = user, new_tags) do
1429 {:ok, updated_user} =
1431 |> change(%{tags: new_tags})
1432 |> update_and_set_cache()
1437 defp normalize_tags(tags) do
1440 |> Enum.map(&String.downcase(&1))
1443 defp local_nickname_regex do
1444 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1445 @extended_local_nickname_regex
1447 @strict_local_nickname_regex
1451 def local_nickname(nickname_or_mention) do
1454 |> String.split("@")
1458 def full_nickname(nickname_or_mention),
1459 do: String.trim_leading(nickname_or_mention, "@")
1461 def error_user(ap_id) do
1466 nickname: "erroruser@example.com",
1467 inserted_at: NaiveDateTime.utc_now()
1471 @spec all_superusers() :: [User.t()]
1472 def all_superusers do
1473 User.Query.build(%{super_users: true, local: true, deactivated: false})
1477 def showing_reblogs?(%User{} = user, %User{} = target) do
1478 target.ap_id not in user.info.muted_reblogs
1482 The function returns a query to get users with no activity for given interval of days.
1483 Inactive users are those who didn't read any notification, or had any activity where
1484 the user is the activity's actor, during `inactivity_threshold` days.
1485 Deactivated users will not appear in this list.
1489 iex> Pleroma.User.list_inactive_users()
1492 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1493 def list_inactive_users_query(inactivity_threshold \\ 7) do
1494 negative_inactivity_threshold = -inactivity_threshold
1495 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1496 # Subqueries are not supported in `where` clauses, join gets too complicated.
1497 has_read_notifications =
1498 from(n in Pleroma.Notification,
1499 where: n.seen == true,
1501 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1504 |> Pleroma.Repo.all()
1506 from(u in Pleroma.User,
1507 left_join: a in Pleroma.Activity,
1508 on: u.ap_id == a.actor,
1509 where: not is_nil(u.nickname),
1510 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1511 where: u.id not in ^has_read_notifications,
1514 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1515 is_nil(max(a.inserted_at))
1520 Enable or disable email notifications for user
1524 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1525 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1527 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1528 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1530 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1531 {:ok, t()} | {:error, Ecto.Changeset.t()}
1532 def switch_email_notifications(user, type, status) do
1533 info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})
1536 |> put_embed(:info, info)
1537 |> update_and_set_cache()
1541 Set `last_digest_emailed_at` value for the user to current time
1543 @spec touch_last_digest_emailed_at(t()) :: t()
1544 def touch_last_digest_emailed_at(user) do
1545 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1547 {:ok, updated_user} =
1549 |> change(%{last_digest_emailed_at: now})
1550 |> update_and_set_cache()
1555 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1556 def toggle_confirmation(%User{} = user) do
1557 need_confirmation? = !user.info.confirmation_pending
1560 User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
1564 |> put_embed(:info, info_changeset)
1565 |> update_and_set_cache()
1568 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1572 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1573 # use instance-default
1574 config = Pleroma.Config.get([:assets, :mascots])
1575 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1576 mascot = Keyword.get(config, default_mascot)
1579 "id" => "default-mascot",
1580 "url" => mascot[:url],
1581 "preview_url" => mascot[:url],
1583 "mime_type" => mascot[:mime_type]
1588 def ensure_keys_present(%User{info: info} = user) do
1592 {:ok, pem} = Keys.generate_rsa_pem()
1595 |> Ecto.Changeset.change()
1596 |> Ecto.Changeset.put_embed(:info, User.Info.set_keys(info, pem))
1597 |> update_and_set_cache()
1601 def get_ap_ids_by_nicknames(nicknames) do
1603 where: u.nickname in ^nicknames,
1609 defdelegate search(query, opts \\ []), to: User.Search
1611 defp put_password_hash(
1612 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1614 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1617 defp put_password_hash(changeset), do: changeset
1619 def is_internal_user?(%User{nickname: nil}), do: true
1620 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1621 def is_internal_user?(_), do: false