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 follow_state(%User{} = user, %User{} = target) do
136 follow_activity = Utils.fetch_latest_follow(user, target)
139 do: follow_activity.data["state"],
140 # Ideally this would be nil, but then Cachex does not commit the value
144 def get_cached_follow_state(user, target) do
145 key = "follow_state:#{user.ap_id}|#{target.ap_id}"
146 Cachex.fetch!(:user_cache, key, fn _ -> {:commit, follow_state(user, target)} end)
149 def set_follow_state_cache(user_ap_id, target_ap_id, state) do
152 "follow_state:#{user_ap_id}|#{target_ap_id}",
157 def set_info_cache(user, args) do
158 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user, args))
161 @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
162 def restrict_deactivated(query) do
164 where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info)
168 def following_count(%User{following: []}), do: 0
170 def following_count(%User{} = user) do
172 |> get_friends_query()
173 |> Repo.aggregate(:count, :id)
176 def remote_user_creation(params) do
177 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
178 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
180 params = Map.put(params, :info, params[:info] || %{})
181 info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])
185 |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
186 |> validate_required([:name, :ap_id])
187 |> unique_constraint(:nickname)
188 |> validate_format(:nickname, @email_regex)
189 |> validate_length(:bio, max: bio_limit)
190 |> validate_length(:name, max: name_limit)
191 |> put_change(:local, false)
192 |> put_embed(:info, info_cng)
195 case info_cng.changes[:source_data] do
196 %{"followers" => followers, "following" => following} ->
198 |> put_change(:follower_address, followers)
199 |> put_change(:following_address, following)
202 followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
205 |> put_change(:follower_address, followers)
212 def update_changeset(struct, params \\ %{}) do
213 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
214 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
217 |> cast(params, [:bio, :name, :avatar, :following])
218 |> unique_constraint(:nickname)
219 |> validate_format(:nickname, local_nickname_regex())
220 |> validate_length(:bio, max: bio_limit)
221 |> validate_length(:name, min: 1, max: name_limit)
224 def upgrade_changeset(struct, params \\ %{}) do
225 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
226 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
228 params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
229 info_cng = User.Info.user_upgrade(struct.info, params[:info])
240 |> unique_constraint(:nickname)
241 |> validate_format(:nickname, local_nickname_regex())
242 |> validate_length(:bio, max: bio_limit)
243 |> validate_length(:name, max: name_limit)
244 |> put_embed(:info, info_cng)
247 def password_update_changeset(struct, params) do
249 |> cast(params, [:password, :password_confirmation])
250 |> validate_required([:password, :password_confirmation])
251 |> validate_confirmation(:password)
255 @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
256 def reset_password(%User{id: user_id} = user, data) do
259 |> Multi.update(:user, password_update_changeset(user, data))
260 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
261 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
263 case Repo.transaction(multi) do
264 {:ok, %{user: user} = _} -> set_cache(user)
265 {:error, _, changeset, _} -> {:error, changeset}
269 def register_changeset(struct, params \\ %{}, opts \\ []) do
270 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
271 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
274 if is_nil(opts[:need_confirmation]) do
275 Pleroma.Config.get([:instance, :account_activation_required])
277 opts[:need_confirmation]
281 User.Info.confirmation_changeset(%User.Info{}, need_confirmation: need_confirmation?)
285 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
286 |> validate_required([:name, :nickname, :password, :password_confirmation])
287 |> validate_confirmation(:password)
288 |> unique_constraint(:email)
289 |> unique_constraint(:nickname)
290 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
291 |> validate_format(:nickname, local_nickname_regex())
292 |> validate_format(:email, @email_regex)
293 |> validate_length(:bio, max: bio_limit)
294 |> validate_length(:name, min: 1, max: name_limit)
295 |> put_change(:info, info_change)
298 if opts[:external] do
301 validate_required(changeset, [:email])
304 if changeset.valid? do
305 ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
306 followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
310 |> put_change(:ap_id, ap_id)
311 |> unique_constraint(:ap_id)
312 |> put_change(:following, [followers])
313 |> put_change(:follower_address, followers)
319 defp autofollow_users(user) do
320 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
323 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
326 follow_all(user, autofollowed_users)
329 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
330 def register(%Ecto.Changeset{} = changeset) do
331 with {:ok, user} <- Repo.insert(changeset),
332 {:ok, user} <- autofollow_users(user),
333 {:ok, user} <- set_cache(user),
334 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
335 {:ok, _} <- try_send_confirmation_email(user) do
340 def try_send_confirmation_email(%User{} = user) do
341 if user.info.confirmation_pending &&
342 Pleroma.Config.get([:instance, :account_activation_required]) do
344 |> Pleroma.Emails.UserEmail.account_confirmation_email()
345 |> Pleroma.Emails.Mailer.deliver_async()
353 def needs_update?(%User{local: true}), do: false
355 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
357 def needs_update?(%User{local: false} = user) do
358 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
361 def needs_update?(_), do: true
363 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
364 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
368 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
369 follow(follower, followed)
372 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
373 if not User.ap_enabled?(followed) do
374 follow(follower, followed)
380 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
381 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
382 def follow_all(follower, followeds) do
385 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
386 |> Enum.map(fn %{follower_address: fa} -> fa end)
390 where: u.id == ^follower.id,
395 "array(select distinct unnest (array_cat(?, ?)))",
404 {1, [follower]} = Repo.update_all(q, [])
406 Enum.each(followeds, fn followed ->
407 update_follower_count(followed)
413 def follow(%User{} = follower, %User{info: info} = followed) do
414 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
415 ap_followers = followed.follower_address
419 {:error, "Could not follow user: You are deactivated."}
421 deny_follow_blocked and blocks?(followed, follower) ->
422 {:error, "Could not follow user: #{followed.nickname} blocked you."}
425 if !followed.local && follower.local && !ap_enabled?(followed) do
426 Websub.subscribe(follower, followed)
431 where: u.id == ^follower.id,
432 update: [push: [following: ^ap_followers]],
436 {1, [follower]} = Repo.update_all(q, [])
438 follower = maybe_update_following_count(follower)
440 {:ok, _} = update_follower_count(followed)
446 def unfollow(%User{} = follower, %User{} = followed) do
447 ap_followers = followed.follower_address
449 if following?(follower, followed) and follower.ap_id != followed.ap_id do
452 where: u.id == ^follower.id,
453 update: [pull: [following: ^ap_followers]],
457 {1, [follower]} = Repo.update_all(q, [])
459 follower = maybe_update_following_count(follower)
461 {:ok, followed} = update_follower_count(followed)
465 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
467 {:error, "Not subscribed!"}
471 @spec following?(User.t(), User.t()) :: boolean
472 def following?(%User{} = follower, %User{} = followed) do
473 Enum.member?(follower.following, followed.follower_address)
476 def locked?(%User{} = user) do
477 user.info.locked || false
481 Repo.get_by(User, id: id)
484 def get_by_ap_id(ap_id) do
485 Repo.get_by(User, ap_id: ap_id)
488 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
489 # of the ap_id and the domain and tries to get that user
490 def get_by_guessed_nickname(ap_id) do
491 domain = URI.parse(ap_id).host
492 name = List.last(String.split(ap_id, "/"))
493 nickname = "#{name}@#{domain}"
495 get_cached_by_nickname(nickname)
498 def set_cache({:ok, user}), do: set_cache(user)
499 def set_cache({:error, err}), do: {:error, err}
501 def set_cache(%User{} = user) do
502 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
503 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
504 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
508 def update_and_set_cache(changeset) do
509 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
516 def invalidate_cache(user) do
517 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
518 Cachex.del(:user_cache, "nickname:#{user.nickname}")
519 Cachex.del(:user_cache, "user_info:#{user.id}")
522 def get_cached_by_ap_id(ap_id) do
523 key = "ap_id:#{ap_id}"
524 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
527 def get_cached_by_id(id) do
531 Cachex.fetch!(:user_cache, key, fn _ ->
535 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
536 {:commit, user.ap_id}
542 get_cached_by_ap_id(ap_id)
545 def get_cached_by_nickname(nickname) do
546 key = "nickname:#{nickname}"
548 Cachex.fetch!(:user_cache, key, fn ->
549 user_result = get_or_fetch_by_nickname(nickname)
552 {:ok, user} -> {:commit, user}
553 {:error, _error} -> {:ignore, nil}
558 def get_cached_by_nickname_or_id(nickname_or_id) do
559 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
562 def get_by_nickname(nickname) do
563 Repo.get_by(User, nickname: nickname) ||
564 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
565 Repo.get_by(User, nickname: local_nickname(nickname))
569 def get_by_email(email), do: Repo.get_by(User, email: email)
571 def get_by_nickname_or_email(nickname_or_email) do
572 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
575 def get_cached_user_info(user) do
576 key = "user_info:#{user.id}"
577 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
580 def fetch_by_nickname(nickname) do
581 ap_try = ActivityPub.make_user_from_nickname(nickname)
584 {:ok, user} -> {:ok, user}
585 _ -> OStatus.make_user(nickname)
589 def get_or_fetch_by_nickname(nickname) do
590 with %User{} = user <- get_by_nickname(nickname) do
594 with [_nick, _domain] <- String.split(nickname, "@"),
595 {:ok, user} <- fetch_by_nickname(nickname) do
596 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
597 fetch_initial_posts(user)
602 _e -> {:error, "not found " <> nickname}
607 @doc "Fetch some posts when the user has just been federated with"
608 def fetch_initial_posts(user),
609 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:fetch_initial_posts, user])
611 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
612 def get_followers_query(%User{} = user, nil) do
613 User.Query.build(%{followers: user, deactivated: false})
616 def get_followers_query(user, page) do
617 from(u in get_followers_query(user, nil))
618 |> User.Query.paginate(page, 20)
621 @spec get_followers_query(User.t()) :: Ecto.Query.t()
622 def get_followers_query(user), do: get_followers_query(user, nil)
624 @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
625 def get_followers(user, page \\ nil) do
626 q = get_followers_query(user, page)
631 @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
632 def get_external_followers(user, page \\ nil) do
635 |> get_followers_query(page)
636 |> User.Query.build(%{external: true})
641 def get_followers_ids(user, page \\ nil) do
642 q = get_followers_query(user, page)
644 Repo.all(from(u in q, select: u.id))
647 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
648 def get_friends_query(%User{} = user, nil) do
649 User.Query.build(%{friends: user, deactivated: false})
652 def get_friends_query(user, page) do
653 from(u in get_friends_query(user, nil))
654 |> User.Query.paginate(page, 20)
657 @spec get_friends_query(User.t()) :: Ecto.Query.t()
658 def get_friends_query(user), do: get_friends_query(user, nil)
660 def get_friends(user, page \\ nil) do
661 q = get_friends_query(user, page)
666 def get_friends_ids(user, page \\ nil) do
667 q = get_friends_query(user, page)
669 Repo.all(from(u in q, select: u.id))
672 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
673 def get_follow_requests(%User{} = user) do
675 Activity.follow_requests_for_actor(user)
676 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
677 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
678 |> group_by([a, u], u.id)
685 def increase_note_count(%User{} = user) do
687 |> where(id: ^user.id)
692 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
699 |> Repo.update_all([])
701 {1, [user]} -> set_cache(user)
706 def decrease_note_count(%User{} = user) do
708 |> where(id: ^user.id)
713 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
720 |> Repo.update_all([])
722 {1, [user]} -> set_cache(user)
727 def update_note_count(%User{} = user) do
731 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
735 note_count = Repo.one(note_count_query)
737 info_cng = User.Info.set_note_count(user.info, note_count)
741 |> put_embed(:info, info_cng)
742 |> update_and_set_cache()
745 def maybe_fetch_follow_information(user) do
746 with {:ok, user} <- fetch_follow_information(user) do
750 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
756 def fetch_follow_information(user) do
757 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
758 info_cng = User.Info.follow_information_update(user.info, info)
763 |> put_embed(:info, info_cng)
765 update_and_set_cache(changeset)
772 def update_follower_count(%User{} = user) do
773 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
774 follower_count_query =
775 User.Query.build(%{followers: user, deactivated: false})
776 |> select([u], %{count: count(u.id)})
779 |> where(id: ^user.id)
780 |> join(:inner, [u], s in subquery(follower_count_query))
785 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
792 |> Repo.update_all([])
794 {1, [user]} -> set_cache(user)
798 {:ok, maybe_fetch_follow_information(user)}
802 def maybe_update_following_count(%User{local: false} = user) do
803 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
804 {:ok, maybe_fetch_follow_information(user)}
810 def maybe_update_following_count(user), do: user
812 def remove_duplicated_following(%User{following: following} = user) do
813 uniq_following = Enum.uniq(following)
815 if length(following) == length(uniq_following) do
819 |> update_changeset(%{following: uniq_following})
820 |> update_and_set_cache()
824 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
825 def get_users_from_set(ap_ids, local_only \\ true) do
826 criteria = %{ap_id: ap_ids, deactivated: false}
827 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
829 User.Query.build(criteria)
833 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
834 def get_recipients_from_activity(%Activity{recipients: to}) do
835 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
839 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
840 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
844 User.Info.add_to_mutes(info, ap_id)
845 |> User.Info.add_to_muted_notifications(info, ap_id, notifications?)
849 |> put_embed(:info, info_cng)
851 update_and_set_cache(cng)
854 def unmute(muter, %{ap_id: ap_id}) do
858 User.Info.remove_from_mutes(info, ap_id)
859 |> User.Info.remove_from_muted_notifications(info, ap_id)
863 |> put_embed(:info, info_cng)
865 update_and_set_cache(cng)
868 def subscribe(subscriber, %{ap_id: ap_id}) do
869 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
871 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
872 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
875 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
879 |> User.Info.add_to_subscribers(subscriber.ap_id)
882 |> put_embed(:info, info_cng)
883 |> update_and_set_cache()
888 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
889 with %User{} = user <- get_cached_by_ap_id(ap_id) do
892 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
895 |> put_embed(:info, info_cng)
896 |> update_and_set_cache()
900 def block(blocker, %User{ap_id: ap_id} = blocked) do
901 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
903 if following?(blocker, blocked) do
904 {:ok, blocker, _} = unfollow(blocker, blocked)
911 if subscribed_to?(blocked, blocker) do
912 {:ok, blocker} = unsubscribe(blocked, blocker)
918 if following?(blocked, blocker) do
919 unfollow(blocked, blocker)
922 {:ok, blocker} = update_follower_count(blocker)
926 |> User.Info.add_to_block(ap_id)
930 |> put_embed(:info, info_cng)
932 update_and_set_cache(cng)
935 # helper to handle the block given only an actor's AP id
936 def block(blocker, %{ap_id: ap_id}) do
937 block(blocker, get_cached_by_ap_id(ap_id))
940 def unblock(blocker, %{ap_id: ap_id}) do
943 |> User.Info.remove_from_block(ap_id)
947 |> put_embed(:info, info_cng)
949 update_and_set_cache(cng)
952 def mutes?(nil, _), do: false
953 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
955 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
956 def muted_notifications?(nil, _), do: false
958 def muted_notifications?(user, %{ap_id: ap_id}),
959 do: Enum.member?(user.info.muted_notifications, ap_id)
961 def blocks?(%User{} = user, %User{} = target) do
962 blocks_ap_id?(user, target) || blocks_domain?(user, target)
965 def blocks?(nil, _), do: false
967 def blocks_ap_id?(%User{} = user, %User{} = target) do
968 Enum.member?(user.info.blocks, target.ap_id)
971 def blocks_ap_id?(_, _), do: false
973 def blocks_domain?(%User{} = user, %User{} = target) do
974 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
975 %{host: host} = URI.parse(target.ap_id)
976 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
979 def blocks_domain?(_, _), do: false
981 def subscribed_to?(user, %{ap_id: ap_id}) do
982 with %User{} = target <- get_cached_by_ap_id(ap_id) do
983 Enum.member?(target.info.subscribers, user.ap_id)
987 @spec muted_users(User.t()) :: [User.t()]
988 def muted_users(user) do
989 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
993 @spec blocked_users(User.t()) :: [User.t()]
994 def blocked_users(user) do
995 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
999 @spec subscribers(User.t()) :: [User.t()]
1000 def subscribers(user) do
1001 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
1005 def block_domain(user, domain) do
1008 |> User.Info.add_to_domain_block(domain)
1012 |> put_embed(:info, info_cng)
1014 update_and_set_cache(cng)
1017 def unblock_domain(user, domain) do
1020 |> User.Info.remove_from_domain_block(domain)
1024 |> put_embed(:info, info_cng)
1026 update_and_set_cache(cng)
1029 def deactivate_async(user, status \\ true) do
1030 PleromaJobQueue.enqueue(:background, __MODULE__, [:deactivate_async, user, status])
1033 def deactivate(%User{} = user, status \\ true) do
1034 info_cng = User.Info.set_activation_status(user.info, status)
1036 with {:ok, friends} <- User.get_friends(user),
1037 {:ok, followers} <- User.get_followers(user),
1041 |> put_embed(:info, info_cng)
1042 |> update_and_set_cache() do
1043 Enum.each(followers, &invalidate_cache(&1))
1044 Enum.each(friends, &update_follower_count(&1))
1050 def update_notification_settings(%User{} = user, settings \\ %{}) do
1051 info_changeset = User.Info.update_notification_settings(user.info, settings)
1054 |> put_embed(:info, info_changeset)
1055 |> update_and_set_cache()
1058 @spec delete(User.t()) :: :ok
1059 def delete(%User{} = user),
1060 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:delete, user])
1062 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1063 def perform(:delete, %User{} = user) do
1064 {:ok, _user} = ActivityPub.delete(user)
1066 # Remove all relationships
1067 {:ok, followers} = User.get_followers(user)
1069 Enum.each(followers, fn follower ->
1070 ActivityPub.unfollow(follower, user)
1071 User.unfollow(follower, user)
1074 {:ok, friends} = User.get_friends(user)
1076 Enum.each(friends, fn followed ->
1077 ActivityPub.unfollow(user, followed)
1078 User.unfollow(user, followed)
1081 delete_user_activities(user)
1082 invalidate_cache(user)
1086 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1087 def perform(:fetch_initial_posts, %User{} = user) do
1088 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1091 # Insert all the posts in reverse order, so they're in the right order on the timeline
1092 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
1093 &Pleroma.Web.Federator.incoming_ap_doc/1
1099 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1101 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1102 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1103 when is_list(blocked_identifiers) do
1105 blocked_identifiers,
1106 fn blocked_identifier ->
1107 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1108 {:ok, blocker} <- block(blocker, blocked),
1109 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1113 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1120 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1121 def perform(:follow_import, %User{} = follower, followed_identifiers)
1122 when is_list(followed_identifiers) do
1124 followed_identifiers,
1125 fn followed_identifier ->
1126 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1127 {:ok, follower} <- maybe_direct_follow(follower, followed),
1128 {:ok, _} <- ActivityPub.follow(follower, followed) do
1132 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1139 @spec external_users_query() :: Ecto.Query.t()
1140 def external_users_query do
1148 @spec external_users(keyword()) :: [User.t()]
1149 def external_users(opts \\ []) do
1151 external_users_query()
1152 |> select([u], struct(u, [:id, :ap_id, :info]))
1156 do: where(query, [u], u.id > ^opts[:max_id]),
1161 do: limit(query, ^opts[:limit]),
1167 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers),
1169 PleromaJobQueue.enqueue(:background, __MODULE__, [
1175 def follow_import(%User{} = follower, followed_identifiers) when is_list(followed_identifiers),
1177 PleromaJobQueue.enqueue(:background, __MODULE__, [
1180 followed_identifiers
1183 def delete_user_activities(%User{ap_id: ap_id} = user) do
1185 |> Activity.query_by_actor()
1186 |> RepoStreamer.chunk_stream(50)
1187 |> Stream.each(fn activities ->
1188 Enum.each(activities, &delete_activity(&1))
1195 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1197 |> Object.normalize()
1198 |> ActivityPub.delete()
1201 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1202 user = get_cached_by_ap_id(activity.actor)
1203 object = Object.normalize(activity)
1205 ActivityPub.unlike(user, object)
1208 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1209 user = get_cached_by_ap_id(activity.actor)
1210 object = Object.normalize(activity)
1212 ActivityPub.unannounce(user, object)
1215 defp delete_activity(_activity), do: "Doing nothing"
1217 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1218 Pleroma.HTML.Scrubber.TwitterText
1221 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1223 def fetch_by_ap_id(ap_id) do
1224 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1231 case OStatus.make_user(ap_id) do
1232 {:ok, user} -> {:ok, user}
1233 _ -> {:error, "Could not fetch by AP id"}
1238 def get_or_fetch_by_ap_id(ap_id) do
1239 user = get_cached_by_ap_id(ap_id)
1241 if !is_nil(user) and !User.needs_update?(user) do
1244 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1245 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1247 resp = fetch_by_ap_id(ap_id)
1249 if should_fetch_initial do
1250 with {:ok, %User{} = user} <- resp do
1251 fetch_initial_posts(user)
1259 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1260 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1261 if user = get_cached_by_ap_id(uri) do
1265 %User{info: %User.Info{}}
1266 |> cast(%{}, [:ap_id, :nickname, :local])
1267 |> put_change(:ap_id, uri)
1268 |> put_change(:nickname, nickname)
1269 |> put_change(:local, true)
1270 |> put_change(:follower_address, uri <> "/followers")
1272 {:ok, user} = Repo.insert(changes)
1278 def public_key_from_info(%{
1279 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1283 |> :public_key.pem_decode()
1285 |> :public_key.pem_entry_decode()
1291 def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
1292 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1295 def public_key_from_info(_), do: {:error, "not found key"}
1297 def get_public_key_for_ap_id(ap_id) do
1298 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1299 {:ok, public_key} <- public_key_from_info(user.info) do
1306 defp blank?(""), do: nil
1307 defp blank?(n), do: n
1309 def insert_or_update_user(data) do
1311 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1312 |> remote_user_creation()
1313 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1317 def ap_enabled?(%User{local: true}), do: true
1318 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1319 def ap_enabled?(_), do: false
1321 @doc "Gets or fetch a user by uri or nickname."
1322 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1323 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1324 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1326 # wait a period of time and return newest version of the User structs
1327 # this is because we have synchronous follow APIs and need to simulate them
1328 # with an async handshake
1329 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1330 with %User{} = a <- User.get_cached_by_id(a.id),
1331 %User{} = b <- User.get_cached_by_id(b.id) do
1339 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1340 with :ok <- :timer.sleep(timeout),
1341 %User{} = a <- User.get_cached_by_id(a.id),
1342 %User{} = b <- User.get_cached_by_id(b.id) do
1350 def parse_bio(bio) when is_binary(bio) and bio != "" do
1352 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1356 def parse_bio(_), do: ""
1358 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1359 # TODO: get profile URLs other than user.ap_id
1360 profile_urls = [user.ap_id]
1363 |> CommonUtils.format_input("text/plain",
1364 mentions_format: :full,
1365 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1370 def parse_bio(_, _), do: ""
1372 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1373 Repo.transaction(fn ->
1374 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1378 def tag(nickname, tags) when is_binary(nickname),
1379 do: tag(get_by_nickname(nickname), tags)
1381 def tag(%User{} = user, tags),
1382 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1384 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1385 Repo.transaction(fn ->
1386 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1390 def untag(nickname, tags) when is_binary(nickname),
1391 do: untag(get_by_nickname(nickname), tags)
1393 def untag(%User{} = user, tags),
1394 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1396 defp update_tags(%User{} = user, new_tags) do
1397 {:ok, updated_user} =
1399 |> change(%{tags: new_tags})
1400 |> update_and_set_cache()
1405 defp normalize_tags(tags) do
1408 |> Enum.map(&String.downcase(&1))
1411 defp local_nickname_regex do
1412 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1413 @extended_local_nickname_regex
1415 @strict_local_nickname_regex
1419 def local_nickname(nickname_or_mention) do
1422 |> String.split("@")
1426 def full_nickname(nickname_or_mention),
1427 do: String.trim_leading(nickname_or_mention, "@")
1429 def error_user(ap_id) do
1434 nickname: "erroruser@example.com",
1435 inserted_at: NaiveDateTime.utc_now()
1439 @spec all_superusers() :: [User.t()]
1440 def all_superusers do
1441 User.Query.build(%{super_users: true, local: true, deactivated: false})
1445 def showing_reblogs?(%User{} = user, %User{} = target) do
1446 target.ap_id not in user.info.muted_reblogs
1450 The function returns a query to get users with no activity for given interval of days.
1451 Inactive users are those who didn't read any notification, or had any activity where
1452 the user is the activity's actor, during `inactivity_threshold` days.
1453 Deactivated users will not appear in this list.
1457 iex> Pleroma.User.list_inactive_users()
1460 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1461 def list_inactive_users_query(inactivity_threshold \\ 7) do
1462 negative_inactivity_threshold = -inactivity_threshold
1463 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1464 # Subqueries are not supported in `where` clauses, join gets too complicated.
1465 has_read_notifications =
1466 from(n in Pleroma.Notification,
1467 where: n.seen == true,
1469 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1472 |> Pleroma.Repo.all()
1474 from(u in Pleroma.User,
1475 left_join: a in Pleroma.Activity,
1476 on: u.ap_id == a.actor,
1477 where: not is_nil(u.nickname),
1478 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1479 where: u.id not in ^has_read_notifications,
1482 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1483 is_nil(max(a.inserted_at))
1488 Enable or disable email notifications for user
1492 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1493 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1495 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1496 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1498 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1499 {:ok, t()} | {:error, Ecto.Changeset.t()}
1500 def switch_email_notifications(user, type, status) do
1501 info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})
1504 |> put_embed(:info, info)
1505 |> update_and_set_cache()
1509 Set `last_digest_emailed_at` value for the user to current time
1511 @spec touch_last_digest_emailed_at(t()) :: t()
1512 def touch_last_digest_emailed_at(user) do
1513 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1515 {:ok, updated_user} =
1517 |> change(%{last_digest_emailed_at: now})
1518 |> update_and_set_cache()
1523 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1524 def toggle_confirmation(%User{} = user) do
1525 need_confirmation? = !user.info.confirmation_pending
1528 User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
1532 |> put_embed(:info, info_changeset)
1533 |> update_and_set_cache()
1536 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1540 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1541 # use instance-default
1542 config = Pleroma.Config.get([:assets, :mascots])
1543 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1544 mascot = Keyword.get(config, default_mascot)
1547 "id" => "default-mascot",
1548 "url" => mascot[:url],
1549 "preview_url" => mascot[:url],
1551 "mime_type" => mascot[:mime_type]
1556 def ensure_keys_present(%User{info: info} = user) do
1560 {:ok, pem} = Keys.generate_rsa_pem()
1563 |> Ecto.Changeset.change()
1564 |> Ecto.Changeset.put_embed(:info, User.Info.set_keys(info, pem))
1565 |> update_and_set_cache()
1569 def get_ap_ids_by_nicknames(nicknames) do
1571 where: u.nickname in ^nicknames,
1577 defdelegate search(query, opts \\ []), to: User.Search
1579 defp put_password_hash(
1580 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1582 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1585 defp put_password_hash(changeset), do: changeset
1587 def is_internal_user?(%User{nickname: nil}), do: true
1588 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1589 def is_internal_user?(_), do: false