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
31 alias Pleroma.Workers.BackgroundWorker
35 @type t :: %__MODULE__{}
37 @primary_key {:id, Pleroma.FlakeId, autogenerate: true}
39 # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
40 @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])?)*$/
42 @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
43 @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
47 field(:email, :string)
49 field(:nickname, :string)
50 field(:password_hash, :string)
51 field(:password, :string, virtual: true)
52 field(:password_confirmation, :string, virtual: true)
53 field(:following, {:array, :string}, default: [])
54 field(:ap_id, :string)
56 field(:local, :boolean, default: true)
57 field(:follower_address, :string)
58 field(:following_address, :string)
59 field(:search_rank, :float, virtual: true)
60 field(:search_type, :integer, virtual: true)
61 field(:tags, {:array, :string}, default: [])
62 field(:last_refreshed_at, :naive_datetime_usec)
63 field(:last_digest_emailed_at, :naive_datetime)
64 has_many(:notifications, Notification)
65 has_many(:registrations, Registration)
66 has_many(:deliveries, Delivery)
67 embeds_one(:info, User.Info)
72 def auth_active?(%User{info: %User.Info{confirmation_pending: true}}),
73 do: !Pleroma.Config.get([:instance, :account_activation_required])
75 def auth_active?(%User{}), do: true
77 def visible_for?(user, for_user \\ nil)
79 def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
81 def visible_for?(%User{} = user, for_user) do
82 auth_active?(user) || superuser?(for_user)
85 def visible_for?(_, _), do: false
87 def superuser?(%User{local: true, info: %User.Info{is_admin: true}}), do: true
88 def superuser?(%User{local: true, info: %User.Info{is_moderator: true}}), do: true
89 def superuser?(_), do: false
91 def avatar_url(user, options \\ []) do
93 %{"url" => [%{"href" => href} | _]} -> href
94 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
98 def banner_url(user, options \\ []) do
99 case user.info.banner do
100 %{"url" => [%{"href" => href} | _]} -> href
101 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
105 def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url
106 def profile_url(%User{ap_id: ap_id}), do: ap_id
107 def profile_url(_), do: nil
109 def ap_id(%User{nickname: nickname}) do
110 "#{Web.base_url()}/users/#{nickname}"
113 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
114 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
116 @spec ap_following(User.t()) :: Sring.t()
117 def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
118 def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
120 def user_info(%User{} = user, args \\ %{}) do
122 if args[:following_count],
123 do: args[:following_count],
124 else: user.info.following_count || following_count(user)
127 if args[:follower_count], do: args[:follower_count], else: user.info.follower_count
130 note_count: user.info.note_count,
131 locked: user.info.locked,
132 confirmation_pending: user.info.confirmation_pending,
133 default_scope: user.info.default_scope
135 |> Map.put(:following_count, following_count)
136 |> Map.put(:follower_count, follower_count)
139 def follow_state(%User{} = user, %User{} = target) do
140 follow_activity = Utils.fetch_latest_follow(user, target)
143 do: follow_activity.data["state"],
144 # Ideally this would be nil, but then Cachex does not commit the value
148 def get_cached_follow_state(user, target) do
149 key = "follow_state:#{user.ap_id}|#{target.ap_id}"
150 Cachex.fetch!(:user_cache, key, fn _ -> {:commit, follow_state(user, target)} end)
153 def set_follow_state_cache(user_ap_id, target_ap_id, state) do
156 "follow_state:#{user_ap_id}|#{target_ap_id}",
161 def set_info_cache(user, args) do
162 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user, args))
165 @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
166 def restrict_deactivated(query) do
168 where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info)
172 def following_count(%User{following: []}), do: 0
174 def following_count(%User{} = user) do
176 |> get_friends_query()
177 |> Repo.aggregate(:count, :id)
180 defp truncate_if_exists(params, key, max_length) do
181 if Map.has_key?(params, key) and is_binary(params[key]) do
182 {value, _chopped} = String.split_at(params[key], max_length)
183 Map.put(params, key, value)
189 def remote_user_creation(params) do
190 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
191 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
195 |> Map.put(:info, params[:info] || %{})
196 |> truncate_if_exists(:name, name_limit)
197 |> truncate_if_exists(:bio, bio_limit)
199 info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])
203 |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
204 |> validate_required([:name, :ap_id])
205 |> unique_constraint(:nickname)
206 |> validate_format(:nickname, @email_regex)
207 |> validate_length(:bio, max: bio_limit)
208 |> validate_length(:name, max: name_limit)
209 |> put_change(:local, false)
210 |> put_embed(:info, info_cng)
213 case info_cng.changes[:source_data] do
214 %{"followers" => followers, "following" => following} ->
216 |> put_change(:follower_address, followers)
217 |> put_change(:following_address, following)
220 followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
223 |> put_change(:follower_address, followers)
230 def update_changeset(struct, params \\ %{}) do
231 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
232 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
235 |> cast(params, [:bio, :name, :avatar, :following])
236 |> unique_constraint(:nickname)
237 |> validate_format(:nickname, local_nickname_regex())
238 |> validate_length(:bio, max: bio_limit)
239 |> validate_length(:name, min: 1, max: name_limit)
242 def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
243 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
244 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
246 params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
247 info_cng = User.Info.user_upgrade(struct.info, params[:info], remote?)
258 |> unique_constraint(:nickname)
259 |> validate_format(:nickname, local_nickname_regex())
260 |> validate_length(:bio, max: bio_limit)
261 |> validate_length(:name, max: name_limit)
262 |> put_embed(:info, info_cng)
265 def password_update_changeset(struct, params) do
267 |> cast(params, [:password, :password_confirmation])
268 |> validate_required([:password, :password_confirmation])
269 |> validate_confirmation(:password)
273 @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
274 def reset_password(%User{id: user_id} = user, data) do
277 |> Multi.update(:user, password_update_changeset(user, data))
278 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
279 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
281 case Repo.transaction(multi) do
282 {:ok, %{user: user} = _} -> set_cache(user)
283 {:error, _, changeset, _} -> {:error, changeset}
287 def register_changeset(struct, params \\ %{}, opts \\ []) do
288 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
289 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
292 if is_nil(opts[:need_confirmation]) do
293 Pleroma.Config.get([:instance, :account_activation_required])
295 opts[:need_confirmation]
299 User.Info.confirmation_changeset(%User.Info{}, need_confirmation: need_confirmation?)
303 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
304 |> validate_required([:name, :nickname, :password, :password_confirmation])
305 |> validate_confirmation(:password)
306 |> unique_constraint(:email)
307 |> unique_constraint(:nickname)
308 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
309 |> validate_format(:nickname, local_nickname_regex())
310 |> validate_format(:email, @email_regex)
311 |> validate_length(:bio, max: bio_limit)
312 |> validate_length(:name, min: 1, max: name_limit)
313 |> put_change(:info, info_change)
316 if opts[:external] do
319 validate_required(changeset, [:email])
322 if changeset.valid? do
323 ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
324 followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
328 |> put_change(:ap_id, ap_id)
329 |> unique_constraint(:ap_id)
330 |> put_change(:following, [followers])
331 |> put_change(:follower_address, followers)
337 defp autofollow_users(user) do
338 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
341 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
344 follow_all(user, autofollowed_users)
347 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
348 def register(%Ecto.Changeset{} = changeset) do
349 with {:ok, user} <- Repo.insert(changeset),
350 {:ok, user} <- post_register_action(user) do
355 def post_register_action(%User{} = user) do
356 with {:ok, user} <- autofollow_users(user),
357 {:ok, user} <- set_cache(user),
358 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
359 {:ok, _} <- try_send_confirmation_email(user) do
364 def try_send_confirmation_email(%User{} = user) do
365 if user.info.confirmation_pending &&
366 Pleroma.Config.get([:instance, :account_activation_required]) do
368 |> Pleroma.Emails.UserEmail.account_confirmation_email()
369 |> Pleroma.Emails.Mailer.deliver_async()
377 def needs_update?(%User{local: true}), do: false
379 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
381 def needs_update?(%User{local: false} = user) do
382 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
385 def needs_update?(_), do: true
387 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
388 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
392 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
393 follow(follower, followed)
396 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
397 if not User.ap_enabled?(followed) do
398 follow(follower, followed)
404 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
405 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
406 def follow_all(follower, followeds) do
409 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
410 |> Enum.map(fn %{follower_address: fa} -> fa end)
414 where: u.id == ^follower.id,
419 "array(select distinct unnest (array_cat(?, ?)))",
428 {1, [follower]} = Repo.update_all(q, [])
430 Enum.each(followeds, fn followed ->
431 update_follower_count(followed)
437 def follow(%User{} = follower, %User{info: info} = followed) do
438 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
439 ap_followers = followed.follower_address
443 {:error, "Could not follow user: You are deactivated."}
445 deny_follow_blocked and blocks?(followed, follower) ->
446 {:error, "Could not follow user: #{followed.nickname} blocked you."}
449 if !followed.local && follower.local && !ap_enabled?(followed) do
450 Websub.subscribe(follower, followed)
455 where: u.id == ^follower.id,
456 update: [push: [following: ^ap_followers]],
460 {1, [follower]} = Repo.update_all(q, [])
462 follower = maybe_update_following_count(follower)
464 {:ok, _} = update_follower_count(followed)
470 def unfollow(%User{} = follower, %User{} = followed) do
471 ap_followers = followed.follower_address
473 if following?(follower, followed) and follower.ap_id != followed.ap_id do
476 where: u.id == ^follower.id,
477 update: [pull: [following: ^ap_followers]],
481 {1, [follower]} = Repo.update_all(q, [])
483 follower = maybe_update_following_count(follower)
485 {:ok, followed} = update_follower_count(followed)
489 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
491 {:error, "Not subscribed!"}
495 @spec following?(User.t(), User.t()) :: boolean
496 def following?(%User{} = follower, %User{} = followed) do
497 Enum.member?(follower.following, followed.follower_address)
500 def locked?(%User{} = user) do
501 user.info.locked || false
505 Repo.get_by(User, id: id)
508 def get_by_ap_id(ap_id) do
509 Repo.get_by(User, ap_id: ap_id)
512 def get_all_by_ap_id(ap_ids) do
513 from(u in __MODULE__,
514 where: u.ap_id in ^ap_ids
519 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
520 # of the ap_id and the domain and tries to get that user
521 def get_by_guessed_nickname(ap_id) do
522 domain = URI.parse(ap_id).host
523 name = List.last(String.split(ap_id, "/"))
524 nickname = "#{name}@#{domain}"
526 get_cached_by_nickname(nickname)
529 def set_cache({:ok, user}), do: set_cache(user)
530 def set_cache({:error, err}), do: {:error, err}
532 def set_cache(%User{} = user) do
533 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
534 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
535 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
539 def update_and_set_cache(changeset) do
540 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
547 def invalidate_cache(user) do
548 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
549 Cachex.del(:user_cache, "nickname:#{user.nickname}")
550 Cachex.del(:user_cache, "user_info:#{user.id}")
553 def get_cached_by_ap_id(ap_id) do
554 key = "ap_id:#{ap_id}"
555 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
558 def get_cached_by_id(id) do
562 Cachex.fetch!(:user_cache, key, fn _ ->
566 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
567 {:commit, user.ap_id}
573 get_cached_by_ap_id(ap_id)
576 def get_cached_by_nickname(nickname) do
577 key = "nickname:#{nickname}"
579 Cachex.fetch!(:user_cache, key, fn ->
580 user_result = get_or_fetch_by_nickname(nickname)
583 {:ok, user} -> {:commit, user}
584 {:error, _error} -> {:ignore, nil}
589 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
590 restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
593 is_integer(nickname_or_id) or Pleroma.FlakeId.is_flake_id?(nickname_or_id) ->
594 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
596 restrict_to_local == false ->
597 get_cached_by_nickname(nickname_or_id)
599 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
600 get_cached_by_nickname(nickname_or_id)
607 def get_by_nickname(nickname) do
608 Repo.get_by(User, nickname: nickname) ||
609 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
610 Repo.get_by(User, nickname: local_nickname(nickname))
614 def get_by_email(email), do: Repo.get_by(User, email: email)
616 def get_by_nickname_or_email(nickname_or_email) do
617 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
620 def get_cached_user_info(user) do
621 key = "user_info:#{user.id}"
622 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
625 def fetch_by_nickname(nickname) do
626 ap_try = ActivityPub.make_user_from_nickname(nickname)
629 {:ok, user} -> {:ok, user}
630 _ -> OStatus.make_user(nickname)
634 def get_or_fetch_by_nickname(nickname) do
635 with %User{} = user <- get_by_nickname(nickname) do
639 with [_nick, _domain] <- String.split(nickname, "@"),
640 {:ok, user} <- fetch_by_nickname(nickname) do
641 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
642 fetch_initial_posts(user)
647 _e -> {:error, "not found " <> nickname}
652 @doc "Fetch some posts when the user has just been federated with"
653 def fetch_initial_posts(user) do
654 BackgroundWorker.enqueue("fetch_initial_posts", %{"user_id" => user.id})
657 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
658 def get_followers_query(%User{} = user, nil) do
659 User.Query.build(%{followers: user, deactivated: false})
662 def get_followers_query(user, page) do
663 from(u in get_followers_query(user, nil))
664 |> User.Query.paginate(page, 20)
667 @spec get_followers_query(User.t()) :: Ecto.Query.t()
668 def get_followers_query(user), do: get_followers_query(user, nil)
670 @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
671 def get_followers(user, page \\ nil) do
672 q = get_followers_query(user, page)
677 @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
678 def get_external_followers(user, page \\ nil) do
681 |> get_followers_query(page)
682 |> User.Query.build(%{external: true})
687 def get_followers_ids(user, page \\ nil) do
688 q = get_followers_query(user, page)
690 Repo.all(from(u in q, select: u.id))
693 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
694 def get_friends_query(%User{} = user, nil) do
695 User.Query.build(%{friends: user, deactivated: false})
698 def get_friends_query(user, page) do
699 from(u in get_friends_query(user, nil))
700 |> User.Query.paginate(page, 20)
703 @spec get_friends_query(User.t()) :: Ecto.Query.t()
704 def get_friends_query(user), do: get_friends_query(user, nil)
706 def get_friends(user, page \\ nil) do
707 q = get_friends_query(user, page)
712 def get_friends_ids(user, page \\ nil) do
713 q = get_friends_query(user, page)
715 Repo.all(from(u in q, select: u.id))
718 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
719 def get_follow_requests(%User{} = user) do
721 Activity.follow_requests_for_actor(user)
722 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
723 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
724 |> group_by([a, u], u.id)
731 def increase_note_count(%User{} = user) do
733 |> where(id: ^user.id)
738 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
745 |> Repo.update_all([])
747 {1, [user]} -> set_cache(user)
752 def decrease_note_count(%User{} = user) do
754 |> where(id: ^user.id)
759 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
766 |> Repo.update_all([])
768 {1, [user]} -> set_cache(user)
773 def update_note_count(%User{} = user) do
777 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
781 note_count = Repo.one(note_count_query)
783 info_cng = User.Info.set_note_count(user.info, note_count)
787 |> put_embed(:info, info_cng)
788 |> update_and_set_cache()
791 @spec maybe_fetch_follow_information(User.t()) :: User.t()
792 def maybe_fetch_follow_information(user) do
793 with {:ok, user} <- fetch_follow_information(user) do
797 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
803 def fetch_follow_information(user) do
804 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
805 info_cng = User.Info.follow_information_update(user.info, info)
810 |> put_embed(:info, info_cng)
812 update_and_set_cache(changeset)
819 def update_follower_count(%User{} = user) do
820 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
821 follower_count_query =
822 User.Query.build(%{followers: user, deactivated: false})
823 |> select([u], %{count: count(u.id)})
826 |> where(id: ^user.id)
827 |> join(:inner, [u], s in subquery(follower_count_query))
832 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
839 |> Repo.update_all([])
841 {1, [user]} -> set_cache(user)
845 {:ok, maybe_fetch_follow_information(user)}
849 @spec maybe_update_following_count(User.t()) :: User.t()
850 def maybe_update_following_count(%User{local: false} = user) do
851 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
852 maybe_fetch_follow_information(user)
858 def maybe_update_following_count(user), do: user
860 def remove_duplicated_following(%User{following: following} = user) do
861 uniq_following = Enum.uniq(following)
863 if length(following) == length(uniq_following) do
867 |> update_changeset(%{following: uniq_following})
868 |> update_and_set_cache()
872 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
873 def get_users_from_set(ap_ids, local_only \\ true) do
874 criteria = %{ap_id: ap_ids, deactivated: false}
875 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
877 User.Query.build(criteria)
881 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
882 def get_recipients_from_activity(%Activity{recipients: to}) do
883 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
887 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
888 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
892 User.Info.add_to_mutes(info, ap_id)
893 |> User.Info.add_to_muted_notifications(info, ap_id, notifications?)
897 |> put_embed(:info, info_cng)
899 update_and_set_cache(cng)
902 def unmute(muter, %{ap_id: ap_id}) do
906 User.Info.remove_from_mutes(info, ap_id)
907 |> User.Info.remove_from_muted_notifications(info, ap_id)
911 |> put_embed(:info, info_cng)
913 update_and_set_cache(cng)
916 def subscribe(subscriber, %{ap_id: ap_id}) do
917 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
919 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
920 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
923 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
927 |> User.Info.add_to_subscribers(subscriber.ap_id)
930 |> put_embed(:info, info_cng)
931 |> update_and_set_cache()
936 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
937 with %User{} = user <- get_cached_by_ap_id(ap_id) do
940 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
943 |> put_embed(:info, info_cng)
944 |> update_and_set_cache()
948 def block(blocker, %User{ap_id: ap_id} = blocked) do
949 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
951 if following?(blocker, blocked) do
952 {:ok, blocker, _} = unfollow(blocker, blocked)
958 # clear any requested follows as well
960 case CommonAPI.reject_follow_request(blocked, blocker) do
961 {:ok, %User{} = updated_blocked} -> updated_blocked
966 if subscribed_to?(blocked, blocker) do
967 {:ok, blocker} = unsubscribe(blocked, blocker)
973 if following?(blocked, blocker) do
974 unfollow(blocked, blocker)
977 {:ok, blocker} = update_follower_count(blocker)
981 |> User.Info.add_to_block(ap_id)
985 |> put_embed(:info, info_cng)
987 update_and_set_cache(cng)
990 # helper to handle the block given only an actor's AP id
991 def block(blocker, %{ap_id: ap_id}) do
992 block(blocker, get_cached_by_ap_id(ap_id))
995 def unblock(blocker, %{ap_id: ap_id}) do
998 |> User.Info.remove_from_block(ap_id)
1002 |> put_embed(:info, info_cng)
1004 update_and_set_cache(cng)
1007 def mutes?(nil, _), do: false
1008 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
1010 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
1011 def muted_notifications?(nil, _), do: false
1013 def muted_notifications?(user, %{ap_id: ap_id}),
1014 do: Enum.member?(user.info.muted_notifications, ap_id)
1016 def blocks?(%User{} = user, %User{} = target) do
1017 blocks_ap_id?(user, target) || blocks_domain?(user, target)
1020 def blocks?(nil, _), do: false
1022 def blocks_ap_id?(%User{} = user, %User{} = target) do
1023 Enum.member?(user.info.blocks, target.ap_id)
1026 def blocks_ap_id?(_, _), do: false
1028 def blocks_domain?(%User{} = user, %User{} = target) do
1029 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
1030 %{host: host} = URI.parse(target.ap_id)
1031 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1034 def blocks_domain?(_, _), do: false
1036 def subscribed_to?(user, %{ap_id: ap_id}) do
1037 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1038 Enum.member?(target.info.subscribers, user.ap_id)
1042 @spec muted_users(User.t()) :: [User.t()]
1043 def muted_users(user) do
1044 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
1048 @spec blocked_users(User.t()) :: [User.t()]
1049 def blocked_users(user) do
1050 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
1054 @spec subscribers(User.t()) :: [User.t()]
1055 def subscribers(user) do
1056 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
1060 def block_domain(user, domain) do
1063 |> User.Info.add_to_domain_block(domain)
1067 |> put_embed(:info, info_cng)
1069 update_and_set_cache(cng)
1072 def unblock_domain(user, domain) do
1075 |> User.Info.remove_from_domain_block(domain)
1079 |> put_embed(:info, info_cng)
1081 update_and_set_cache(cng)
1084 def deactivate_async(user, status \\ true) do
1085 BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
1088 def deactivate(%User{} = user, status \\ true) do
1089 info_cng = User.Info.set_activation_status(user.info, status)
1091 with {:ok, friends} <- User.get_friends(user),
1092 {:ok, followers} <- User.get_followers(user),
1096 |> put_embed(:info, info_cng)
1097 |> update_and_set_cache() do
1098 Enum.each(followers, &invalidate_cache(&1))
1099 Enum.each(friends, &update_follower_count(&1))
1105 def update_notification_settings(%User{} = user, settings \\ %{}) do
1106 info_changeset = User.Info.update_notification_settings(user.info, settings)
1109 |> put_embed(:info, info_changeset)
1110 |> update_and_set_cache()
1113 def delete(%User{} = user) do
1114 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1117 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1118 def perform(:delete, %User{} = user) do
1119 {:ok, _user} = ActivityPub.delete(user)
1121 # Remove all relationships
1122 {:ok, followers} = User.get_followers(user)
1124 Enum.each(followers, fn follower ->
1125 ActivityPub.unfollow(follower, user)
1126 User.unfollow(follower, user)
1129 {:ok, friends} = User.get_friends(user)
1131 Enum.each(friends, fn followed ->
1132 ActivityPub.unfollow(user, followed)
1133 User.unfollow(user, followed)
1136 delete_user_activities(user)
1137 invalidate_cache(user)
1141 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1142 def perform(:fetch_initial_posts, %User{} = user) do
1143 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1146 # Insert all the posts in reverse order, so they're in the right order on the timeline
1147 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
1148 &Pleroma.Web.Federator.incoming_ap_doc/1
1154 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1156 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1157 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1158 when is_list(blocked_identifiers) do
1160 blocked_identifiers,
1161 fn blocked_identifier ->
1162 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1163 {:ok, blocker} <- block(blocker, blocked),
1164 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1168 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1175 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1176 def perform(:follow_import, %User{} = follower, followed_identifiers)
1177 when is_list(followed_identifiers) do
1179 followed_identifiers,
1180 fn followed_identifier ->
1181 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1182 {:ok, follower} <- maybe_direct_follow(follower, followed),
1183 {:ok, _} <- ActivityPub.follow(follower, followed) do
1187 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1194 @spec external_users_query() :: Ecto.Query.t()
1195 def external_users_query do
1203 @spec external_users(keyword()) :: [User.t()]
1204 def external_users(opts \\ []) do
1206 external_users_query()
1207 |> select([u], struct(u, [:id, :ap_id, :info]))
1211 do: where(query, [u], u.id > ^opts[:max_id]),
1216 do: limit(query, ^opts[:limit]),
1222 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1223 BackgroundWorker.enqueue("blocks_import", %{
1224 "blocker_id" => blocker.id,
1225 "blocked_identifiers" => blocked_identifiers
1229 def follow_import(%User{} = follower, followed_identifiers)
1230 when is_list(followed_identifiers) do
1231 BackgroundWorker.enqueue("follow_import", %{
1232 "follower_id" => follower.id,
1233 "followed_identifiers" => followed_identifiers
1237 def delete_user_activities(%User{ap_id: ap_id} = user) do
1239 |> Activity.Queries.by_actor()
1240 |> RepoStreamer.chunk_stream(50)
1241 |> Stream.each(fn activities ->
1242 Enum.each(activities, &delete_activity(&1))
1249 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1251 |> Object.normalize()
1252 |> ActivityPub.delete()
1255 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1256 user = get_cached_by_ap_id(activity.actor)
1257 object = Object.normalize(activity)
1259 ActivityPub.unlike(user, object)
1262 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1263 user = get_cached_by_ap_id(activity.actor)
1264 object = Object.normalize(activity)
1266 ActivityPub.unannounce(user, object)
1269 defp delete_activity(_activity), do: "Doing nothing"
1271 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1272 Pleroma.HTML.Scrubber.TwitterText
1275 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1277 def fetch_by_ap_id(ap_id) do
1278 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1285 case OStatus.make_user(ap_id) do
1286 {:ok, user} -> {:ok, user}
1287 _ -> {:error, "Could not fetch by AP id"}
1292 def get_or_fetch_by_ap_id(ap_id) do
1293 user = get_cached_by_ap_id(ap_id)
1295 if !is_nil(user) and !User.needs_update?(user) do
1298 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1299 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1301 resp = fetch_by_ap_id(ap_id)
1303 if should_fetch_initial do
1304 with {:ok, %User{} = user} <- resp do
1305 fetch_initial_posts(user)
1313 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1314 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1315 if user = get_cached_by_ap_id(uri) do
1319 %User{info: %User.Info{}}
1320 |> cast(%{}, [:ap_id, :nickname, :local])
1321 |> put_change(:ap_id, uri)
1322 |> put_change(:nickname, nickname)
1323 |> put_change(:local, true)
1324 |> put_change(:follower_address, uri <> "/followers")
1326 {:ok, user} = Repo.insert(changes)
1332 def public_key_from_info(%{
1333 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1337 |> :public_key.pem_decode()
1339 |> :public_key.pem_entry_decode()
1345 def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
1346 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1349 def public_key_from_info(_), do: {:error, "not found key"}
1351 def get_public_key_for_ap_id(ap_id) do
1352 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1353 {:ok, public_key} <- public_key_from_info(user.info) do
1360 defp blank?(""), do: nil
1361 defp blank?(n), do: n
1363 def insert_or_update_user(data) do
1365 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1366 |> remote_user_creation()
1367 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1371 def ap_enabled?(%User{local: true}), do: true
1372 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1373 def ap_enabled?(_), do: false
1375 @doc "Gets or fetch a user by uri or nickname."
1376 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1377 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1378 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1380 # wait a period of time and return newest version of the User structs
1381 # this is because we have synchronous follow APIs and need to simulate them
1382 # with an async handshake
1383 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1384 with %User{} = a <- User.get_cached_by_id(a.id),
1385 %User{} = b <- User.get_cached_by_id(b.id) do
1393 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1394 with :ok <- :timer.sleep(timeout),
1395 %User{} = a <- User.get_cached_by_id(a.id),
1396 %User{} = b <- User.get_cached_by_id(b.id) do
1404 def parse_bio(bio) when is_binary(bio) and bio != "" do
1406 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1410 def parse_bio(_), do: ""
1412 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1413 # TODO: get profile URLs other than user.ap_id
1414 profile_urls = [user.ap_id]
1417 |> CommonUtils.format_input("text/plain",
1418 mentions_format: :full,
1419 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1424 def parse_bio(_, _), do: ""
1426 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1427 Repo.transaction(fn ->
1428 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1432 def tag(nickname, tags) when is_binary(nickname),
1433 do: tag(get_by_nickname(nickname), tags)
1435 def tag(%User{} = user, tags),
1436 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1438 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1439 Repo.transaction(fn ->
1440 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1444 def untag(nickname, tags) when is_binary(nickname),
1445 do: untag(get_by_nickname(nickname), tags)
1447 def untag(%User{} = user, tags),
1448 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1450 defp update_tags(%User{} = user, new_tags) do
1451 {:ok, updated_user} =
1453 |> change(%{tags: new_tags})
1454 |> update_and_set_cache()
1459 defp normalize_tags(tags) do
1462 |> Enum.map(&String.downcase(&1))
1465 defp local_nickname_regex do
1466 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1467 @extended_local_nickname_regex
1469 @strict_local_nickname_regex
1473 def local_nickname(nickname_or_mention) do
1476 |> String.split("@")
1480 def full_nickname(nickname_or_mention),
1481 do: String.trim_leading(nickname_or_mention, "@")
1483 def error_user(ap_id) do
1488 nickname: "erroruser@example.com",
1489 inserted_at: NaiveDateTime.utc_now()
1493 @spec all_superusers() :: [User.t()]
1494 def all_superusers do
1495 User.Query.build(%{super_users: true, local: true, deactivated: false})
1499 def showing_reblogs?(%User{} = user, %User{} = target) do
1500 target.ap_id not in user.info.muted_reblogs
1504 The function returns a query to get users with no activity for given interval of days.
1505 Inactive users are those who didn't read any notification, or had any activity where
1506 the user is the activity's actor, during `inactivity_threshold` days.
1507 Deactivated users will not appear in this list.
1511 iex> Pleroma.User.list_inactive_users()
1514 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1515 def list_inactive_users_query(inactivity_threshold \\ 7) do
1516 negative_inactivity_threshold = -inactivity_threshold
1517 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1518 # Subqueries are not supported in `where` clauses, join gets too complicated.
1519 has_read_notifications =
1520 from(n in Pleroma.Notification,
1521 where: n.seen == true,
1523 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1526 |> Pleroma.Repo.all()
1528 from(u in Pleroma.User,
1529 left_join: a in Pleroma.Activity,
1530 on: u.ap_id == a.actor,
1531 where: not is_nil(u.nickname),
1532 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1533 where: u.id not in ^has_read_notifications,
1536 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1537 is_nil(max(a.inserted_at))
1542 Enable or disable email notifications for user
1546 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1547 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1549 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1550 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1552 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1553 {:ok, t()} | {:error, Ecto.Changeset.t()}
1554 def switch_email_notifications(user, type, status) do
1555 info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})
1558 |> put_embed(:info, info)
1559 |> update_and_set_cache()
1563 Set `last_digest_emailed_at` value for the user to current time
1565 @spec touch_last_digest_emailed_at(t()) :: t()
1566 def touch_last_digest_emailed_at(user) do
1567 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1569 {:ok, updated_user} =
1571 |> change(%{last_digest_emailed_at: now})
1572 |> update_and_set_cache()
1577 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1578 def toggle_confirmation(%User{} = user) do
1579 need_confirmation? = !user.info.confirmation_pending
1582 User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
1586 |> put_embed(:info, info_changeset)
1587 |> update_and_set_cache()
1590 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1594 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1595 # use instance-default
1596 config = Pleroma.Config.get([:assets, :mascots])
1597 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1598 mascot = Keyword.get(config, default_mascot)
1601 "id" => "default-mascot",
1602 "url" => mascot[:url],
1603 "preview_url" => mascot[:url],
1605 "mime_type" => mascot[:mime_type]
1610 def ensure_keys_present(%User{info: info} = user) do
1614 {:ok, pem} = Keys.generate_rsa_pem()
1617 |> Ecto.Changeset.change()
1618 |> Ecto.Changeset.put_embed(:info, User.Info.set_keys(info, pem))
1619 |> update_and_set_cache()
1623 def get_ap_ids_by_nicknames(nicknames) do
1625 where: u.nickname in ^nicknames,
1631 defdelegate search(query, opts \\ []), to: User.Search
1633 defp put_password_hash(
1634 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1636 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1639 defp put_password_hash(changeset), do: changeset
1641 def is_internal_user?(%User{nickname: nil}), do: true
1642 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1643 def is_internal_user?(_), do: false
1645 # A hack because user delete activities have a fake id for whatever reason
1646 # TODO: Get rid of this
1647 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
1649 def get_delivered_users_by_object_id(object_id) do
1651 inner_join: delivery in assoc(u, :deliveries),
1652 where: delivery.object_id == ^object_id
1657 def change_email(user, email) do
1659 |> cast(%{email: email}, [:email])
1660 |> validate_required([:email])
1661 |> unique_constraint(:email)
1662 |> validate_format(:email, @email_regex)
1663 |> update_and_set_cache()