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
14 alias Pleroma.Delivery
16 alias Pleroma.Notification
18 alias Pleroma.Registration
20 alias Pleroma.RepoStreamer
23 alias Pleroma.Web.ActivityPub.ActivityPub
24 alias Pleroma.Web.ActivityPub.Utils
25 alias Pleroma.Web.CommonAPI
26 alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
27 alias Pleroma.Web.OAuth
28 alias Pleroma.Web.OStatus
29 alias Pleroma.Web.RelMe
30 alias Pleroma.Web.Websub
34 @type t :: %__MODULE__{}
36 @primary_key {:id, Pleroma.FlakeId, autogenerate: true}
38 # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
39 @email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
41 @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
42 @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
46 field(:email, :string)
48 field(:nickname, :string)
49 field(:password_hash, :string)
50 field(:password, :string, virtual: true)
51 field(:password_confirmation, :string, virtual: true)
52 field(:following, {:array, :string}, default: [])
53 field(:ap_id, :string)
55 field(:local, :boolean, default: true)
56 field(:follower_address, :string)
57 field(:following_address, :string)
58 field(:search_rank, :float, virtual: true)
59 field(:search_type, :integer, virtual: true)
60 field(:tags, {:array, :string}, default: [])
61 field(:last_refreshed_at, :naive_datetime_usec)
62 field(:last_digest_emailed_at, :naive_datetime)
63 has_many(:notifications, Notification)
64 has_many(:registrations, Registration)
65 has_many(:deliveries, Delivery)
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 follow_state(%User{} = user, %User{} = target) do
139 follow_activity = Utils.fetch_latest_follow(user, target)
142 do: follow_activity.data["state"],
143 # Ideally this would be nil, but then Cachex does not commit the value
147 def get_cached_follow_state(user, target) do
148 key = "follow_state:#{user.ap_id}|#{target.ap_id}"
149 Cachex.fetch!(:user_cache, key, fn _ -> {:commit, follow_state(user, target)} end)
152 def set_follow_state_cache(user_ap_id, target_ap_id, state) do
155 "follow_state:#{user_ap_id}|#{target_ap_id}",
160 def set_info_cache(user, args) do
161 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user, args))
164 @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
165 def restrict_deactivated(query) do
167 where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info)
171 def following_count(%User{following: []}), do: 0
173 def following_count(%User{} = user) do
175 |> get_friends_query()
176 |> Repo.aggregate(:count, :id)
179 def remote_user_creation(params) do
180 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
181 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
183 params = Map.put(params, :info, params[:info] || %{})
184 info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])
188 |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
189 |> validate_required([:name, :ap_id])
190 |> unique_constraint(:nickname)
191 |> validate_format(:nickname, @email_regex)
192 |> validate_length(:bio, max: bio_limit)
193 |> validate_length(:name, max: name_limit)
194 |> put_change(:local, false)
195 |> put_embed(:info, info_cng)
198 case info_cng.changes[:source_data] do
199 %{"followers" => followers, "following" => following} ->
201 |> put_change(:follower_address, followers)
202 |> put_change(:following_address, following)
205 followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
208 |> put_change(:follower_address, followers)
215 def update_changeset(struct, params \\ %{}) do
216 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
217 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
220 |> cast(params, [:bio, :name, :avatar, :following])
221 |> unique_constraint(:nickname)
222 |> validate_format(:nickname, local_nickname_regex())
223 |> validate_length(:bio, max: bio_limit)
224 |> validate_length(:name, min: 1, max: name_limit)
227 def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
228 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
229 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
231 params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
232 info_cng = User.Info.user_upgrade(struct.info, params[:info], remote?)
243 |> unique_constraint(:nickname)
244 |> validate_format(:nickname, local_nickname_regex())
245 |> validate_length(:bio, max: bio_limit)
246 |> validate_length(:name, max: name_limit)
247 |> put_embed(:info, info_cng)
250 def password_update_changeset(struct, params) do
252 |> cast(params, [:password, :password_confirmation])
253 |> validate_required([:password, :password_confirmation])
254 |> validate_confirmation(:password)
258 @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
259 def reset_password(%User{id: user_id} = user, data) do
262 |> Multi.update(:user, password_update_changeset(user, data))
263 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
264 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
266 case Repo.transaction(multi) do
267 {:ok, %{user: user} = _} -> set_cache(user)
268 {:error, _, changeset, _} -> {:error, changeset}
272 def register_changeset(struct, params \\ %{}, opts \\ []) do
273 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
274 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
277 if is_nil(opts[:need_confirmation]) do
278 Pleroma.Config.get([:instance, :account_activation_required])
280 opts[:need_confirmation]
284 User.Info.confirmation_changeset(%User.Info{}, need_confirmation: need_confirmation?)
288 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
289 |> validate_required([:name, :nickname, :password, :password_confirmation])
290 |> validate_confirmation(:password)
291 |> unique_constraint(:email)
292 |> unique_constraint(:nickname)
293 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
294 |> validate_format(:nickname, local_nickname_regex())
295 |> validate_format(:email, @email_regex)
296 |> validate_length(:bio, max: bio_limit)
297 |> validate_length(:name, min: 1, max: name_limit)
298 |> put_change(:info, info_change)
301 if opts[:external] do
304 validate_required(changeset, [:email])
307 if changeset.valid? do
308 ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
309 followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
313 |> put_change(:ap_id, ap_id)
314 |> unique_constraint(:ap_id)
315 |> put_change(:following, [followers])
316 |> put_change(:follower_address, followers)
322 defp autofollow_users(user) do
323 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
326 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
329 follow_all(user, autofollowed_users)
332 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
333 def register(%Ecto.Changeset{} = changeset) do
334 with {:ok, user} <- Repo.insert(changeset),
335 {:ok, user} <- post_register_action(user) do
340 def post_register_action(%User{} = user) do
341 with {:ok, user} <- autofollow_users(user),
342 {:ok, user} <- set_cache(user),
343 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
344 {:ok, _} <- try_send_confirmation_email(user) do
349 def try_send_confirmation_email(%User{} = user) do
350 if user.info.confirmation_pending &&
351 Pleroma.Config.get([:instance, :account_activation_required]) do
353 |> Pleroma.Emails.UserEmail.account_confirmation_email()
354 |> Pleroma.Emails.Mailer.deliver_async()
362 def needs_update?(%User{local: true}), do: false
364 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
366 def needs_update?(%User{local: false} = user) do
367 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
370 def needs_update?(_), do: true
372 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
373 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
377 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
378 follow(follower, followed)
381 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
382 if not User.ap_enabled?(followed) do
383 follow(follower, followed)
389 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
390 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
391 def follow_all(follower, followeds) do
394 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
395 |> Enum.map(fn %{follower_address: fa} -> fa end)
399 where: u.id == ^follower.id,
404 "array(select distinct unnest (array_cat(?, ?)))",
413 {1, [follower]} = Repo.update_all(q, [])
415 Enum.each(followeds, fn followed ->
416 update_follower_count(followed)
422 def follow(%User{} = follower, %User{info: info} = followed) do
423 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
424 ap_followers = followed.follower_address
428 {:error, "Could not follow user: You are deactivated."}
430 deny_follow_blocked and blocks?(followed, follower) ->
431 {:error, "Could not follow user: #{followed.nickname} blocked you."}
434 if !followed.local && follower.local && !ap_enabled?(followed) do
435 Websub.subscribe(follower, followed)
440 where: u.id == ^follower.id,
441 update: [push: [following: ^ap_followers]],
445 {1, [follower]} = Repo.update_all(q, [])
447 follower = maybe_update_following_count(follower)
449 {:ok, _} = update_follower_count(followed)
455 def unfollow(%User{} = follower, %User{} = followed) do
456 ap_followers = followed.follower_address
458 if following?(follower, followed) and follower.ap_id != followed.ap_id do
461 where: u.id == ^follower.id,
462 update: [pull: [following: ^ap_followers]],
466 {1, [follower]} = Repo.update_all(q, [])
468 follower = maybe_update_following_count(follower)
470 {:ok, followed} = update_follower_count(followed)
474 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
476 {:error, "Not subscribed!"}
480 @spec following?(User.t(), User.t()) :: boolean
481 def following?(%User{} = follower, %User{} = followed) do
482 Enum.member?(follower.following, followed.follower_address)
485 def locked?(%User{} = user) do
486 user.info.locked || false
490 Repo.get_by(User, id: id)
493 def get_by_ap_id(ap_id) do
494 Repo.get_by(User, ap_id: ap_id)
497 def get_all_by_ap_id(ap_ids) do
498 from(u in __MODULE__,
499 where: u.ap_id in ^ap_ids
504 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
505 # of the ap_id and the domain and tries to get that user
506 def get_by_guessed_nickname(ap_id) do
507 domain = URI.parse(ap_id).host
508 name = List.last(String.split(ap_id, "/"))
509 nickname = "#{name}@#{domain}"
511 get_cached_by_nickname(nickname)
514 def set_cache({:ok, user}), do: set_cache(user)
515 def set_cache({:error, err}), do: {:error, err}
517 def set_cache(%User{} = user) do
518 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
519 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
520 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
524 def update_and_set_cache(changeset) do
525 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
532 def invalidate_cache(user) do
533 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
534 Cachex.del(:user_cache, "nickname:#{user.nickname}")
535 Cachex.del(:user_cache, "user_info:#{user.id}")
538 def get_cached_by_ap_id(ap_id) do
539 key = "ap_id:#{ap_id}"
540 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
543 def get_cached_by_id(id) do
547 Cachex.fetch!(:user_cache, key, fn _ ->
551 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
552 {:commit, user.ap_id}
558 get_cached_by_ap_id(ap_id)
561 def get_cached_by_nickname(nickname) do
562 key = "nickname:#{nickname}"
564 Cachex.fetch!(:user_cache, key, fn ->
565 user_result = get_or_fetch_by_nickname(nickname)
568 {:ok, user} -> {:commit, user}
569 {:error, _error} -> {:ignore, nil}
574 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
575 restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
578 is_integer(nickname_or_id) or Pleroma.FlakeId.is_flake_id?(nickname_or_id) ->
579 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
581 restrict_to_local == false ->
582 get_cached_by_nickname(nickname_or_id)
584 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
585 get_cached_by_nickname(nickname_or_id)
592 def get_by_nickname(nickname) do
593 Repo.get_by(User, nickname: nickname) ||
594 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
595 Repo.get_by(User, nickname: local_nickname(nickname))
599 def get_by_email(email), do: Repo.get_by(User, email: email)
601 def get_by_nickname_or_email(nickname_or_email) do
602 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
605 def get_cached_user_info(user) do
606 key = "user_info:#{user.id}"
607 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
610 def fetch_by_nickname(nickname) do
611 ap_try = ActivityPub.make_user_from_nickname(nickname)
614 {:ok, user} -> {:ok, user}
615 _ -> OStatus.make_user(nickname)
619 def get_or_fetch_by_nickname(nickname) do
620 with %User{} = user <- get_by_nickname(nickname) do
624 with [_nick, _domain] <- String.split(nickname, "@"),
625 {:ok, user} <- fetch_by_nickname(nickname) do
626 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
627 fetch_initial_posts(user)
632 _e -> {:error, "not found " <> nickname}
637 @doc "Fetch some posts when the user has just been federated with"
638 def fetch_initial_posts(user),
639 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:fetch_initial_posts, user])
641 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
642 def get_followers_query(%User{} = user, nil) do
643 User.Query.build(%{followers: user, deactivated: false})
646 def get_followers_query(user, page) do
647 from(u in get_followers_query(user, nil))
648 |> User.Query.paginate(page, 20)
651 @spec get_followers_query(User.t()) :: Ecto.Query.t()
652 def get_followers_query(user), do: get_followers_query(user, nil)
654 @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
655 def get_followers(user, page \\ nil) do
656 q = get_followers_query(user, page)
661 @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
662 def get_external_followers(user, page \\ nil) do
665 |> get_followers_query(page)
666 |> User.Query.build(%{external: true})
671 def get_followers_ids(user, page \\ nil) do
672 q = get_followers_query(user, page)
674 Repo.all(from(u in q, select: u.id))
677 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
678 def get_friends_query(%User{} = user, nil) do
679 User.Query.build(%{friends: user, deactivated: false})
682 def get_friends_query(user, page) do
683 from(u in get_friends_query(user, nil))
684 |> User.Query.paginate(page, 20)
687 @spec get_friends_query(User.t()) :: Ecto.Query.t()
688 def get_friends_query(user), do: get_friends_query(user, nil)
690 def get_friends(user, page \\ nil) do
691 q = get_friends_query(user, page)
696 def get_friends_ids(user, page \\ nil) do
697 q = get_friends_query(user, page)
699 Repo.all(from(u in q, select: u.id))
702 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
703 def get_follow_requests(%User{} = user) do
705 Activity.follow_requests_for_actor(user)
706 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
707 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
708 |> group_by([a, u], u.id)
715 def increase_note_count(%User{} = user) do
717 |> where(id: ^user.id)
722 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
729 |> Repo.update_all([])
731 {1, [user]} -> set_cache(user)
736 def decrease_note_count(%User{} = user) do
738 |> where(id: ^user.id)
743 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
750 |> Repo.update_all([])
752 {1, [user]} -> set_cache(user)
757 def update_note_count(%User{} = user) do
761 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
765 note_count = Repo.one(note_count_query)
767 info_cng = User.Info.set_note_count(user.info, note_count)
771 |> put_embed(:info, info_cng)
772 |> update_and_set_cache()
775 @spec maybe_fetch_follow_information(User.t()) :: User.t()
776 def maybe_fetch_follow_information(user) do
777 with {:ok, user} <- fetch_follow_information(user) do
781 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
787 def fetch_follow_information(user) do
788 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
789 info_cng = User.Info.follow_information_update(user.info, info)
794 |> put_embed(:info, info_cng)
796 update_and_set_cache(changeset)
803 def update_follower_count(%User{} = user) do
804 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
805 follower_count_query =
806 User.Query.build(%{followers: user, deactivated: false})
807 |> select([u], %{count: count(u.id)})
810 |> where(id: ^user.id)
811 |> join(:inner, [u], s in subquery(follower_count_query))
816 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
823 |> Repo.update_all([])
825 {1, [user]} -> set_cache(user)
829 {:ok, maybe_fetch_follow_information(user)}
833 @spec maybe_update_following_count(User.t()) :: User.t()
834 def maybe_update_following_count(%User{local: false} = user) do
835 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
836 maybe_fetch_follow_information(user)
842 def maybe_update_following_count(user), do: user
844 def remove_duplicated_following(%User{following: following} = user) do
845 uniq_following = Enum.uniq(following)
847 if length(following) == length(uniq_following) do
851 |> update_changeset(%{following: uniq_following})
852 |> update_and_set_cache()
856 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
857 def get_users_from_set(ap_ids, local_only \\ true) do
858 criteria = %{ap_id: ap_ids, deactivated: false}
859 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
861 User.Query.build(criteria)
865 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
866 def get_recipients_from_activity(%Activity{recipients: to}) do
867 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
871 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
872 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
876 User.Info.add_to_mutes(info, ap_id)
877 |> User.Info.add_to_muted_notifications(info, ap_id, notifications?)
881 |> put_embed(:info, info_cng)
883 update_and_set_cache(cng)
886 def unmute(muter, %{ap_id: ap_id}) do
890 User.Info.remove_from_mutes(info, ap_id)
891 |> User.Info.remove_from_muted_notifications(info, ap_id)
895 |> put_embed(:info, info_cng)
897 update_and_set_cache(cng)
900 def subscribe(subscriber, %{ap_id: ap_id}) do
901 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
903 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
904 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
907 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
911 |> User.Info.add_to_subscribers(subscriber.ap_id)
914 |> put_embed(:info, info_cng)
915 |> update_and_set_cache()
920 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
921 with %User{} = user <- get_cached_by_ap_id(ap_id) do
924 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
927 |> put_embed(:info, info_cng)
928 |> update_and_set_cache()
932 def block(blocker, %User{ap_id: ap_id} = blocked) do
933 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
935 if following?(blocker, blocked) do
936 {:ok, blocker, _} = unfollow(blocker, blocked)
942 # clear any requested follows as well
944 case CommonAPI.reject_follow_request(blocked, blocker) do
945 {:ok, %User{} = updated_blocked} -> updated_blocked
950 if subscribed_to?(blocked, blocker) do
951 {:ok, blocker} = unsubscribe(blocked, blocker)
957 if following?(blocked, blocker) do
958 unfollow(blocked, blocker)
961 {:ok, blocker} = update_follower_count(blocker)
965 |> User.Info.add_to_block(ap_id)
969 |> put_embed(:info, info_cng)
971 update_and_set_cache(cng)
974 # helper to handle the block given only an actor's AP id
975 def block(blocker, %{ap_id: ap_id}) do
976 block(blocker, get_cached_by_ap_id(ap_id))
979 def unblock(blocker, %{ap_id: ap_id}) do
982 |> User.Info.remove_from_block(ap_id)
986 |> put_embed(:info, info_cng)
988 update_and_set_cache(cng)
991 def mutes?(nil, _), do: false
992 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
994 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
995 def muted_notifications?(nil, _), do: false
997 def muted_notifications?(user, %{ap_id: ap_id}),
998 do: Enum.member?(user.info.muted_notifications, ap_id)
1000 def blocks?(%User{} = user, %User{} = target) do
1001 blocks_ap_id?(user, target) || blocks_domain?(user, target)
1004 def blocks?(nil, _), do: false
1006 def blocks_ap_id?(%User{} = user, %User{} = target) do
1007 Enum.member?(user.info.blocks, target.ap_id)
1010 def blocks_ap_id?(_, _), do: false
1012 def blocks_domain?(%User{} = user, %User{} = target) do
1013 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
1014 %{host: host} = URI.parse(target.ap_id)
1015 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1018 def blocks_domain?(_, _), do: false
1020 def subscribed_to?(user, %{ap_id: ap_id}) do
1021 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1022 Enum.member?(target.info.subscribers, user.ap_id)
1026 @spec muted_users(User.t()) :: [User.t()]
1027 def muted_users(user) do
1028 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
1032 @spec blocked_users(User.t()) :: [User.t()]
1033 def blocked_users(user) do
1034 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
1038 @spec subscribers(User.t()) :: [User.t()]
1039 def subscribers(user) do
1040 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
1044 def block_domain(user, domain) do
1047 |> User.Info.add_to_domain_block(domain)
1051 |> put_embed(:info, info_cng)
1053 update_and_set_cache(cng)
1056 def unblock_domain(user, domain) do
1059 |> User.Info.remove_from_domain_block(domain)
1063 |> put_embed(:info, info_cng)
1065 update_and_set_cache(cng)
1068 def deactivate_async(user, status \\ true) do
1069 PleromaJobQueue.enqueue(:background, __MODULE__, [:deactivate_async, user, status])
1072 def deactivate(%User{} = user, status \\ true) do
1073 info_cng = User.Info.set_activation_status(user.info, status)
1075 with {:ok, friends} <- User.get_friends(user),
1076 {:ok, followers} <- User.get_followers(user),
1080 |> put_embed(:info, info_cng)
1081 |> update_and_set_cache() do
1082 Enum.each(followers, &invalidate_cache(&1))
1083 Enum.each(friends, &update_follower_count(&1))
1089 def update_notification_settings(%User{} = user, settings \\ %{}) do
1090 info_changeset = User.Info.update_notification_settings(user.info, settings)
1093 |> put_embed(:info, info_changeset)
1094 |> update_and_set_cache()
1097 @spec delete(User.t()) :: :ok
1098 def delete(%User{} = user),
1099 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:delete, user])
1101 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1102 def perform(:delete, %User{} = user) do
1103 {:ok, _user} = ActivityPub.delete(user)
1105 # Remove all relationships
1106 {:ok, followers} = User.get_followers(user)
1108 Enum.each(followers, fn follower ->
1109 ActivityPub.unfollow(follower, user)
1110 User.unfollow(follower, user)
1113 {:ok, friends} = User.get_friends(user)
1115 Enum.each(friends, fn followed ->
1116 ActivityPub.unfollow(user, followed)
1117 User.unfollow(user, followed)
1120 delete_user_activities(user)
1121 invalidate_cache(user)
1125 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1126 def perform(:fetch_initial_posts, %User{} = user) do
1127 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1130 # Insert all the posts in reverse order, so they're in the right order on the timeline
1131 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
1132 &Pleroma.Web.Federator.incoming_ap_doc/1
1138 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1140 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1141 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1142 when is_list(blocked_identifiers) do
1144 blocked_identifiers,
1145 fn blocked_identifier ->
1146 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1147 {:ok, blocker} <- block(blocker, blocked),
1148 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1152 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1159 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1160 def perform(:follow_import, %User{} = follower, followed_identifiers)
1161 when is_list(followed_identifiers) do
1163 followed_identifiers,
1164 fn followed_identifier ->
1165 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1166 {:ok, follower} <- maybe_direct_follow(follower, followed),
1167 {:ok, _} <- ActivityPub.follow(follower, followed) do
1171 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1178 @spec external_users_query() :: Ecto.Query.t()
1179 def external_users_query do
1187 @spec external_users(keyword()) :: [User.t()]
1188 def external_users(opts \\ []) do
1190 external_users_query()
1191 |> select([u], struct(u, [:id, :ap_id, :info]))
1195 do: where(query, [u], u.id > ^opts[:max_id]),
1200 do: limit(query, ^opts[:limit]),
1206 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers),
1208 PleromaJobQueue.enqueue(:background, __MODULE__, [
1214 def follow_import(%User{} = follower, followed_identifiers) when is_list(followed_identifiers),
1216 PleromaJobQueue.enqueue(:background, __MODULE__, [
1219 followed_identifiers
1222 def delete_user_activities(%User{ap_id: ap_id} = user) do
1224 |> Activity.query_by_actor()
1225 |> RepoStreamer.chunk_stream(50)
1226 |> Stream.each(fn activities ->
1227 Enum.each(activities, &delete_activity(&1))
1234 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1236 |> Object.normalize()
1237 |> ActivityPub.delete()
1240 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1241 user = get_cached_by_ap_id(activity.actor)
1242 object = Object.normalize(activity)
1244 ActivityPub.unlike(user, object)
1247 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1248 user = get_cached_by_ap_id(activity.actor)
1249 object = Object.normalize(activity)
1251 ActivityPub.unannounce(user, object)
1254 defp delete_activity(_activity), do: "Doing nothing"
1256 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1257 Pleroma.HTML.Scrubber.TwitterText
1260 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1262 def fetch_by_ap_id(ap_id) do
1263 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1270 case OStatus.make_user(ap_id) do
1271 {:ok, user} -> {:ok, user}
1272 _ -> {:error, "Could not fetch by AP id"}
1277 def get_or_fetch_by_ap_id(ap_id) do
1278 user = get_cached_by_ap_id(ap_id)
1280 if !is_nil(user) and !User.needs_update?(user) do
1283 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1284 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1286 resp = fetch_by_ap_id(ap_id)
1288 if should_fetch_initial do
1289 with {:ok, %User{} = user} <- resp do
1290 fetch_initial_posts(user)
1298 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1299 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1300 if user = get_cached_by_ap_id(uri) do
1304 %User{info: %User.Info{}}
1305 |> cast(%{}, [:ap_id, :nickname, :local])
1306 |> put_change(:ap_id, uri)
1307 |> put_change(:nickname, nickname)
1308 |> put_change(:local, true)
1309 |> put_change(:follower_address, uri <> "/followers")
1311 {:ok, user} = Repo.insert(changes)
1317 def public_key_from_info(%{
1318 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1322 |> :public_key.pem_decode()
1324 |> :public_key.pem_entry_decode()
1330 def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
1331 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1334 def public_key_from_info(_), do: {:error, "not found key"}
1336 def get_public_key_for_ap_id(ap_id) do
1337 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1338 {:ok, public_key} <- public_key_from_info(user.info) do
1345 defp blank?(""), do: nil
1346 defp blank?(n), do: n
1348 def insert_or_update_user(data) do
1350 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1351 |> remote_user_creation()
1352 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1356 def ap_enabled?(%User{local: true}), do: true
1357 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1358 def ap_enabled?(_), do: false
1360 @doc "Gets or fetch a user by uri or nickname."
1361 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1362 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1363 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1365 # wait a period of time and return newest version of the User structs
1366 # this is because we have synchronous follow APIs and need to simulate them
1367 # with an async handshake
1368 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1369 with %User{} = a <- User.get_cached_by_id(a.id),
1370 %User{} = b <- User.get_cached_by_id(b.id) do
1378 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1379 with :ok <- :timer.sleep(timeout),
1380 %User{} = a <- User.get_cached_by_id(a.id),
1381 %User{} = b <- User.get_cached_by_id(b.id) do
1389 def parse_bio(bio) when is_binary(bio) and bio != "" do
1391 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1395 def parse_bio(_), do: ""
1397 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1398 # TODO: get profile URLs other than user.ap_id
1399 profile_urls = [user.ap_id]
1402 |> CommonUtils.format_input("text/plain",
1403 mentions_format: :full,
1404 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1409 def parse_bio(_, _), do: ""
1411 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1412 Repo.transaction(fn ->
1413 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1417 def tag(nickname, tags) when is_binary(nickname),
1418 do: tag(get_by_nickname(nickname), tags)
1420 def tag(%User{} = user, tags),
1421 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1423 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1424 Repo.transaction(fn ->
1425 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1429 def untag(nickname, tags) when is_binary(nickname),
1430 do: untag(get_by_nickname(nickname), tags)
1432 def untag(%User{} = user, tags),
1433 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1435 defp update_tags(%User{} = user, new_tags) do
1436 {:ok, updated_user} =
1438 |> change(%{tags: new_tags})
1439 |> update_and_set_cache()
1444 defp normalize_tags(tags) do
1447 |> Enum.map(&String.downcase(&1))
1450 defp local_nickname_regex do
1451 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1452 @extended_local_nickname_regex
1454 @strict_local_nickname_regex
1458 def local_nickname(nickname_or_mention) do
1461 |> String.split("@")
1465 def full_nickname(nickname_or_mention),
1466 do: String.trim_leading(nickname_or_mention, "@")
1468 def error_user(ap_id) do
1473 nickname: "erroruser@example.com",
1474 inserted_at: NaiveDateTime.utc_now()
1478 @spec all_superusers() :: [User.t()]
1479 def all_superusers do
1480 User.Query.build(%{super_users: true, local: true, deactivated: false})
1484 def showing_reblogs?(%User{} = user, %User{} = target) do
1485 target.ap_id not in user.info.muted_reblogs
1489 The function returns a query to get users with no activity for given interval of days.
1490 Inactive users are those who didn't read any notification, or had any activity where
1491 the user is the activity's actor, during `inactivity_threshold` days.
1492 Deactivated users will not appear in this list.
1496 iex> Pleroma.User.list_inactive_users()
1499 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1500 def list_inactive_users_query(inactivity_threshold \\ 7) do
1501 negative_inactivity_threshold = -inactivity_threshold
1502 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1503 # Subqueries are not supported in `where` clauses, join gets too complicated.
1504 has_read_notifications =
1505 from(n in Pleroma.Notification,
1506 where: n.seen == true,
1508 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1511 |> Pleroma.Repo.all()
1513 from(u in Pleroma.User,
1514 left_join: a in Pleroma.Activity,
1515 on: u.ap_id == a.actor,
1516 where: not is_nil(u.nickname),
1517 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1518 where: u.id not in ^has_read_notifications,
1521 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1522 is_nil(max(a.inserted_at))
1527 Enable or disable email notifications for user
1531 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1532 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1534 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1535 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1537 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1538 {:ok, t()} | {:error, Ecto.Changeset.t()}
1539 def switch_email_notifications(user, type, status) do
1540 info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})
1543 |> put_embed(:info, info)
1544 |> update_and_set_cache()
1548 Set `last_digest_emailed_at` value for the user to current time
1550 @spec touch_last_digest_emailed_at(t()) :: t()
1551 def touch_last_digest_emailed_at(user) do
1552 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1554 {:ok, updated_user} =
1556 |> change(%{last_digest_emailed_at: now})
1557 |> update_and_set_cache()
1562 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1563 def toggle_confirmation(%User{} = user) do
1564 need_confirmation? = !user.info.confirmation_pending
1567 User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
1571 |> put_embed(:info, info_changeset)
1572 |> update_and_set_cache()
1575 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1579 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1580 # use instance-default
1581 config = Pleroma.Config.get([:assets, :mascots])
1582 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1583 mascot = Keyword.get(config, default_mascot)
1586 "id" => "default-mascot",
1587 "url" => mascot[:url],
1588 "preview_url" => mascot[:url],
1590 "mime_type" => mascot[:mime_type]
1595 def ensure_keys_present(%User{info: info} = user) do
1599 {:ok, pem} = Keys.generate_rsa_pem()
1602 |> Ecto.Changeset.change()
1603 |> Ecto.Changeset.put_embed(:info, User.Info.set_keys(info, pem))
1604 |> update_and_set_cache()
1608 def get_ap_ids_by_nicknames(nicknames) do
1610 where: u.nickname in ^nicknames,
1616 defdelegate search(query, opts \\ []), to: User.Search
1618 defp put_password_hash(
1619 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1621 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1624 defp put_password_hash(changeset), do: changeset
1626 def is_internal_user?(%User{nickname: nil}), do: true
1627 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1628 def is_internal_user?(_), do: false
1630 # A hack because user delete activities have a fake id for whatever reason
1631 # TODO: Get rid of this
1632 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
1634 def get_delivered_users_by_object_id(object_id) do
1636 inner_join: delivery in assoc(u, :deliveries),
1637 where: delivery.object_id == ^object_id