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.Utils, as: CommonUtils
25 alias Pleroma.Web.OAuth
26 alias Pleroma.Web.OStatus
27 alias Pleroma.Web.RelMe
28 alias Pleroma.Web.Websub
29 alias Pleroma.Workers.BackgroundWorker
33 @type t :: %__MODULE__{}
35 @primary_key {:id, Pleroma.FlakeId, 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_-]+$/
43 defdelegate worker_args(queue), to: Pleroma.Workers.Helper
47 field(:email, :string)
49 field(:nickname, :string)
50 field(:password_hash, :string)
51 field(:password, :string, virtual: true)
52 field(:password_confirmation, :string, virtual: true)
53 field(:following, {:array, :string}, default: [])
54 field(:ap_id, :string)
56 field(:local, :boolean, default: true)
57 field(:follower_address, :string)
58 field(:following_address, :string)
59 field(:search_rank, :float, virtual: true)
60 field(:search_type, :integer, virtual: true)
61 field(:tags, {:array, :string}, default: [])
62 field(:last_refreshed_at, :naive_datetime_usec)
63 field(:last_digest_emailed_at, :naive_datetime)
64 has_many(:notifications, Notification)
65 has_many(:registrations, Registration)
66 embeds_one(:info, User.Info)
71 def auth_active?(%User{info: %User.Info{confirmation_pending: true}}),
72 do: !Pleroma.Config.get([:instance, :account_activation_required])
74 def auth_active?(%User{}), do: true
76 def visible_for?(user, for_user \\ nil)
78 def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
80 def visible_for?(%User{} = user, for_user) do
81 auth_active?(user) || superuser?(for_user)
84 def visible_for?(_, _), do: false
86 def superuser?(%User{local: true, info: %User.Info{is_admin: true}}), do: true
87 def superuser?(%User{local: true, info: %User.Info{is_moderator: true}}), do: true
88 def superuser?(_), do: false
90 def avatar_url(user, options \\ []) do
92 %{"url" => [%{"href" => href} | _]} -> href
93 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
97 def banner_url(user, options \\ []) do
98 case user.info.banner do
99 %{"url" => [%{"href" => href} | _]} -> href
100 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
104 def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url
105 def profile_url(%User{ap_id: ap_id}), do: ap_id
106 def profile_url(_), do: nil
108 def ap_id(%User{nickname: nickname}) do
109 "#{Web.base_url()}/users/#{nickname}"
112 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
113 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
115 @spec ap_following(User.t()) :: Sring.t()
116 def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
117 def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
119 def user_info(%User{} = user, args \\ %{}) do
121 if args[:following_count],
122 do: args[:following_count],
123 else: user.info.following_count || following_count(user)
126 if args[:follower_count], do: args[:follower_count], else: user.info.follower_count
129 note_count: user.info.note_count,
130 locked: user.info.locked,
131 confirmation_pending: user.info.confirmation_pending,
132 default_scope: user.info.default_scope
134 |> Map.put(:following_count, following_count)
135 |> Map.put(:follower_count, follower_count)
138 def set_info_cache(user, args) do
139 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user, args))
142 @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
143 def restrict_deactivated(query) do
145 where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info)
149 def following_count(%User{following: []}), do: 0
151 def following_count(%User{} = user) do
153 |> get_friends_query()
154 |> Repo.aggregate(:count, :id)
157 def remote_user_creation(params) do
160 |> Map.put(:info, params[:info] || %{})
162 info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])
166 |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
167 |> validate_required([:name, :ap_id])
168 |> unique_constraint(:nickname)
169 |> validate_format(:nickname, @email_regex)
170 |> validate_length(:bio, max: 5000)
171 |> validate_length(:name, max: 100)
172 |> put_change(:local, false)
173 |> put_embed(:info, info_cng)
176 case info_cng.changes[:source_data] do
177 %{"followers" => followers, "following" => following} ->
179 |> put_change(:follower_address, followers)
180 |> put_change(:following_address, following)
183 followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
186 |> put_change(:follower_address, followers)
193 def update_changeset(struct, params \\ %{}) do
195 |> cast(params, [:bio, :name, :avatar, :following])
196 |> unique_constraint(:nickname)
197 |> validate_format(:nickname, local_nickname_regex())
198 |> validate_length(:bio, max: 5000)
199 |> validate_length(:name, min: 1, max: 100)
202 def upgrade_changeset(struct, params \\ %{}) do
205 |> Map.put(:last_refreshed_at, NaiveDateTime.utc_now())
209 |> User.Info.user_upgrade(params[:info])
220 |> unique_constraint(:nickname)
221 |> validate_format(:nickname, local_nickname_regex())
222 |> validate_length(:bio, max: 5000)
223 |> validate_length(:name, max: 100)
224 |> put_embed(:info, info_cng)
227 def password_update_changeset(struct, params) do
229 |> cast(params, [:password, :password_confirmation])
230 |> validate_required([:password, :password_confirmation])
231 |> validate_confirmation(:password)
235 @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
236 def reset_password(%User{id: user_id} = user, data) do
239 |> Multi.update(:user, password_update_changeset(user, data))
240 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
241 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
243 case Repo.transaction(multi) do
244 {:ok, %{user: user} = _} -> set_cache(user)
245 {:error, _, changeset, _} -> {:error, changeset}
249 def register_changeset(struct, params \\ %{}, opts \\ []) do
251 if is_nil(opts[:need_confirmation]) do
252 Pleroma.Config.get([:instance, :account_activation_required])
254 opts[:need_confirmation]
258 User.Info.confirmation_changeset(%User.Info{}, need_confirmation: need_confirmation?)
262 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
263 |> validate_required([:name, :nickname, :password, :password_confirmation])
264 |> validate_confirmation(:password)
265 |> unique_constraint(:email)
266 |> unique_constraint(:nickname)
267 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
268 |> validate_format(:nickname, local_nickname_regex())
269 |> validate_format(:email, @email_regex)
270 |> validate_length(:bio, max: 1000)
271 |> validate_length(:name, min: 1, max: 100)
272 |> put_change(:info, info_change)
275 if opts[:external] do
278 validate_required(changeset, [:email])
281 if changeset.valid? do
282 ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
283 followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
287 |> put_change(:ap_id, ap_id)
288 |> unique_constraint(:ap_id)
289 |> put_change(:following, [followers])
290 |> put_change(:follower_address, followers)
296 defp autofollow_users(user) do
297 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
300 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
303 follow_all(user, autofollowed_users)
306 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
307 def register(%Ecto.Changeset{} = changeset) do
308 with {:ok, user} <- Repo.insert(changeset),
309 {:ok, user} <- autofollow_users(user),
310 {:ok, user} <- set_cache(user),
311 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
312 {:ok, _} <- try_send_confirmation_email(user) do
317 def try_send_confirmation_email(%User{} = user) do
318 if user.info.confirmation_pending &&
319 Pleroma.Config.get([:instance, :account_activation_required]) do
321 |> Pleroma.Emails.UserEmail.account_confirmation_email()
322 |> Pleroma.Emails.Mailer.deliver_async()
330 def needs_update?(%User{local: true}), do: false
332 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
334 def needs_update?(%User{local: false} = user) do
335 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
338 def needs_update?(_), do: true
340 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
341 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
345 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
346 follow(follower, followed)
349 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
350 if not User.ap_enabled?(followed) do
351 follow(follower, followed)
357 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
358 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
359 def follow_all(follower, followeds) do
362 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
363 |> Enum.map(fn %{follower_address: fa} -> fa end)
367 where: u.id == ^follower.id,
372 "array(select distinct unnest (array_cat(?, ?)))",
381 {1, [follower]} = Repo.update_all(q, [])
383 Enum.each(followeds, fn followed ->
384 update_follower_count(followed)
390 def follow(%User{} = follower, %User{info: info} = followed) do
391 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
392 ap_followers = followed.follower_address
396 {:error, "Could not follow user: You are deactivated."}
398 deny_follow_blocked and blocks?(followed, follower) ->
399 {:error, "Could not follow user: #{followed.nickname} blocked you."}
402 if !followed.local && follower.local && !ap_enabled?(followed) do
403 Websub.subscribe(follower, followed)
408 where: u.id == ^follower.id,
409 update: [push: [following: ^ap_followers]],
413 {1, [follower]} = Repo.update_all(q, [])
415 follower = maybe_update_following_count(follower)
417 {:ok, _} = update_follower_count(followed)
423 def unfollow(%User{} = follower, %User{} = followed) do
424 ap_followers = followed.follower_address
426 if following?(follower, followed) and follower.ap_id != followed.ap_id do
429 where: u.id == ^follower.id,
430 update: [pull: [following: ^ap_followers]],
434 {1, [follower]} = Repo.update_all(q, [])
436 follower = maybe_update_following_count(follower)
438 {:ok, followed} = update_follower_count(followed)
442 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
444 {:error, "Not subscribed!"}
448 @spec following?(User.t(), User.t()) :: boolean
449 def following?(%User{} = follower, %User{} = followed) do
450 Enum.member?(follower.following, followed.follower_address)
453 def locked?(%User{} = user) do
454 user.info.locked || false
458 Repo.get_by(User, id: id)
461 def get_by_ap_id(ap_id) do
462 Repo.get_by(User, ap_id: ap_id)
465 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
466 # of the ap_id and the domain and tries to get that user
467 def get_by_guessed_nickname(ap_id) do
468 domain = URI.parse(ap_id).host
469 name = List.last(String.split(ap_id, "/"))
470 nickname = "#{name}@#{domain}"
472 get_cached_by_nickname(nickname)
475 def set_cache({:ok, user}), do: set_cache(user)
476 def set_cache({:error, err}), do: {:error, err}
478 def set_cache(%User{} = user) do
479 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
480 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
481 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
485 def update_and_set_cache(changeset) do
486 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
493 def invalidate_cache(user) do
494 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
495 Cachex.del(:user_cache, "nickname:#{user.nickname}")
496 Cachex.del(:user_cache, "user_info:#{user.id}")
499 def get_cached_by_ap_id(ap_id) do
500 key = "ap_id:#{ap_id}"
501 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
504 def get_cached_by_id(id) do
508 Cachex.fetch!(:user_cache, key, fn _ ->
512 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
513 {:commit, user.ap_id}
519 get_cached_by_ap_id(ap_id)
522 def get_cached_by_nickname(nickname) do
523 key = "nickname:#{nickname}"
525 Cachex.fetch!(:user_cache, key, fn ->
526 user_result = get_or_fetch_by_nickname(nickname)
529 {:ok, user} -> {:commit, user}
530 {:error, _error} -> {:ignore, nil}
535 def get_cached_by_nickname_or_id(nickname_or_id) do
536 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
539 def get_by_nickname(nickname) do
540 Repo.get_by(User, nickname: nickname) ||
541 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
542 Repo.get_by(User, nickname: local_nickname(nickname))
546 def get_by_email(email), do: Repo.get_by(User, email: email)
548 def get_by_nickname_or_email(nickname_or_email) do
549 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
552 def get_cached_user_info(user) do
553 key = "user_info:#{user.id}"
554 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
557 def fetch_by_nickname(nickname) do
558 ap_try = ActivityPub.make_user_from_nickname(nickname)
561 {:ok, user} -> {:ok, user}
562 _ -> OStatus.make_user(nickname)
566 def get_or_fetch_by_nickname(nickname) do
567 with %User{} = user <- get_by_nickname(nickname) do
571 with [_nick, _domain] <- String.split(nickname, "@"),
572 {:ok, user} <- fetch_by_nickname(nickname) do
573 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
574 fetch_initial_posts(user)
579 _e -> {:error, "not found " <> nickname}
584 @doc "Fetch some posts when the user has just been federated with"
585 def fetch_initial_posts(user) do
586 %{"op" => "fetch_initial_posts", "user_id" => user.id}
587 |> BackgroundWorker.new(worker_args(:background))
591 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
592 def get_followers_query(%User{} = user, nil) do
593 User.Query.build(%{followers: user, deactivated: false})
596 def get_followers_query(user, page) do
597 from(u in get_followers_query(user, nil))
598 |> User.Query.paginate(page, 20)
601 @spec get_followers_query(User.t()) :: Ecto.Query.t()
602 def get_followers_query(user), do: get_followers_query(user, nil)
604 @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
605 def get_followers(user, page \\ nil) do
606 q = get_followers_query(user, page)
611 @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
612 def get_external_followers(user, page \\ nil) do
615 |> get_followers_query(page)
616 |> User.Query.build(%{external: true})
621 def get_followers_ids(user, page \\ nil) do
622 q = get_followers_query(user, page)
624 Repo.all(from(u in q, select: u.id))
627 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
628 def get_friends_query(%User{} = user, nil) do
629 User.Query.build(%{friends: user, deactivated: false})
632 def get_friends_query(user, page) do
633 from(u in get_friends_query(user, nil))
634 |> User.Query.paginate(page, 20)
637 @spec get_friends_query(User.t()) :: Ecto.Query.t()
638 def get_friends_query(user), do: get_friends_query(user, nil)
640 def get_friends(user, page \\ nil) do
641 q = get_friends_query(user, page)
646 def get_friends_ids(user, page \\ nil) do
647 q = get_friends_query(user, page)
649 Repo.all(from(u in q, select: u.id))
652 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
653 def get_follow_requests(%User{} = user) do
655 Activity.follow_requests_for_actor(user)
656 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
657 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
658 |> group_by([a, u], u.id)
665 def increase_note_count(%User{} = user) do
667 |> where(id: ^user.id)
672 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
679 |> Repo.update_all([])
681 {1, [user]} -> set_cache(user)
686 def decrease_note_count(%User{} = user) do
688 |> where(id: ^user.id)
693 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
700 |> Repo.update_all([])
702 {1, [user]} -> set_cache(user)
707 def update_note_count(%User{} = user) do
711 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
715 note_count = Repo.one(note_count_query)
717 info_cng = User.Info.set_note_count(user.info, note_count)
721 |> put_embed(:info, info_cng)
722 |> update_and_set_cache()
725 def maybe_fetch_follow_information(user) do
726 with {:ok, user} <- fetch_follow_information(user) do
730 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
736 def fetch_follow_information(user) do
737 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
738 info_cng = User.Info.follow_information_update(user.info, info)
743 |> put_embed(:info, info_cng)
745 update_and_set_cache(changeset)
752 def update_follower_count(%User{} = user) do
753 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
754 follower_count_query =
755 User.Query.build(%{followers: user, deactivated: false})
756 |> select([u], %{count: count(u.id)})
759 |> where(id: ^user.id)
760 |> join(:inner, [u], s in subquery(follower_count_query))
765 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
772 |> Repo.update_all([])
774 {1, [user]} -> set_cache(user)
778 {:ok, maybe_fetch_follow_information(user)}
782 def maybe_update_following_count(%User{local: false} = user) do
783 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
784 {:ok, maybe_fetch_follow_information(user)}
790 def maybe_update_following_count(user), do: user
792 def remove_duplicated_following(%User{following: following} = user) do
793 uniq_following = Enum.uniq(following)
795 if length(following) == length(uniq_following) do
799 |> update_changeset(%{following: uniq_following})
800 |> update_and_set_cache()
804 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
805 def get_users_from_set(ap_ids, local_only \\ true) do
806 criteria = %{ap_id: ap_ids, deactivated: false}
807 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
809 User.Query.build(criteria)
813 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
814 def get_recipients_from_activity(%Activity{recipients: to}) do
815 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
819 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
820 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
824 User.Info.add_to_mutes(info, ap_id)
825 |> User.Info.add_to_muted_notifications(info, ap_id, notifications?)
829 |> put_embed(:info, info_cng)
831 update_and_set_cache(cng)
834 def unmute(muter, %{ap_id: ap_id}) do
838 User.Info.remove_from_mutes(info, ap_id)
839 |> User.Info.remove_from_muted_notifications(info, ap_id)
843 |> put_embed(:info, info_cng)
845 update_and_set_cache(cng)
848 def subscribe(subscriber, %{ap_id: ap_id}) do
849 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
851 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
852 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
855 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
859 |> User.Info.add_to_subscribers(subscriber.ap_id)
862 |> put_embed(:info, info_cng)
863 |> update_and_set_cache()
868 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
869 with %User{} = user <- get_cached_by_ap_id(ap_id) do
872 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
875 |> put_embed(:info, info_cng)
876 |> update_and_set_cache()
880 def block(blocker, %User{ap_id: ap_id} = blocked) do
881 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
883 if following?(blocker, blocked) do
884 {:ok, blocker, _} = unfollow(blocker, blocked)
891 if subscribed_to?(blocked, blocker) do
892 {:ok, blocker} = unsubscribe(blocked, blocker)
898 if following?(blocked, blocker) do
899 unfollow(blocked, blocker)
902 {:ok, blocker} = update_follower_count(blocker)
906 |> User.Info.add_to_block(ap_id)
910 |> put_embed(:info, info_cng)
912 update_and_set_cache(cng)
915 # helper to handle the block given only an actor's AP id
916 def block(blocker, %{ap_id: ap_id}) do
917 block(blocker, get_cached_by_ap_id(ap_id))
920 def unblock(blocker, %{ap_id: ap_id}) do
923 |> User.Info.remove_from_block(ap_id)
927 |> put_embed(:info, info_cng)
929 update_and_set_cache(cng)
932 def mutes?(nil, _), do: false
933 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
935 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
936 def muted_notifications?(nil, _), do: false
938 def muted_notifications?(user, %{ap_id: ap_id}),
939 do: Enum.member?(user.info.muted_notifications, ap_id)
941 def blocks?(%User{} = user, %User{} = target) do
942 blocks_ap_id?(user, target) || blocks_domain?(user, target)
945 def blocks?(nil, _), do: false
947 def blocks_ap_id?(%User{} = user, %User{} = target) do
948 Enum.member?(user.info.blocks, target.ap_id)
951 def blocks_ap_id?(_, _), do: false
953 def blocks_domain?(%User{} = user, %User{} = target) do
954 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
955 %{host: host} = URI.parse(target.ap_id)
956 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
959 def blocks_domain?(_, _), do: false
961 def subscribed_to?(user, %{ap_id: ap_id}) do
962 with %User{} = target <- get_cached_by_ap_id(ap_id) do
963 Enum.member?(target.info.subscribers, user.ap_id)
967 @spec muted_users(User.t()) :: [User.t()]
968 def muted_users(user) do
969 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
973 @spec blocked_users(User.t()) :: [User.t()]
974 def blocked_users(user) do
975 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
979 @spec subscribers(User.t()) :: [User.t()]
980 def subscribers(user) do
981 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
985 def block_domain(user, domain) do
988 |> User.Info.add_to_domain_block(domain)
992 |> put_embed(:info, info_cng)
994 update_and_set_cache(cng)
997 def unblock_domain(user, domain) do
1000 |> User.Info.remove_from_domain_block(domain)
1004 |> put_embed(:info, info_cng)
1006 update_and_set_cache(cng)
1009 def deactivate_async(user, status \\ true) do
1010 %{"op" => "deactivate_user", "user_id" => user.id, "status" => status}
1011 |> BackgroundWorker.new(worker_args(:background))
1015 def deactivate(%User{} = user, status \\ true) do
1016 info_cng = User.Info.set_activation_status(user.info, status)
1018 with {:ok, friends} <- User.get_friends(user),
1019 {:ok, followers} <- User.get_followers(user),
1023 |> put_embed(:info, info_cng)
1024 |> update_and_set_cache() do
1025 Enum.each(followers, &invalidate_cache(&1))
1026 Enum.each(friends, &update_follower_count(&1))
1032 def update_notification_settings(%User{} = user, settings \\ %{}) do
1033 info_changeset = User.Info.update_notification_settings(user.info, settings)
1036 |> put_embed(:info, info_changeset)
1037 |> update_and_set_cache()
1040 def delete(%User{} = user) do
1041 %{"op" => "delete_user", "user_id" => user.id}
1042 |> BackgroundWorker.new(worker_args(:background))
1046 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1047 def perform(:delete, %User{} = user) do
1048 {:ok, _user} = ActivityPub.delete(user)
1050 # Remove all relationships
1051 {:ok, followers} = User.get_followers(user)
1053 Enum.each(followers, fn follower ->
1054 ActivityPub.unfollow(follower, user)
1055 User.unfollow(follower, user)
1058 {:ok, friends} = User.get_friends(user)
1060 Enum.each(friends, fn followed ->
1061 ActivityPub.unfollow(user, followed)
1062 User.unfollow(user, followed)
1065 delete_user_activities(user)
1066 invalidate_cache(user)
1070 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1071 def perform(:fetch_initial_posts, %User{} = user) do
1072 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1075 # Insert all the posts in reverse order, so they're in the right order on the timeline
1076 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
1077 &Pleroma.Web.Federator.incoming_ap_doc/1
1083 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1085 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1086 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1087 when is_list(blocked_identifiers) do
1089 blocked_identifiers,
1090 fn blocked_identifier ->
1091 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1092 {:ok, blocker} <- block(blocker, blocked),
1093 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1097 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1104 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1105 def perform(:follow_import, %User{} = follower, followed_identifiers)
1106 when is_list(followed_identifiers) do
1108 followed_identifiers,
1109 fn followed_identifier ->
1110 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1111 {:ok, follower} <- maybe_direct_follow(follower, followed),
1112 {:ok, _} <- ActivityPub.follow(follower, followed) do
1116 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1123 @spec external_users_query() :: Ecto.Query.t()
1124 def external_users_query do
1132 @spec external_users(keyword()) :: [User.t()]
1133 def external_users(opts \\ []) do
1135 external_users_query()
1136 |> select([u], struct(u, [:id, :ap_id, :info]))
1140 do: where(query, [u], u.id > ^opts[:max_id]),
1145 do: limit(query, ^opts[:limit]),
1151 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1153 "op" => "blocks_import",
1154 "blocker_id" => blocker.id,
1155 "blocked_identifiers" => blocked_identifiers
1157 |> BackgroundWorker.new(worker_args(:background))
1161 def follow_import(%User{} = follower, followed_identifiers)
1162 when is_list(followed_identifiers) do
1164 "op" => "follow_import",
1165 "follower_id" => follower.id,
1166 "followed_identifiers" => followed_identifiers
1168 |> BackgroundWorker.new(worker_args(:background))
1172 def delete_user_activities(%User{ap_id: ap_id} = user) do
1174 |> Activity.query_by_actor()
1175 |> RepoStreamer.chunk_stream(50)
1176 |> Stream.each(fn activities ->
1177 Enum.each(activities, &delete_activity(&1))
1184 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1186 |> Object.normalize()
1187 |> ActivityPub.delete()
1190 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1191 user = get_cached_by_ap_id(activity.actor)
1192 object = Object.normalize(activity)
1194 ActivityPub.unlike(user, object)
1197 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1198 user = get_cached_by_ap_id(activity.actor)
1199 object = Object.normalize(activity)
1201 ActivityPub.unannounce(user, object)
1204 defp delete_activity(_activity), do: "Doing nothing"
1206 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1207 Pleroma.HTML.Scrubber.TwitterText
1210 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1212 def fetch_by_ap_id(ap_id) do
1213 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1220 case OStatus.make_user(ap_id) do
1221 {:ok, user} -> {:ok, user}
1222 _ -> {:error, "Could not fetch by AP id"}
1227 def get_or_fetch_by_ap_id(ap_id) do
1228 user = get_cached_by_ap_id(ap_id)
1230 if !is_nil(user) and !User.needs_update?(user) do
1233 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1234 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1236 resp = fetch_by_ap_id(ap_id)
1238 if should_fetch_initial do
1239 with {:ok, %User{} = user} <- resp do
1240 fetch_initial_posts(user)
1248 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1249 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1250 if user = get_cached_by_ap_id(uri) do
1254 %User{info: %User.Info{}}
1255 |> cast(%{}, [:ap_id, :nickname, :local])
1256 |> put_change(:ap_id, uri)
1257 |> put_change(:nickname, nickname)
1258 |> put_change(:local, true)
1259 |> put_change(:follower_address, uri <> "/followers")
1261 {:ok, user} = Repo.insert(changes)
1267 def public_key_from_info(%{
1268 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1272 |> :public_key.pem_decode()
1274 |> :public_key.pem_entry_decode()
1280 def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
1281 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1284 def public_key_from_info(_), do: {:error, "not found key"}
1286 def get_public_key_for_ap_id(ap_id) do
1287 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1288 {:ok, public_key} <- public_key_from_info(user.info) do
1295 defp blank?(""), do: nil
1296 defp blank?(n), do: n
1298 def insert_or_update_user(data) do
1300 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1301 |> remote_user_creation()
1302 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1306 def ap_enabled?(%User{local: true}), do: true
1307 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1308 def ap_enabled?(_), do: false
1310 @doc "Gets or fetch a user by uri or nickname."
1311 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1312 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1313 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1315 # wait a period of time and return newest version of the User structs
1316 # this is because we have synchronous follow APIs and need to simulate them
1317 # with an async handshake
1318 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1319 with %User{} = a <- User.get_cached_by_id(a.id),
1320 %User{} = b <- User.get_cached_by_id(b.id) do
1328 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1329 with :ok <- :timer.sleep(timeout),
1330 %User{} = a <- User.get_cached_by_id(a.id),
1331 %User{} = b <- User.get_cached_by_id(b.id) do
1339 def parse_bio(bio) when is_binary(bio) and bio != "" do
1341 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1345 def parse_bio(_), do: ""
1347 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1348 # TODO: get profile URLs other than user.ap_id
1349 profile_urls = [user.ap_id]
1352 |> CommonUtils.format_input("text/plain",
1353 mentions_format: :full,
1354 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1359 def parse_bio(_, _), do: ""
1361 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1362 Repo.transaction(fn ->
1363 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1367 def tag(nickname, tags) when is_binary(nickname),
1368 do: tag(get_by_nickname(nickname), tags)
1370 def tag(%User{} = user, tags),
1371 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1373 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1374 Repo.transaction(fn ->
1375 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1379 def untag(nickname, tags) when is_binary(nickname),
1380 do: untag(get_by_nickname(nickname), tags)
1382 def untag(%User{} = user, tags),
1383 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1385 defp update_tags(%User{} = user, new_tags) do
1386 {:ok, updated_user} =
1388 |> change(%{tags: new_tags})
1389 |> update_and_set_cache()
1394 defp normalize_tags(tags) do
1397 |> Enum.map(&String.downcase(&1))
1400 defp local_nickname_regex do
1401 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1402 @extended_local_nickname_regex
1404 @strict_local_nickname_regex
1408 def local_nickname(nickname_or_mention) do
1411 |> String.split("@")
1415 def full_nickname(nickname_or_mention),
1416 do: String.trim_leading(nickname_or_mention, "@")
1418 def error_user(ap_id) do
1423 nickname: "erroruser@example.com",
1424 inserted_at: NaiveDateTime.utc_now()
1428 @spec all_superusers() :: [User.t()]
1429 def all_superusers do
1430 User.Query.build(%{super_users: true, local: true, deactivated: false})
1434 def showing_reblogs?(%User{} = user, %User{} = target) do
1435 target.ap_id not in user.info.muted_reblogs
1439 The function returns a query to get users with no activity for given interval of days.
1440 Inactive users are those who didn't read any notification, or had any activity where
1441 the user is the activity's actor, during `inactivity_threshold` days.
1442 Deactivated users will not appear in this list.
1446 iex> Pleroma.User.list_inactive_users()
1449 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1450 def list_inactive_users_query(inactivity_threshold \\ 7) do
1451 negative_inactivity_threshold = -inactivity_threshold
1452 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1453 # Subqueries are not supported in `where` clauses, join gets too complicated.
1454 has_read_notifications =
1455 from(n in Pleroma.Notification,
1456 where: n.seen == true,
1458 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1461 |> Pleroma.Repo.all()
1463 from(u in Pleroma.User,
1464 left_join: a in Pleroma.Activity,
1465 on: u.ap_id == a.actor,
1466 where: not is_nil(u.nickname),
1467 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1468 where: u.id not in ^has_read_notifications,
1471 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1472 is_nil(max(a.inserted_at))
1477 Enable or disable email notifications for user
1481 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1482 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1484 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1485 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1487 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1488 {:ok, t()} | {:error, Ecto.Changeset.t()}
1489 def switch_email_notifications(user, type, status) do
1490 info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})
1493 |> put_embed(:info, info)
1494 |> update_and_set_cache()
1498 Set `last_digest_emailed_at` value for the user to current time
1500 @spec touch_last_digest_emailed_at(t()) :: t()
1501 def touch_last_digest_emailed_at(user) do
1502 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1504 {:ok, updated_user} =
1506 |> change(%{last_digest_emailed_at: now})
1507 |> update_and_set_cache()
1512 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1513 def toggle_confirmation(%User{} = user) do
1514 need_confirmation? = !user.info.confirmation_pending
1517 User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
1521 |> put_embed(:info, info_changeset)
1522 |> update_and_set_cache()
1525 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1529 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1530 # use instance-default
1531 config = Pleroma.Config.get([:assets, :mascots])
1532 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1533 mascot = Keyword.get(config, default_mascot)
1536 "id" => "default-mascot",
1537 "url" => mascot[:url],
1538 "preview_url" => mascot[:url],
1540 "mime_type" => mascot[:mime_type]
1545 def ensure_keys_present(%User{info: info} = user) do
1549 {:ok, pem} = Keys.generate_rsa_pem()
1552 |> Ecto.Changeset.change()
1553 |> Ecto.Changeset.put_embed(:info, User.Info.set_keys(info, pem))
1554 |> update_and_set_cache()
1558 def get_ap_ids_by_nicknames(nicknames) do
1560 where: u.nickname in ^nicknames,
1566 defdelegate search(query, opts \\ []), to: User.Search
1568 defp put_password_hash(
1569 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1571 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1574 defp put_password_hash(changeset), do: changeset
1576 def is_internal_user?(%User{nickname: nil}), do: true
1577 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1578 def is_internal_user?(_), do: false