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
32 @type t :: %__MODULE__{}
34 @primary_key {:id, Pleroma.FlakeId, autogenerate: true}
36 # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
37 @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])?)*$/
39 @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
40 @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
44 field(:email, :string)
46 field(:nickname, :string)
47 field(:password_hash, :string)
48 field(:password, :string, virtual: true)
49 field(:password_confirmation, :string, virtual: true)
50 field(:following, {:array, :string}, default: [])
51 field(:ap_id, :string)
53 field(:local, :boolean, default: true)
54 field(:follower_address, :string)
55 field(:following_address, :string)
56 field(:search_rank, :float, virtual: true)
57 field(:search_type, :integer, virtual: true)
58 field(:tags, {:array, :string}, default: [])
59 field(:last_refreshed_at, :naive_datetime_usec)
60 field(:last_digest_emailed_at, :naive_datetime)
61 has_many(:notifications, Notification)
62 has_many(:registrations, Registration)
63 embeds_one(:info, User.Info)
68 def auth_active?(%User{info: %User.Info{confirmation_pending: true}}),
69 do: !Pleroma.Config.get([:instance, :account_activation_required])
71 def auth_active?(%User{}), do: true
73 def visible_for?(user, for_user \\ nil)
75 def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
77 def visible_for?(%User{} = user, for_user) do
78 auth_active?(user) || superuser?(for_user)
81 def visible_for?(_, _), do: false
83 def superuser?(%User{local: true, info: %User.Info{is_admin: true}}), do: true
84 def superuser?(%User{local: true, info: %User.Info{is_moderator: true}}), do: true
85 def superuser?(_), do: false
87 def avatar_url(user, options \\ []) do
89 %{"url" => [%{"href" => href} | _]} -> href
90 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
94 def banner_url(user, options \\ []) do
95 case user.info.banner do
96 %{"url" => [%{"href" => href} | _]} -> href
97 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
101 def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url
102 def profile_url(%User{ap_id: ap_id}), do: ap_id
103 def profile_url(_), do: nil
105 def ap_id(%User{nickname: nickname}) do
106 "#{Web.base_url()}/users/#{nickname}"
109 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
110 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
112 @spec ap_following(User.t()) :: Sring.t()
113 def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
114 def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
116 def user_info(%User{} = user, args \\ %{}) do
118 if args[:following_count],
119 do: args[:following_count],
120 else: user.info.following_count || following_count(user)
123 if args[:follower_count], do: args[:follower_count], else: user.info.follower_count
126 note_count: user.info.note_count,
127 locked: user.info.locked,
128 confirmation_pending: user.info.confirmation_pending,
129 default_scope: user.info.default_scope
131 |> Map.put(:following_count, following_count)
132 |> Map.put(:follower_count, follower_count)
135 def set_info_cache(user, args) do
136 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user, args))
139 @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
140 def restrict_deactivated(query) do
142 where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info)
146 def following_count(%User{following: []}), do: 0
148 def following_count(%User{} = user) do
150 |> get_friends_query()
151 |> Repo.aggregate(:count, :id)
154 def remote_user_creation(params) do
157 |> Map.put(:info, params[:info] || %{})
159 info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])
163 |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
164 |> validate_required([:name, :ap_id])
165 |> unique_constraint(:nickname)
166 |> validate_format(:nickname, @email_regex)
167 |> validate_length(:bio, max: 5000)
168 |> validate_length(:name, max: 100)
169 |> put_change(:local, false)
170 |> put_embed(:info, info_cng)
173 case info_cng.changes[:source_data] do
174 %{"followers" => followers, "following" => following} ->
176 |> put_change(:follower_address, followers)
177 |> put_change(:following_address, following)
180 followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
183 |> put_change(:follower_address, followers)
190 def update_changeset(struct, params \\ %{}) do
192 |> cast(params, [:bio, :name, :avatar, :following])
193 |> unique_constraint(:nickname)
194 |> validate_format(:nickname, local_nickname_regex())
195 |> validate_length(:bio, max: 5000)
196 |> validate_length(:name, min: 1, max: 100)
199 def upgrade_changeset(struct, params \\ %{}) do
202 |> Map.put(:last_refreshed_at, NaiveDateTime.utc_now())
206 |> User.Info.user_upgrade(params[:info])
217 |> unique_constraint(:nickname)
218 |> validate_format(:nickname, local_nickname_regex())
219 |> validate_length(:bio, max: 5000)
220 |> validate_length(:name, max: 100)
221 |> put_embed(:info, info_cng)
224 def password_update_changeset(struct, params) do
226 |> cast(params, [:password, :password_confirmation])
227 |> validate_required([:password, :password_confirmation])
228 |> validate_confirmation(:password)
232 @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
233 def reset_password(%User{id: user_id} = user, data) do
236 |> Multi.update(:user, password_update_changeset(user, data))
237 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
238 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
240 case Repo.transaction(multi) do
241 {:ok, %{user: user} = _} -> set_cache(user)
242 {:error, _, changeset, _} -> {:error, changeset}
246 def register_changeset(struct, params \\ %{}, opts \\ []) do
248 if is_nil(opts[:need_confirmation]) do
249 Pleroma.Config.get([:instance, :account_activation_required])
251 opts[:need_confirmation]
255 User.Info.confirmation_changeset(%User.Info{}, need_confirmation: need_confirmation?)
259 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
260 |> validate_required([:name, :nickname, :password, :password_confirmation])
261 |> validate_confirmation(:password)
262 |> unique_constraint(:email)
263 |> unique_constraint(:nickname)
264 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
265 |> validate_format(:nickname, local_nickname_regex())
266 |> validate_format(:email, @email_regex)
267 |> validate_length(:bio, max: 1000)
268 |> validate_length(:name, min: 1, max: 100)
269 |> put_change(:info, info_change)
272 if opts[:external] do
275 validate_required(changeset, [:email])
278 if changeset.valid? do
279 ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
280 followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
284 |> put_change(:ap_id, ap_id)
285 |> unique_constraint(:ap_id)
286 |> put_change(:following, [followers])
287 |> put_change(:follower_address, followers)
293 defp autofollow_users(user) do
294 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
297 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
300 follow_all(user, autofollowed_users)
303 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
304 def register(%Ecto.Changeset{} = changeset) do
305 with {:ok, user} <- Repo.insert(changeset),
306 {:ok, user} <- autofollow_users(user),
307 {:ok, user} <- set_cache(user),
308 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
309 {:ok, _} <- try_send_confirmation_email(user) do
314 def try_send_confirmation_email(%User{} = user) do
315 if user.info.confirmation_pending &&
316 Pleroma.Config.get([:instance, :account_activation_required]) do
318 |> Pleroma.Emails.UserEmail.account_confirmation_email()
319 |> Pleroma.Emails.Mailer.deliver_async()
327 def needs_update?(%User{local: true}), do: false
329 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
331 def needs_update?(%User{local: false} = user) do
332 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
335 def needs_update?(_), do: true
337 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
338 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
342 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
343 follow(follower, followed)
346 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
347 if not User.ap_enabled?(followed) do
348 follow(follower, followed)
354 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
355 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
356 def follow_all(follower, followeds) do
359 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
360 |> Enum.map(fn %{follower_address: fa} -> fa end)
364 where: u.id == ^follower.id,
369 "array(select distinct unnest (array_cat(?, ?)))",
378 {1, [follower]} = Repo.update_all(q, [])
380 Enum.each(followeds, fn followed ->
381 update_follower_count(followed)
387 def follow(%User{} = follower, %User{info: info} = followed) do
388 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
389 ap_followers = followed.follower_address
393 {:error, "Could not follow user: You are deactivated."}
395 deny_follow_blocked and blocks?(followed, follower) ->
396 {:error, "Could not follow user: #{followed.nickname} blocked you."}
399 if !followed.local && follower.local && !ap_enabled?(followed) do
400 Websub.subscribe(follower, followed)
405 where: u.id == ^follower.id,
406 update: [push: [following: ^ap_followers]],
410 {1, [follower]} = Repo.update_all(q, [])
412 follower = maybe_update_following_count(follower)
414 {:ok, _} = update_follower_count(followed)
420 def unfollow(%User{} = follower, %User{} = followed) do
421 ap_followers = followed.follower_address
423 if following?(follower, followed) and follower.ap_id != followed.ap_id do
426 where: u.id == ^follower.id,
427 update: [pull: [following: ^ap_followers]],
431 {1, [follower]} = Repo.update_all(q, [])
433 follower = maybe_update_following_count(follower)
435 {:ok, followed} = update_follower_count(followed)
439 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
441 {:error, "Not subscribed!"}
445 @spec following?(User.t(), User.t()) :: boolean
446 def following?(%User{} = follower, %User{} = followed) do
447 Enum.member?(follower.following, followed.follower_address)
450 def locked?(%User{} = user) do
451 user.info.locked || false
455 Repo.get_by(User, id: id)
458 def get_by_ap_id(ap_id) do
459 Repo.get_by(User, ap_id: ap_id)
462 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
463 # of the ap_id and the domain and tries to get that user
464 def get_by_guessed_nickname(ap_id) do
465 domain = URI.parse(ap_id).host
466 name = List.last(String.split(ap_id, "/"))
467 nickname = "#{name}@#{domain}"
469 get_cached_by_nickname(nickname)
472 def set_cache({:ok, user}), do: set_cache(user)
473 def set_cache({:error, err}), do: {:error, err}
475 def set_cache(%User{} = user) do
476 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
477 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
478 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
482 def update_and_set_cache(changeset) do
483 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
490 def invalidate_cache(user) do
491 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
492 Cachex.del(:user_cache, "nickname:#{user.nickname}")
493 Cachex.del(:user_cache, "user_info:#{user.id}")
496 def get_cached_by_ap_id(ap_id) do
497 key = "ap_id:#{ap_id}"
498 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
501 def get_cached_by_id(id) do
505 Cachex.fetch!(:user_cache, key, fn _ ->
509 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
510 {:commit, user.ap_id}
516 get_cached_by_ap_id(ap_id)
519 def get_cached_by_nickname(nickname) do
520 key = "nickname:#{nickname}"
522 Cachex.fetch!(:user_cache, key, fn ->
523 user_result = get_or_fetch_by_nickname(nickname)
526 {:ok, user} -> {:commit, user}
527 {:error, _error} -> {:ignore, nil}
532 def get_cached_by_nickname_or_id(nickname_or_id) do
533 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
536 def get_by_nickname(nickname) do
537 Repo.get_by(User, nickname: nickname) ||
538 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
539 Repo.get_by(User, nickname: local_nickname(nickname))
543 def get_by_email(email), do: Repo.get_by(User, email: email)
545 def get_by_nickname_or_email(nickname_or_email) do
546 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
549 def get_cached_user_info(user) do
550 key = "user_info:#{user.id}"
551 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
554 def fetch_by_nickname(nickname) do
555 ap_try = ActivityPub.make_user_from_nickname(nickname)
558 {:ok, user} -> {:ok, user}
559 _ -> OStatus.make_user(nickname)
563 def get_or_fetch_by_nickname(nickname) do
564 with %User{} = user <- get_by_nickname(nickname) do
568 with [_nick, _domain] <- String.split(nickname, "@"),
569 {:ok, user} <- fetch_by_nickname(nickname) do
570 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
571 fetch_initial_posts(user)
576 _e -> {:error, "not found " <> nickname}
581 @doc "Fetch some posts when the user has just been federated with"
582 def fetch_initial_posts(user),
583 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:fetch_initial_posts, user])
585 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
586 def get_followers_query(%User{} = user, nil) do
587 User.Query.build(%{followers: user, deactivated: false})
590 def get_followers_query(user, page) do
591 from(u in get_followers_query(user, nil))
592 |> User.Query.paginate(page, 20)
595 @spec get_followers_query(User.t()) :: Ecto.Query.t()
596 def get_followers_query(user), do: get_followers_query(user, nil)
598 @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
599 def get_followers(user, page \\ nil) do
600 q = get_followers_query(user, page)
605 @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
606 def get_external_followers(user, page \\ nil) do
609 |> get_followers_query(page)
610 |> User.Query.build(%{external: true})
615 def get_followers_ids(user, page \\ nil) do
616 q = get_followers_query(user, page)
618 Repo.all(from(u in q, select: u.id))
621 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
622 def get_friends_query(%User{} = user, nil) do
623 User.Query.build(%{friends: user, deactivated: false})
626 def get_friends_query(user, page) do
627 from(u in get_friends_query(user, nil))
628 |> User.Query.paginate(page, 20)
631 @spec get_friends_query(User.t()) :: Ecto.Query.t()
632 def get_friends_query(user), do: get_friends_query(user, nil)
634 def get_friends(user, page \\ nil) do
635 q = get_friends_query(user, page)
640 def get_friends_ids(user, page \\ nil) do
641 q = get_friends_query(user, page)
643 Repo.all(from(u in q, select: u.id))
646 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
647 def get_follow_requests(%User{} = user) do
649 Activity.follow_requests_for_actor(user)
650 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
651 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
652 |> group_by([a, u], u.id)
659 def increase_note_count(%User{} = user) do
661 |> where(id: ^user.id)
666 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
673 |> Repo.update_all([])
675 {1, [user]} -> set_cache(user)
680 def decrease_note_count(%User{} = user) do
682 |> where(id: ^user.id)
687 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
694 |> Repo.update_all([])
696 {1, [user]} -> set_cache(user)
701 def update_note_count(%User{} = user) do
705 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
709 note_count = Repo.one(note_count_query)
711 info_cng = User.Info.set_note_count(user.info, note_count)
715 |> put_embed(:info, info_cng)
716 |> update_and_set_cache()
719 def maybe_fetch_follow_information(user) do
720 with {:ok, user} <- fetch_follow_information(user) do
724 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
730 def fetch_follow_information(user) do
731 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
732 info_cng = User.Info.follow_information_update(user.info, info)
737 |> put_embed(:info, info_cng)
739 update_and_set_cache(changeset)
746 def update_follower_count(%User{} = user) do
747 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
748 follower_count_query =
749 User.Query.build(%{followers: user, deactivated: false})
750 |> select([u], %{count: count(u.id)})
753 |> where(id: ^user.id)
754 |> join(:inner, [u], s in subquery(follower_count_query))
759 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
766 |> Repo.update_all([])
768 {1, [user]} -> set_cache(user)
772 {:ok, maybe_fetch_follow_information(user)}
776 def maybe_update_following_count(%User{local: false} = user) do
777 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
778 {:ok, maybe_fetch_follow_information(user)}
784 def maybe_update_following_count(user), do: user
786 def remove_duplicated_following(%User{following: following} = user) do
787 uniq_following = Enum.uniq(following)
789 if length(following) == length(uniq_following) do
793 |> update_changeset(%{following: uniq_following})
794 |> update_and_set_cache()
798 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
799 def get_users_from_set(ap_ids, local_only \\ true) do
800 criteria = %{ap_id: ap_ids, deactivated: false}
801 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
803 User.Query.build(criteria)
807 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
808 def get_recipients_from_activity(%Activity{recipients: to}) do
809 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
813 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
814 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
818 User.Info.add_to_mutes(info, ap_id)
819 |> User.Info.add_to_muted_notifications(info, ap_id, notifications?)
823 |> put_embed(:info, info_cng)
825 update_and_set_cache(cng)
828 def unmute(muter, %{ap_id: ap_id}) do
832 User.Info.remove_from_mutes(info, ap_id)
833 |> User.Info.remove_from_muted_notifications(info, ap_id)
837 |> put_embed(:info, info_cng)
839 update_and_set_cache(cng)
842 def subscribe(subscriber, %{ap_id: ap_id}) do
843 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
845 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
846 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
849 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
853 |> User.Info.add_to_subscribers(subscriber.ap_id)
856 |> put_embed(:info, info_cng)
857 |> update_and_set_cache()
862 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
863 with %User{} = user <- get_cached_by_ap_id(ap_id) do
866 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
869 |> put_embed(:info, info_cng)
870 |> update_and_set_cache()
874 def block(blocker, %User{ap_id: ap_id} = blocked) do
875 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
877 if following?(blocker, blocked) do
878 {:ok, blocker, _} = unfollow(blocker, blocked)
885 if subscribed_to?(blocked, blocker) do
886 {:ok, blocker} = unsubscribe(blocked, blocker)
892 if following?(blocked, blocker) do
893 unfollow(blocked, blocker)
896 {:ok, blocker} = update_follower_count(blocker)
900 |> User.Info.add_to_block(ap_id)
904 |> put_embed(:info, info_cng)
906 update_and_set_cache(cng)
909 # helper to handle the block given only an actor's AP id
910 def block(blocker, %{ap_id: ap_id}) do
911 block(blocker, get_cached_by_ap_id(ap_id))
914 def unblock(blocker, %{ap_id: ap_id}) do
917 |> User.Info.remove_from_block(ap_id)
921 |> put_embed(:info, info_cng)
923 update_and_set_cache(cng)
926 def mutes?(nil, _), do: false
927 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
929 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
930 def muted_notifications?(nil, _), do: false
932 def muted_notifications?(user, %{ap_id: ap_id}),
933 do: Enum.member?(user.info.muted_notifications, ap_id)
935 def blocks?(%User{} = user, %User{} = target) do
936 blocks_ap_id?(user, target) || blocks_domain?(user, target)
939 def blocks?(nil, _), do: false
941 def blocks_ap_id?(%User{} = user, %User{} = target) do
942 Enum.member?(user.info.blocks, target.ap_id)
945 def blocks_ap_id?(_, _), do: false
947 def blocks_domain?(%User{} = user, %User{} = target) do
948 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
949 %{host: host} = URI.parse(target.ap_id)
950 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
953 def blocks_domain?(_, _), do: false
955 def subscribed_to?(user, %{ap_id: ap_id}) do
956 with %User{} = target <- get_cached_by_ap_id(ap_id) do
957 Enum.member?(target.info.subscribers, user.ap_id)
961 @spec muted_users(User.t()) :: [User.t()]
962 def muted_users(user) do
963 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
967 @spec blocked_users(User.t()) :: [User.t()]
968 def blocked_users(user) do
969 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
973 @spec subscribers(User.t()) :: [User.t()]
974 def subscribers(user) do
975 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
979 def block_domain(user, domain) do
982 |> User.Info.add_to_domain_block(domain)
986 |> put_embed(:info, info_cng)
988 update_and_set_cache(cng)
991 def unblock_domain(user, domain) do
994 |> User.Info.remove_from_domain_block(domain)
998 |> put_embed(:info, info_cng)
1000 update_and_set_cache(cng)
1003 def deactivate_async(user, status \\ true) do
1004 PleromaJobQueue.enqueue(:background, __MODULE__, [:deactivate_async, user, status])
1007 def deactivate(%User{} = user, status \\ true) do
1008 info_cng = User.Info.set_activation_status(user.info, status)
1010 with {:ok, friends} <- User.get_friends(user),
1011 {:ok, followers} <- User.get_followers(user),
1015 |> put_embed(:info, info_cng)
1016 |> update_and_set_cache() do
1017 Enum.each(followers, &invalidate_cache(&1))
1018 Enum.each(friends, &update_follower_count(&1))
1024 def update_notification_settings(%User{} = user, settings \\ %{}) do
1025 info_changeset = User.Info.update_notification_settings(user.info, settings)
1028 |> put_embed(:info, info_changeset)
1029 |> update_and_set_cache()
1032 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1033 def perform(:fetch_initial_posts, %User{} = user) do
1034 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1037 # Insert all the posts in reverse order, so they're in the right order on the timeline
1038 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
1039 &Pleroma.Web.Federator.incoming_ap_doc/1
1045 @spec delete(User.t()) :: :ok
1046 def delete(%User{} = user, actor \\ nil),
1047 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:delete, user, actor])
1049 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1050 def perform(:delete, %User{} = user, actor) do
1051 {:ok, _user} = ActivityPub.delete(user, actor: actor)
1053 # Remove all relationships
1054 {:ok, followers} = User.get_followers(user)
1056 Enum.each(followers, fn follower ->
1057 ActivityPub.unfollow(follower, user)
1058 User.unfollow(follower, user)
1061 {:ok, friends} = User.get_friends(user)
1063 Enum.each(friends, fn followed ->
1064 ActivityPub.unfollow(user, followed)
1065 User.unfollow(user, followed)
1068 delete_user_activities(user)
1069 invalidate_cache(user)
1073 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1075 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1076 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1077 when is_list(blocked_identifiers) do
1079 blocked_identifiers,
1080 fn blocked_identifier ->
1081 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1082 {:ok, blocker} <- block(blocker, blocked),
1083 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1087 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1094 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1095 def perform(:follow_import, %User{} = follower, followed_identifiers)
1096 when is_list(followed_identifiers) do
1098 followed_identifiers,
1099 fn followed_identifier ->
1100 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1101 {:ok, follower} <- maybe_direct_follow(follower, followed),
1102 {:ok, _} <- ActivityPub.follow(follower, followed) do
1106 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1113 @spec external_users_query() :: Ecto.Query.t()
1114 def external_users_query do
1122 @spec external_users(keyword()) :: [User.t()]
1123 def external_users(opts \\ []) do
1125 external_users_query()
1126 |> select([u], struct(u, [:id, :ap_id, :info]))
1130 do: where(query, [u], u.id > ^opts[:max_id]),
1135 do: limit(query, ^opts[:limit]),
1141 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers),
1143 PleromaJobQueue.enqueue(:background, __MODULE__, [
1149 def follow_import(%User{} = follower, followed_identifiers) when is_list(followed_identifiers),
1151 PleromaJobQueue.enqueue(:background, __MODULE__, [
1154 followed_identifiers
1157 def delete_user_activities(%User{ap_id: ap_id} = user) do
1159 |> Activity.query_by_actor()
1160 |> RepoStreamer.chunk_stream(50)
1161 |> Stream.each(fn activities ->
1162 Enum.each(activities, &delete_activity(&1))
1169 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1171 |> Object.normalize()
1172 |> ActivityPub.delete()
1175 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1176 user = get_cached_by_ap_id(activity.actor)
1177 object = Object.normalize(activity)
1179 ActivityPub.unlike(user, object)
1182 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1183 user = get_cached_by_ap_id(activity.actor)
1184 object = Object.normalize(activity)
1186 ActivityPub.unannounce(user, object)
1189 defp delete_activity(_activity), do: "Doing nothing"
1191 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1192 Pleroma.HTML.Scrubber.TwitterText
1195 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1197 def fetch_by_ap_id(ap_id) do
1198 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1205 case OStatus.make_user(ap_id) do
1206 {:ok, user} -> {:ok, user}
1207 _ -> {:error, "Could not fetch by AP id"}
1212 def get_or_fetch_by_ap_id(ap_id) do
1213 user = get_cached_by_ap_id(ap_id)
1215 if !is_nil(user) and !User.needs_update?(user) do
1218 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1219 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1221 resp = fetch_by_ap_id(ap_id)
1223 if should_fetch_initial do
1224 with {:ok, %User{} = user} <- resp do
1225 fetch_initial_posts(user)
1233 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1234 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1235 if user = get_cached_by_ap_id(uri) do
1239 %User{info: %User.Info{}}
1240 |> cast(%{}, [:ap_id, :nickname, :local])
1241 |> put_change(:ap_id, uri)
1242 |> put_change(:nickname, nickname)
1243 |> put_change(:local, true)
1244 |> put_change(:follower_address, uri <> "/followers")
1246 {:ok, user} = Repo.insert(changes)
1252 def public_key_from_info(%{
1253 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1257 |> :public_key.pem_decode()
1259 |> :public_key.pem_entry_decode()
1265 def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
1266 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1269 def public_key_from_info(_), do: {:error, "not found key"}
1271 def get_public_key_for_ap_id(ap_id) do
1272 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1273 {:ok, public_key} <- public_key_from_info(user.info) do
1280 defp blank?(""), do: nil
1281 defp blank?(n), do: n
1283 def insert_or_update_user(data) do
1285 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1286 |> remote_user_creation()
1287 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1291 def ap_enabled?(%User{local: true}), do: true
1292 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1293 def ap_enabled?(_), do: false
1295 @doc "Gets or fetch a user by uri or nickname."
1296 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1297 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1298 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1300 # wait a period of time and return newest version of the User structs
1301 # this is because we have synchronous follow APIs and need to simulate them
1302 # with an async handshake
1303 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1304 with %User{} = a <- User.get_cached_by_id(a.id),
1305 %User{} = b <- User.get_cached_by_id(b.id) do
1313 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1314 with :ok <- :timer.sleep(timeout),
1315 %User{} = a <- User.get_cached_by_id(a.id),
1316 %User{} = b <- User.get_cached_by_id(b.id) do
1324 def parse_bio(bio) when is_binary(bio) and bio != "" do
1326 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1330 def parse_bio(_), do: ""
1332 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1333 # TODO: get profile URLs other than user.ap_id
1334 profile_urls = [user.ap_id]
1337 |> CommonUtils.format_input("text/plain",
1338 mentions_format: :full,
1339 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1344 def parse_bio(_, _), do: ""
1346 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1347 Repo.transaction(fn ->
1348 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1352 def tag(nickname, tags) when is_binary(nickname),
1353 do: tag(get_by_nickname(nickname), tags)
1355 def tag(%User{} = user, tags),
1356 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1358 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1359 Repo.transaction(fn ->
1360 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1364 def untag(nickname, tags) when is_binary(nickname),
1365 do: untag(get_by_nickname(nickname), tags)
1367 def untag(%User{} = user, tags),
1368 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1370 defp update_tags(%User{} = user, new_tags) do
1371 {:ok, updated_user} =
1373 |> change(%{tags: new_tags})
1374 |> update_and_set_cache()
1379 defp normalize_tags(tags) do
1382 |> Enum.map(&String.downcase(&1))
1385 defp local_nickname_regex do
1386 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1387 @extended_local_nickname_regex
1389 @strict_local_nickname_regex
1393 def local_nickname(nickname_or_mention) do
1396 |> String.split("@")
1400 def full_nickname(nickname_or_mention),
1401 do: String.trim_leading(nickname_or_mention, "@")
1403 def error_user(ap_id) do
1408 nickname: "erroruser@example.com",
1409 inserted_at: NaiveDateTime.utc_now()
1413 @spec all_superusers() :: [User.t()]
1414 def all_superusers do
1415 User.Query.build(%{super_users: true, local: true, deactivated: false})
1419 def showing_reblogs?(%User{} = user, %User{} = target) do
1420 target.ap_id not in user.info.muted_reblogs
1424 The function returns a query to get users with no activity for given interval of days.
1425 Inactive users are those who didn't read any notification, or had any activity where
1426 the user is the activity's actor, during `inactivity_threshold` days.
1427 Deactivated users will not appear in this list.
1431 iex> Pleroma.User.list_inactive_users()
1434 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1435 def list_inactive_users_query(inactivity_threshold \\ 7) do
1436 negative_inactivity_threshold = -inactivity_threshold
1437 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1438 # Subqueries are not supported in `where` clauses, join gets too complicated.
1439 has_read_notifications =
1440 from(n in Pleroma.Notification,
1441 where: n.seen == true,
1443 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1446 |> Pleroma.Repo.all()
1448 from(u in Pleroma.User,
1449 left_join: a in Pleroma.Activity,
1450 on: u.ap_id == a.actor,
1451 where: not is_nil(u.nickname),
1452 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1453 where: u.id not in ^has_read_notifications,
1456 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1457 is_nil(max(a.inserted_at))
1462 Enable or disable email notifications for user
1466 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1467 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1469 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1470 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1472 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1473 {:ok, t()} | {:error, Ecto.Changeset.t()}
1474 def switch_email_notifications(user, type, status) do
1475 info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})
1478 |> put_embed(:info, info)
1479 |> update_and_set_cache()
1483 Set `last_digest_emailed_at` value for the user to current time
1485 @spec touch_last_digest_emailed_at(t()) :: t()
1486 def touch_last_digest_emailed_at(user) do
1487 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1489 {:ok, updated_user} =
1491 |> change(%{last_digest_emailed_at: now})
1492 |> update_and_set_cache()
1497 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1498 def toggle_confirmation(%User{} = user) do
1499 need_confirmation? = !user.info.confirmation_pending
1502 User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
1506 |> put_embed(:info, info_changeset)
1507 |> update_and_set_cache()
1510 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1514 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1515 # use instance-default
1516 config = Pleroma.Config.get([:assets, :mascots])
1517 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1518 mascot = Keyword.get(config, default_mascot)
1521 "id" => "default-mascot",
1522 "url" => mascot[:url],
1523 "preview_url" => mascot[:url],
1525 "mime_type" => mascot[:mime_type]
1530 def ensure_keys_present(%User{info: info} = user) do
1534 {:ok, pem} = Keys.generate_rsa_pem()
1537 |> Ecto.Changeset.change()
1538 |> Ecto.Changeset.put_embed(:info, User.Info.set_keys(info, pem))
1539 |> update_and_set_cache()
1543 def get_ap_ids_by_nicknames(nicknames) do
1545 where: u.nickname in ^nicknames,
1551 defdelegate search(query, opts \\ []), to: User.Search
1553 defp put_password_hash(
1554 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1556 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1559 defp put_password_hash(changeset), do: changeset
1561 def is_internal_user?(%User{nickname: nil}), do: true
1562 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1563 def is_internal_user?(_), do: false