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 @spec set_follow_state_cache(String.t(), String.t(), String.t()) :: {:ok | :error, boolean()}
154 def set_follow_state_cache(user_ap_id, target_ap_id, state) do
157 "follow_state:#{user_ap_id}|#{target_ap_id}",
162 def set_info_cache(user, args) do
163 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user, args))
166 @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
167 def restrict_deactivated(query) do
169 where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info)
173 def following_count(%User{following: []}), do: 0
175 def following_count(%User{} = user) do
177 |> get_friends_query()
178 |> Repo.aggregate(:count, :id)
181 defp truncate_if_exists(params, key, max_length) do
182 if Map.has_key?(params, key) and is_binary(params[key]) do
183 {value, _chopped} = String.split_at(params[key], max_length)
184 Map.put(params, key, value)
190 def remote_user_creation(params) do
191 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
192 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
196 |> Map.put(:info, params[:info] || %{})
197 |> truncate_if_exists(:name, name_limit)
198 |> truncate_if_exists(:bio, bio_limit)
200 info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])
204 |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
205 |> validate_required([:name, :ap_id])
206 |> unique_constraint(:nickname)
207 |> validate_format(:nickname, @email_regex)
208 |> validate_length(:bio, max: bio_limit)
209 |> validate_length(:name, max: name_limit)
210 |> put_change(:local, false)
211 |> put_embed(:info, info_cng)
214 case info_cng.changes[:source_data] do
215 %{"followers" => followers, "following" => following} ->
217 |> put_change(:follower_address, followers)
218 |> put_change(:following_address, following)
221 followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
224 |> put_change(:follower_address, followers)
231 def update_changeset(struct, params \\ %{}) do
232 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
233 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
236 |> cast(params, [:bio, :name, :avatar, :following])
237 |> unique_constraint(:nickname)
238 |> validate_format(:nickname, local_nickname_regex())
239 |> validate_length(:bio, max: bio_limit)
240 |> validate_length(:name, min: 1, max: name_limit)
243 def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
244 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
245 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
247 params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
248 info_cng = User.Info.user_upgrade(struct.info, params[:info], remote?)
259 |> unique_constraint(:nickname)
260 |> validate_format(:nickname, local_nickname_regex())
261 |> validate_length(:bio, max: bio_limit)
262 |> validate_length(:name, max: name_limit)
263 |> put_embed(:info, info_cng)
266 def password_update_changeset(struct, params) do
268 |> cast(params, [:password, :password_confirmation])
269 |> validate_required([:password, :password_confirmation])
270 |> validate_confirmation(:password)
272 |> put_embed(:info, User.Info.set_password_reset_pending(struct.info, false))
275 @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
276 def reset_password(%User{id: user_id} = user, data) do
279 |> Multi.update(:user, password_update_changeset(user, data))
280 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
281 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
283 case Repo.transaction(multi) do
284 {:ok, %{user: user} = _} -> set_cache(user)
285 {:error, _, changeset, _} -> {:error, changeset}
289 def force_password_reset_async(user) do
290 BackgroundWorker.enqueue("force_password_reset", %{"user_id" => user.id})
293 @spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
294 def force_password_reset(user) do
295 info_cng = User.Info.set_password_reset_pending(user.info, true)
299 |> put_embed(:info, info_cng)
300 |> update_and_set_cache()
303 def register_changeset(struct, params \\ %{}, opts \\ []) do
304 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
305 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
308 if is_nil(opts[:need_confirmation]) do
309 Pleroma.Config.get([:instance, :account_activation_required])
311 opts[:need_confirmation]
315 User.Info.confirmation_changeset(%User.Info{}, need_confirmation: need_confirmation?)
319 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
320 |> validate_required([:name, :nickname, :password, :password_confirmation])
321 |> validate_confirmation(:password)
322 |> unique_constraint(:email)
323 |> unique_constraint(:nickname)
324 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
325 |> validate_format(:nickname, local_nickname_regex())
326 |> validate_format(:email, @email_regex)
327 |> validate_length(:bio, max: bio_limit)
328 |> validate_length(:name, min: 1, max: name_limit)
329 |> put_change(:info, info_change)
332 if opts[:external] do
335 validate_required(changeset, [:email])
338 if changeset.valid? do
339 ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
340 followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
344 |> put_change(:ap_id, ap_id)
345 |> unique_constraint(:ap_id)
346 |> put_change(:following, [followers])
347 |> put_change(:follower_address, followers)
353 defp autofollow_users(user) do
354 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
357 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
360 follow_all(user, autofollowed_users)
363 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
364 def register(%Ecto.Changeset{} = changeset) do
365 with {:ok, user} <- Repo.insert(changeset),
366 {:ok, user} <- post_register_action(user) do
371 def post_register_action(%User{} = user) do
372 with {:ok, user} <- autofollow_users(user),
373 {:ok, user} <- set_cache(user),
374 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
375 {:ok, _} <- try_send_confirmation_email(user) do
380 def try_send_confirmation_email(%User{} = user) do
381 if user.info.confirmation_pending &&
382 Pleroma.Config.get([:instance, :account_activation_required]) do
384 |> Pleroma.Emails.UserEmail.account_confirmation_email()
385 |> Pleroma.Emails.Mailer.deliver_async()
393 def needs_update?(%User{local: true}), do: false
395 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
397 def needs_update?(%User{local: false} = user) do
398 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
401 def needs_update?(_), do: true
403 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
404 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
408 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
409 follow(follower, followed)
412 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
413 if not User.ap_enabled?(followed) do
414 follow(follower, followed)
420 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
421 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
422 def follow_all(follower, followeds) do
425 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
426 |> Enum.map(fn %{follower_address: fa} -> fa end)
430 where: u.id == ^follower.id,
435 "array(select distinct unnest (array_cat(?, ?)))",
444 {1, [follower]} = Repo.update_all(q, [])
446 Enum.each(followeds, fn followed ->
447 update_follower_count(followed)
453 def follow(%User{} = follower, %User{info: info} = followed) do
454 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
455 ap_followers = followed.follower_address
459 {:error, "Could not follow user: You are deactivated."}
461 deny_follow_blocked and blocks?(followed, follower) ->
462 {:error, "Could not follow user: #{followed.nickname} blocked you."}
465 if !followed.local && follower.local && !ap_enabled?(followed) do
466 Websub.subscribe(follower, followed)
471 where: u.id == ^follower.id,
472 update: [push: [following: ^ap_followers]],
476 {1, [follower]} = Repo.update_all(q, [])
478 follower = maybe_update_following_count(follower)
480 {:ok, _} = update_follower_count(followed)
486 def unfollow(%User{} = follower, %User{} = followed) do
487 ap_followers = followed.follower_address
489 if following?(follower, followed) and follower.ap_id != followed.ap_id do
492 where: u.id == ^follower.id,
493 update: [pull: [following: ^ap_followers]],
497 {1, [follower]} = Repo.update_all(q, [])
499 follower = maybe_update_following_count(follower)
501 {:ok, followed} = update_follower_count(followed)
505 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
507 {:error, "Not subscribed!"}
511 @spec following?(User.t(), User.t()) :: boolean
512 def following?(%User{} = follower, %User{} = followed) do
513 Enum.member?(follower.following, followed.follower_address)
516 def locked?(%User{} = user) do
517 user.info.locked || false
521 Repo.get_by(User, id: id)
524 def get_by_ap_id(ap_id) do
525 Repo.get_by(User, ap_id: ap_id)
528 def get_all_by_ap_id(ap_ids) do
529 from(u in __MODULE__,
530 where: u.ap_id in ^ap_ids
535 def get_all_by_ids(ids) do
536 from(u in __MODULE__, where: u.id in ^ids)
540 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
541 # of the ap_id and the domain and tries to get that user
542 def get_by_guessed_nickname(ap_id) do
543 domain = URI.parse(ap_id).host
544 name = List.last(String.split(ap_id, "/"))
545 nickname = "#{name}@#{domain}"
547 get_cached_by_nickname(nickname)
550 def set_cache({:ok, user}), do: set_cache(user)
551 def set_cache({:error, err}), do: {:error, err}
553 def set_cache(%User{} = user) do
554 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
555 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
556 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
560 def update_and_set_cache(changeset) do
561 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
568 def invalidate_cache(user) do
569 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
570 Cachex.del(:user_cache, "nickname:#{user.nickname}")
571 Cachex.del(:user_cache, "user_info:#{user.id}")
574 def get_cached_by_ap_id(ap_id) do
575 key = "ap_id:#{ap_id}"
576 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
579 def get_cached_by_id(id) do
583 Cachex.fetch!(:user_cache, key, fn _ ->
587 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
588 {:commit, user.ap_id}
594 get_cached_by_ap_id(ap_id)
597 def get_cached_by_nickname(nickname) do
598 key = "nickname:#{nickname}"
600 Cachex.fetch!(:user_cache, key, fn ->
601 user_result = get_or_fetch_by_nickname(nickname)
604 {:ok, user} -> {:commit, user}
605 {:error, _error} -> {:ignore, nil}
610 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
611 restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
614 is_integer(nickname_or_id) or Pleroma.FlakeId.is_flake_id?(nickname_or_id) ->
615 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
617 restrict_to_local == false ->
618 get_cached_by_nickname(nickname_or_id)
620 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
621 get_cached_by_nickname(nickname_or_id)
628 def get_by_nickname(nickname) do
629 Repo.get_by(User, nickname: nickname) ||
630 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
631 Repo.get_by(User, nickname: local_nickname(nickname))
635 def get_by_email(email), do: Repo.get_by(User, email: email)
637 def get_by_nickname_or_email(nickname_or_email) do
638 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
641 def get_cached_user_info(user) do
642 key = "user_info:#{user.id}"
643 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
646 def fetch_by_nickname(nickname) do
647 ap_try = ActivityPub.make_user_from_nickname(nickname)
650 {:ok, user} -> {:ok, user}
651 _ -> OStatus.make_user(nickname)
655 def get_or_fetch_by_nickname(nickname) do
656 with %User{} = user <- get_by_nickname(nickname) do
660 with [_nick, _domain] <- String.split(nickname, "@"),
661 {:ok, user} <- fetch_by_nickname(nickname) do
662 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
663 fetch_initial_posts(user)
668 _e -> {:error, "not found " <> nickname}
673 @doc "Fetch some posts when the user has just been federated with"
674 def fetch_initial_posts(user) do
675 BackgroundWorker.enqueue("fetch_initial_posts", %{"user_id" => user.id})
678 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
679 def get_followers_query(%User{} = user, nil) do
680 User.Query.build(%{followers: user, deactivated: false})
683 def get_followers_query(user, page) do
684 from(u in get_followers_query(user, nil))
685 |> User.Query.paginate(page, 20)
688 @spec get_followers_query(User.t()) :: Ecto.Query.t()
689 def get_followers_query(user), do: get_followers_query(user, nil)
691 @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
692 def get_followers(user, page \\ nil) do
693 q = get_followers_query(user, page)
698 @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
699 def get_external_followers(user, page \\ nil) do
702 |> get_followers_query(page)
703 |> User.Query.build(%{external: true})
708 def get_followers_ids(user, page \\ nil) do
709 q = get_followers_query(user, page)
711 Repo.all(from(u in q, select: u.id))
714 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
715 def get_friends_query(%User{} = user, nil) do
716 User.Query.build(%{friends: user, deactivated: false})
719 def get_friends_query(user, page) do
720 from(u in get_friends_query(user, nil))
721 |> User.Query.paginate(page, 20)
724 @spec get_friends_query(User.t()) :: Ecto.Query.t()
725 def get_friends_query(user), do: get_friends_query(user, nil)
727 def get_friends(user, page \\ nil) do
728 q = get_friends_query(user, page)
733 def get_friends_ids(user, page \\ nil) do
734 q = get_friends_query(user, page)
736 Repo.all(from(u in q, select: u.id))
739 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
740 def get_follow_requests(%User{} = user) do
742 Activity.follow_requests_for_actor(user)
743 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
744 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
745 |> group_by([a, u], u.id)
752 def increase_note_count(%User{} = user) do
754 |> where(id: ^user.id)
759 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
766 |> Repo.update_all([])
768 {1, [user]} -> set_cache(user)
773 def decrease_note_count(%User{} = user) do
775 |> where(id: ^user.id)
780 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
787 |> Repo.update_all([])
789 {1, [user]} -> set_cache(user)
794 def update_note_count(%User{} = user) do
798 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
802 note_count = Repo.one(note_count_query)
804 info_cng = User.Info.set_note_count(user.info, note_count)
808 |> put_embed(:info, info_cng)
809 |> update_and_set_cache()
812 def update_mascot(user, url) do
814 User.Info.mascot_update(
821 |> put_embed(:info, info_changeset)
822 |> update_and_set_cache()
825 @spec maybe_fetch_follow_information(User.t()) :: User.t()
826 def maybe_fetch_follow_information(user) do
827 with {:ok, user} <- fetch_follow_information(user) do
831 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
837 def fetch_follow_information(user) do
838 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
839 info_cng = User.Info.follow_information_update(user.info, info)
844 |> put_embed(:info, info_cng)
846 update_and_set_cache(changeset)
853 def update_follower_count(%User{} = user) do
854 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
855 follower_count_query =
856 User.Query.build(%{followers: user, deactivated: false})
857 |> select([u], %{count: count(u.id)})
860 |> where(id: ^user.id)
861 |> join(:inner, [u], s in subquery(follower_count_query))
866 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
873 |> Repo.update_all([])
875 {1, [user]} -> set_cache(user)
879 {:ok, maybe_fetch_follow_information(user)}
883 @spec maybe_update_following_count(User.t()) :: User.t()
884 def maybe_update_following_count(%User{local: false} = user) do
885 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
886 maybe_fetch_follow_information(user)
892 def maybe_update_following_count(user), do: user
894 def remove_duplicated_following(%User{following: following} = user) do
895 uniq_following = Enum.uniq(following)
897 if length(following) == length(uniq_following) do
901 |> update_changeset(%{following: uniq_following})
902 |> update_and_set_cache()
906 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
907 def get_users_from_set(ap_ids, local_only \\ true) do
908 criteria = %{ap_id: ap_ids, deactivated: false}
909 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
911 User.Query.build(criteria)
915 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
916 def get_recipients_from_activity(%Activity{recipients: to}) do
917 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
921 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
922 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
926 User.Info.add_to_mutes(info, ap_id)
927 |> User.Info.add_to_muted_notifications(info, ap_id, notifications?)
931 |> put_embed(:info, info_cng)
933 update_and_set_cache(cng)
936 def unmute(muter, %{ap_id: ap_id}) do
940 User.Info.remove_from_mutes(info, ap_id)
941 |> User.Info.remove_from_muted_notifications(info, ap_id)
945 |> put_embed(:info, info_cng)
947 update_and_set_cache(cng)
950 def subscribe(subscriber, %{ap_id: ap_id}) do
951 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
953 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
954 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
957 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
961 |> User.Info.add_to_subscribers(subscriber.ap_id)
964 |> put_embed(:info, info_cng)
965 |> update_and_set_cache()
970 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
971 with %User{} = user <- get_cached_by_ap_id(ap_id) do
972 info_cng = User.Info.remove_from_subscribers(user.info, unsubscriber.ap_id)
975 |> put_embed(:info, info_cng)
976 |> update_and_set_cache()
980 def block(blocker, %User{ap_id: ap_id} = blocked) do
981 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
983 if following?(blocker, blocked) do
984 {:ok, blocker, _} = unfollow(blocker, blocked)
990 # clear any requested follows as well
992 case CommonAPI.reject_follow_request(blocked, blocker) do
993 {:ok, %User{} = updated_blocked} -> updated_blocked
998 if subscribed_to?(blocked, blocker) do
999 {:ok, blocker} = unsubscribe(blocked, blocker)
1005 if following?(blocked, blocker) do
1006 unfollow(blocked, blocker)
1009 {:ok, blocker} = update_follower_count(blocker)
1013 |> User.Info.add_to_block(ap_id)
1017 |> put_embed(:info, info_cng)
1019 update_and_set_cache(cng)
1022 # helper to handle the block given only an actor's AP id
1023 def block(blocker, %{ap_id: ap_id}) do
1024 block(blocker, get_cached_by_ap_id(ap_id))
1027 def unblock(blocker, %{ap_id: ap_id}) do
1030 |> User.Info.remove_from_block(ap_id)
1034 |> put_embed(:info, info_cng)
1036 update_and_set_cache(cng)
1039 def mutes?(nil, _), do: false
1040 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
1042 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
1043 def muted_notifications?(nil, _), do: false
1045 def muted_notifications?(user, %{ap_id: ap_id}),
1046 do: Enum.member?(user.info.muted_notifications, ap_id)
1048 def blocks?(%User{} = user, %User{} = target) do
1049 blocks_ap_id?(user, target) || blocks_domain?(user, target)
1052 def blocks?(nil, _), do: false
1054 def blocks_ap_id?(%User{} = user, %User{} = target) do
1055 Enum.member?(user.info.blocks, target.ap_id)
1058 def blocks_ap_id?(_, _), do: false
1060 def blocks_domain?(%User{} = user, %User{} = target) do
1061 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
1062 %{host: host} = URI.parse(target.ap_id)
1063 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1066 def blocks_domain?(_, _), do: false
1068 def subscribed_to?(user, %{ap_id: ap_id}) do
1069 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1070 Enum.member?(target.info.subscribers, user.ap_id)
1074 @spec muted_users(User.t()) :: [User.t()]
1075 def muted_users(user) do
1076 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
1080 @spec blocked_users(User.t()) :: [User.t()]
1081 def blocked_users(user) do
1082 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
1086 @spec subscribers(User.t()) :: [User.t()]
1087 def subscribers(user) do
1088 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
1092 def block_domain(user, domain) do
1095 |> User.Info.add_to_domain_block(domain)
1099 |> put_embed(:info, info_cng)
1101 update_and_set_cache(cng)
1104 def unblock_domain(user, domain) do
1107 |> User.Info.remove_from_domain_block(domain)
1111 |> put_embed(:info, info_cng)
1113 update_and_set_cache(cng)
1116 def deactivate_async(user, status \\ true) do
1117 BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
1120 def deactivate(%User{} = user, status \\ true) do
1121 info_cng = User.Info.set_activation_status(user.info, status)
1123 with {:ok, friends} <- User.get_friends(user),
1124 {:ok, followers} <- User.get_followers(user),
1128 |> put_embed(:info, info_cng)
1129 |> update_and_set_cache() do
1130 Enum.each(followers, &invalidate_cache(&1))
1131 Enum.each(friends, &update_follower_count(&1))
1137 def update_notification_settings(%User{} = user, settings \\ %{}) do
1138 info_changeset = User.Info.update_notification_settings(user.info, settings)
1141 |> put_embed(:info, info_changeset)
1142 |> update_and_set_cache()
1145 def delete(%User{} = user) do
1146 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1149 def perform(:force_password_reset, user), do: force_password_reset(user)
1151 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1152 def perform(:delete, %User{} = user) do
1153 {:ok, _user} = ActivityPub.delete(user)
1155 # Remove all relationships
1156 {:ok, followers} = User.get_followers(user)
1158 Enum.each(followers, fn follower ->
1159 ActivityPub.unfollow(follower, user)
1160 User.unfollow(follower, user)
1163 {:ok, friends} = User.get_friends(user)
1165 Enum.each(friends, fn followed ->
1166 ActivityPub.unfollow(user, followed)
1167 User.unfollow(user, followed)
1170 delete_user_activities(user)
1171 invalidate_cache(user)
1175 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1176 def perform(:fetch_initial_posts, %User{} = user) do
1177 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1180 # Insert all the posts in reverse order, so they're in the right order on the timeline
1181 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
1182 &Pleroma.Web.Federator.incoming_ap_doc/1
1188 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1190 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1191 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1192 when is_list(blocked_identifiers) do
1194 blocked_identifiers,
1195 fn blocked_identifier ->
1196 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1197 {:ok, blocker} <- block(blocker, blocked),
1198 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1202 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1209 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1210 def perform(:follow_import, %User{} = follower, followed_identifiers)
1211 when is_list(followed_identifiers) do
1213 followed_identifiers,
1214 fn followed_identifier ->
1215 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1216 {:ok, follower} <- maybe_direct_follow(follower, followed),
1217 {:ok, _} <- ActivityPub.follow(follower, followed) do
1221 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1228 @spec external_users_query() :: Ecto.Query.t()
1229 def external_users_query do
1237 @spec external_users(keyword()) :: [User.t()]
1238 def external_users(opts \\ []) do
1240 external_users_query()
1241 |> select([u], struct(u, [:id, :ap_id, :info]))
1245 do: where(query, [u], u.id > ^opts[:max_id]),
1250 do: limit(query, ^opts[:limit]),
1256 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1257 BackgroundWorker.enqueue("blocks_import", %{
1258 "blocker_id" => blocker.id,
1259 "blocked_identifiers" => blocked_identifiers
1263 def follow_import(%User{} = follower, followed_identifiers)
1264 when is_list(followed_identifiers) do
1265 BackgroundWorker.enqueue("follow_import", %{
1266 "follower_id" => follower.id,
1267 "followed_identifiers" => followed_identifiers
1271 def delete_user_activities(%User{ap_id: ap_id} = user) do
1273 |> Activity.Queries.by_actor()
1274 |> RepoStreamer.chunk_stream(50)
1275 |> Stream.each(fn activities ->
1276 Enum.each(activities, &delete_activity(&1))
1283 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1285 |> Object.normalize()
1286 |> ActivityPub.delete()
1289 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1290 user = get_cached_by_ap_id(activity.actor)
1291 object = Object.normalize(activity)
1293 ActivityPub.unlike(user, object)
1296 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1297 user = get_cached_by_ap_id(activity.actor)
1298 object = Object.normalize(activity)
1300 ActivityPub.unannounce(user, object)
1303 defp delete_activity(_activity), do: "Doing nothing"
1305 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1306 Pleroma.HTML.Scrubber.TwitterText
1309 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1311 def fetch_by_ap_id(ap_id) do
1312 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1319 case OStatus.make_user(ap_id) do
1320 {:ok, user} -> {:ok, user}
1321 _ -> {:error, "Could not fetch by AP id"}
1326 def get_or_fetch_by_ap_id(ap_id) do
1327 user = get_cached_by_ap_id(ap_id)
1329 if !is_nil(user) and !User.needs_update?(user) do
1332 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1333 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1335 resp = fetch_by_ap_id(ap_id)
1337 if should_fetch_initial do
1338 with {:ok, %User{} = user} <- resp do
1339 fetch_initial_posts(user)
1347 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1348 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1349 if user = get_cached_by_ap_id(uri) do
1353 %User{info: %User.Info{}}
1354 |> cast(%{}, [:ap_id, :nickname, :local])
1355 |> put_change(:ap_id, uri)
1356 |> put_change(:nickname, nickname)
1357 |> put_change(:local, true)
1358 |> put_change(:follower_address, uri <> "/followers")
1360 {:ok, user} = Repo.insert(changes)
1366 def public_key_from_info(%{
1367 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1371 |> :public_key.pem_decode()
1373 |> :public_key.pem_entry_decode()
1379 def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
1380 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1383 def public_key_from_info(_), do: {:error, "not found key"}
1385 def get_public_key_for_ap_id(ap_id) do
1386 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1387 {:ok, public_key} <- public_key_from_info(user.info) do
1394 defp blank?(""), do: nil
1395 defp blank?(n), do: n
1397 def insert_or_update_user(data) do
1399 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1400 |> remote_user_creation()
1401 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1405 def ap_enabled?(%User{local: true}), do: true
1406 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1407 def ap_enabled?(_), do: false
1409 @doc "Gets or fetch a user by uri or nickname."
1410 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1411 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1412 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1414 # wait a period of time and return newest version of the User structs
1415 # this is because we have synchronous follow APIs and need to simulate them
1416 # with an async handshake
1417 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1418 with %User{} = a <- User.get_cached_by_id(a.id),
1419 %User{} = b <- User.get_cached_by_id(b.id) do
1427 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1428 with :ok <- :timer.sleep(timeout),
1429 %User{} = a <- User.get_cached_by_id(a.id),
1430 %User{} = b <- User.get_cached_by_id(b.id) do
1438 def parse_bio(bio) when is_binary(bio) and bio != "" do
1440 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1444 def parse_bio(_), do: ""
1446 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1447 # TODO: get profile URLs other than user.ap_id
1448 profile_urls = [user.ap_id]
1451 |> CommonUtils.format_input("text/plain",
1452 mentions_format: :full,
1453 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1458 def parse_bio(_, _), do: ""
1460 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1461 Repo.transaction(fn ->
1462 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1466 def tag(nickname, tags) when is_binary(nickname),
1467 do: tag(get_by_nickname(nickname), tags)
1469 def tag(%User{} = user, tags),
1470 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1472 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1473 Repo.transaction(fn ->
1474 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1478 def untag(nickname, tags) when is_binary(nickname),
1479 do: untag(get_by_nickname(nickname), tags)
1481 def untag(%User{} = user, tags),
1482 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1484 defp update_tags(%User{} = user, new_tags) do
1485 {:ok, updated_user} =
1487 |> change(%{tags: new_tags})
1488 |> update_and_set_cache()
1493 defp normalize_tags(tags) do
1496 |> Enum.map(&String.downcase(&1))
1499 defp local_nickname_regex do
1500 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1501 @extended_local_nickname_regex
1503 @strict_local_nickname_regex
1507 def local_nickname(nickname_or_mention) do
1510 |> String.split("@")
1514 def full_nickname(nickname_or_mention),
1515 do: String.trim_leading(nickname_or_mention, "@")
1517 def error_user(ap_id) do
1522 nickname: "erroruser@example.com",
1523 inserted_at: NaiveDateTime.utc_now()
1527 @spec all_superusers() :: [User.t()]
1528 def all_superusers do
1529 User.Query.build(%{super_users: true, local: true, deactivated: false})
1533 def showing_reblogs?(%User{} = user, %User{} = target) do
1534 target.ap_id not in user.info.muted_reblogs
1538 The function returns a query to get users with no activity for given interval of days.
1539 Inactive users are those who didn't read any notification, or had any activity where
1540 the user is the activity's actor, during `inactivity_threshold` days.
1541 Deactivated users will not appear in this list.
1545 iex> Pleroma.User.list_inactive_users()
1548 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1549 def list_inactive_users_query(inactivity_threshold \\ 7) do
1550 negative_inactivity_threshold = -inactivity_threshold
1551 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1552 # Subqueries are not supported in `where` clauses, join gets too complicated.
1553 has_read_notifications =
1554 from(n in Pleroma.Notification,
1555 where: n.seen == true,
1557 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1560 |> Pleroma.Repo.all()
1562 from(u in Pleroma.User,
1563 left_join: a in Pleroma.Activity,
1564 on: u.ap_id == a.actor,
1565 where: not is_nil(u.nickname),
1566 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1567 where: u.id not in ^has_read_notifications,
1570 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1571 is_nil(max(a.inserted_at))
1576 Enable or disable email notifications for user
1580 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1581 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1583 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1584 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1586 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1587 {:ok, t()} | {:error, Ecto.Changeset.t()}
1588 def switch_email_notifications(user, type, status) do
1589 info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})
1592 |> put_embed(:info, info)
1593 |> update_and_set_cache()
1597 Set `last_digest_emailed_at` value for the user to current time
1599 @spec touch_last_digest_emailed_at(t()) :: t()
1600 def touch_last_digest_emailed_at(user) do
1601 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1603 {:ok, updated_user} =
1605 |> change(%{last_digest_emailed_at: now})
1606 |> update_and_set_cache()
1611 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1612 def toggle_confirmation(%User{} = user) do
1613 need_confirmation? = !user.info.confirmation_pending
1616 User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
1620 |> put_embed(:info, info_changeset)
1621 |> update_and_set_cache()
1624 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1628 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1629 # use instance-default
1630 config = Pleroma.Config.get([:assets, :mascots])
1631 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1632 mascot = Keyword.get(config, default_mascot)
1635 "id" => "default-mascot",
1636 "url" => mascot[:url],
1637 "preview_url" => mascot[:url],
1639 "mime_type" => mascot[:mime_type]
1644 def ensure_keys_present(%User{info: info} = user) do
1648 {:ok, pem} = Keys.generate_rsa_pem()
1651 |> Ecto.Changeset.change()
1652 |> Ecto.Changeset.put_embed(:info, User.Info.set_keys(info, pem))
1653 |> update_and_set_cache()
1657 def get_ap_ids_by_nicknames(nicknames) do
1659 where: u.nickname in ^nicknames,
1665 defdelegate search(query, opts \\ []), to: User.Search
1667 defp put_password_hash(
1668 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1670 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1673 defp put_password_hash(changeset), do: changeset
1675 def is_internal_user?(%User{nickname: nil}), do: true
1676 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1677 def is_internal_user?(_), do: false
1679 # A hack because user delete activities have a fake id for whatever reason
1680 # TODO: Get rid of this
1681 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
1683 def get_delivered_users_by_object_id(object_id) do
1685 inner_join: delivery in assoc(u, :deliveries),
1686 where: delivery.object_id == ^object_id
1691 def change_email(user, email) do
1693 |> cast(%{email: email}, [:email])
1694 |> validate_required([:email])
1695 |> unique_constraint(:email)
1696 |> validate_format(:email, @email_regex)
1697 |> update_and_set_cache()