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 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
536 # of the ap_id and the domain and tries to get that user
537 def get_by_guessed_nickname(ap_id) do
538 domain = URI.parse(ap_id).host
539 name = List.last(String.split(ap_id, "/"))
540 nickname = "#{name}@#{domain}"
542 get_cached_by_nickname(nickname)
545 def set_cache({:ok, user}), do: set_cache(user)
546 def set_cache({:error, err}), do: {:error, err}
548 def set_cache(%User{} = user) do
549 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
550 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
551 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
555 def update_and_set_cache(changeset) do
556 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
563 def invalidate_cache(user) do
564 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
565 Cachex.del(:user_cache, "nickname:#{user.nickname}")
566 Cachex.del(:user_cache, "user_info:#{user.id}")
569 def get_cached_by_ap_id(ap_id) do
570 key = "ap_id:#{ap_id}"
571 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
574 def get_cached_by_id(id) do
578 Cachex.fetch!(:user_cache, key, fn _ ->
582 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
583 {:commit, user.ap_id}
589 get_cached_by_ap_id(ap_id)
592 def get_cached_by_nickname(nickname) do
593 key = "nickname:#{nickname}"
595 Cachex.fetch!(:user_cache, key, fn ->
596 user_result = get_or_fetch_by_nickname(nickname)
599 {:ok, user} -> {:commit, user}
600 {:error, _error} -> {:ignore, nil}
605 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
606 restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
609 is_integer(nickname_or_id) or Pleroma.FlakeId.is_flake_id?(nickname_or_id) ->
610 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
612 restrict_to_local == false ->
613 get_cached_by_nickname(nickname_or_id)
615 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
616 get_cached_by_nickname(nickname_or_id)
623 def get_by_nickname(nickname) do
624 Repo.get_by(User, nickname: nickname) ||
625 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
626 Repo.get_by(User, nickname: local_nickname(nickname))
630 def get_by_email(email), do: Repo.get_by(User, email: email)
632 def get_by_nickname_or_email(nickname_or_email) do
633 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
636 def get_cached_user_info(user) do
637 key = "user_info:#{user.id}"
638 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
641 def fetch_by_nickname(nickname) do
642 ap_try = ActivityPub.make_user_from_nickname(nickname)
645 {:ok, user} -> {:ok, user}
646 _ -> OStatus.make_user(nickname)
650 def get_or_fetch_by_nickname(nickname) do
651 with %User{} = user <- get_by_nickname(nickname) do
655 with [_nick, _domain] <- String.split(nickname, "@"),
656 {:ok, user} <- fetch_by_nickname(nickname) do
657 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
658 fetch_initial_posts(user)
663 _e -> {:error, "not found " <> nickname}
668 @doc "Fetch some posts when the user has just been federated with"
669 def fetch_initial_posts(user) do
670 BackgroundWorker.enqueue("fetch_initial_posts", %{"user_id" => user.id})
673 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
674 def get_followers_query(%User{} = user, nil) do
675 User.Query.build(%{followers: user, deactivated: false})
678 def get_followers_query(user, page) do
679 from(u in get_followers_query(user, nil))
680 |> User.Query.paginate(page, 20)
683 @spec get_followers_query(User.t()) :: Ecto.Query.t()
684 def get_followers_query(user), do: get_followers_query(user, nil)
686 @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
687 def get_followers(user, page \\ nil) do
688 q = get_followers_query(user, page)
693 @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
694 def get_external_followers(user, page \\ nil) do
697 |> get_followers_query(page)
698 |> User.Query.build(%{external: true})
703 def get_followers_ids(user, page \\ nil) do
704 q = get_followers_query(user, page)
706 Repo.all(from(u in q, select: u.id))
709 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
710 def get_friends_query(%User{} = user, nil) do
711 User.Query.build(%{friends: user, deactivated: false})
714 def get_friends_query(user, page) do
715 from(u in get_friends_query(user, nil))
716 |> User.Query.paginate(page, 20)
719 @spec get_friends_query(User.t()) :: Ecto.Query.t()
720 def get_friends_query(user), do: get_friends_query(user, nil)
722 def get_friends(user, page \\ nil) do
723 q = get_friends_query(user, page)
728 def get_friends_ids(user, page \\ nil) do
729 q = get_friends_query(user, page)
731 Repo.all(from(u in q, select: u.id))
734 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
735 def get_follow_requests(%User{} = user) do
737 Activity.follow_requests_for_actor(user)
738 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
739 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
740 |> group_by([a, u], u.id)
747 def increase_note_count(%User{} = user) do
749 |> where(id: ^user.id)
754 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
761 |> Repo.update_all([])
763 {1, [user]} -> set_cache(user)
768 def decrease_note_count(%User{} = user) do
770 |> where(id: ^user.id)
775 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
782 |> Repo.update_all([])
784 {1, [user]} -> set_cache(user)
789 def update_note_count(%User{} = user) do
793 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
797 note_count = Repo.one(note_count_query)
799 info_cng = User.Info.set_note_count(user.info, note_count)
803 |> put_embed(:info, info_cng)
804 |> update_and_set_cache()
807 @spec maybe_fetch_follow_information(User.t()) :: User.t()
808 def maybe_fetch_follow_information(user) do
809 with {:ok, user} <- fetch_follow_information(user) do
813 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
819 def fetch_follow_information(user) do
820 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
821 info_cng = User.Info.follow_information_update(user.info, info)
826 |> put_embed(:info, info_cng)
828 update_and_set_cache(changeset)
835 def update_follower_count(%User{} = user) do
836 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
837 follower_count_query =
838 User.Query.build(%{followers: user, deactivated: false})
839 |> select([u], %{count: count(u.id)})
842 |> where(id: ^user.id)
843 |> join(:inner, [u], s in subquery(follower_count_query))
848 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
855 |> Repo.update_all([])
857 {1, [user]} -> set_cache(user)
861 {:ok, maybe_fetch_follow_information(user)}
865 @spec maybe_update_following_count(User.t()) :: User.t()
866 def maybe_update_following_count(%User{local: false} = user) do
867 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
868 maybe_fetch_follow_information(user)
874 def maybe_update_following_count(user), do: user
876 def remove_duplicated_following(%User{following: following} = user) do
877 uniq_following = Enum.uniq(following)
879 if length(following) == length(uniq_following) do
883 |> update_changeset(%{following: uniq_following})
884 |> update_and_set_cache()
888 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
889 def get_users_from_set(ap_ids, local_only \\ true) do
890 criteria = %{ap_id: ap_ids, deactivated: false}
891 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
893 User.Query.build(criteria)
897 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
898 def get_recipients_from_activity(%Activity{recipients: to}) do
899 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
903 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
904 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
908 User.Info.add_to_mutes(info, ap_id)
909 |> User.Info.add_to_muted_notifications(info, ap_id, notifications?)
913 |> put_embed(:info, info_cng)
915 update_and_set_cache(cng)
918 def unmute(muter, %{ap_id: ap_id}) do
922 User.Info.remove_from_mutes(info, ap_id)
923 |> User.Info.remove_from_muted_notifications(info, ap_id)
927 |> put_embed(:info, info_cng)
929 update_and_set_cache(cng)
932 def subscribe(subscriber, %{ap_id: ap_id}) do
933 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
935 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
936 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
939 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
943 |> User.Info.add_to_subscribers(subscriber.ap_id)
946 |> put_embed(:info, info_cng)
947 |> update_and_set_cache()
952 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
953 with %User{} = user <- get_cached_by_ap_id(ap_id) do
956 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
959 |> put_embed(:info, info_cng)
960 |> update_and_set_cache()
964 def block(blocker, %User{ap_id: ap_id} = blocked) do
965 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
967 if following?(blocker, blocked) do
968 {:ok, blocker, _} = unfollow(blocker, blocked)
974 # clear any requested follows as well
976 case CommonAPI.reject_follow_request(blocked, blocker) do
977 {:ok, %User{} = updated_blocked} -> updated_blocked
982 if subscribed_to?(blocked, blocker) do
983 {:ok, blocker} = unsubscribe(blocked, blocker)
989 if following?(blocked, blocker) do
990 unfollow(blocked, blocker)
993 {:ok, blocker} = update_follower_count(blocker)
997 |> User.Info.add_to_block(ap_id)
1001 |> put_embed(:info, info_cng)
1003 update_and_set_cache(cng)
1006 # helper to handle the block given only an actor's AP id
1007 def block(blocker, %{ap_id: ap_id}) do
1008 block(blocker, get_cached_by_ap_id(ap_id))
1011 def unblock(blocker, %{ap_id: ap_id}) do
1014 |> User.Info.remove_from_block(ap_id)
1018 |> put_embed(:info, info_cng)
1020 update_and_set_cache(cng)
1023 def mutes?(nil, _), do: false
1024 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
1026 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
1027 def muted_notifications?(nil, _), do: false
1029 def muted_notifications?(user, %{ap_id: ap_id}),
1030 do: Enum.member?(user.info.muted_notifications, ap_id)
1032 def blocks?(%User{} = user, %User{} = target) do
1033 blocks_ap_id?(user, target) || blocks_domain?(user, target)
1036 def blocks?(nil, _), do: false
1038 def blocks_ap_id?(%User{} = user, %User{} = target) do
1039 Enum.member?(user.info.blocks, target.ap_id)
1042 def blocks_ap_id?(_, _), do: false
1044 def blocks_domain?(%User{} = user, %User{} = target) do
1045 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
1046 %{host: host} = URI.parse(target.ap_id)
1047 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1050 def blocks_domain?(_, _), do: false
1052 def subscribed_to?(user, %{ap_id: ap_id}) do
1053 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1054 Enum.member?(target.info.subscribers, user.ap_id)
1058 @spec muted_users(User.t()) :: [User.t()]
1059 def muted_users(user) do
1060 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
1064 @spec blocked_users(User.t()) :: [User.t()]
1065 def blocked_users(user) do
1066 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
1070 @spec subscribers(User.t()) :: [User.t()]
1071 def subscribers(user) do
1072 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
1076 def block_domain(user, domain) do
1079 |> User.Info.add_to_domain_block(domain)
1083 |> put_embed(:info, info_cng)
1085 update_and_set_cache(cng)
1088 def unblock_domain(user, domain) do
1091 |> User.Info.remove_from_domain_block(domain)
1095 |> put_embed(:info, info_cng)
1097 update_and_set_cache(cng)
1100 def deactivate_async(user, status \\ true) do
1101 BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
1104 def deactivate(%User{} = user, status \\ true) do
1105 info_cng = User.Info.set_activation_status(user.info, status)
1107 with {:ok, friends} <- User.get_friends(user),
1108 {:ok, followers} <- User.get_followers(user),
1112 |> put_embed(:info, info_cng)
1113 |> update_and_set_cache() do
1114 Enum.each(followers, &invalidate_cache(&1))
1115 Enum.each(friends, &update_follower_count(&1))
1121 def update_notification_settings(%User{} = user, settings \\ %{}) do
1122 info_changeset = User.Info.update_notification_settings(user.info, settings)
1125 |> put_embed(:info, info_changeset)
1126 |> update_and_set_cache()
1129 def delete(%User{} = user) do
1130 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1133 def perform(:force_password_reset, user), do: force_password_reset(user)
1135 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1136 def perform(:delete, %User{} = user) do
1137 {:ok, _user} = ActivityPub.delete(user)
1139 # Remove all relationships
1140 {:ok, followers} = User.get_followers(user)
1142 Enum.each(followers, fn follower ->
1143 ActivityPub.unfollow(follower, user)
1144 User.unfollow(follower, user)
1147 {:ok, friends} = User.get_friends(user)
1149 Enum.each(friends, fn followed ->
1150 ActivityPub.unfollow(user, followed)
1151 User.unfollow(user, followed)
1154 delete_user_activities(user)
1155 invalidate_cache(user)
1159 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1160 def perform(:fetch_initial_posts, %User{} = user) do
1161 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1164 # Insert all the posts in reverse order, so they're in the right order on the timeline
1165 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
1166 &Pleroma.Web.Federator.incoming_ap_doc/1
1172 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1174 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1175 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1176 when is_list(blocked_identifiers) do
1178 blocked_identifiers,
1179 fn blocked_identifier ->
1180 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1181 {:ok, blocker} <- block(blocker, blocked),
1182 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1186 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1193 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1194 def perform(:follow_import, %User{} = follower, followed_identifiers)
1195 when is_list(followed_identifiers) do
1197 followed_identifiers,
1198 fn followed_identifier ->
1199 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1200 {:ok, follower} <- maybe_direct_follow(follower, followed),
1201 {:ok, _} <- ActivityPub.follow(follower, followed) do
1205 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1212 @spec external_users_query() :: Ecto.Query.t()
1213 def external_users_query do
1221 @spec external_users(keyword()) :: [User.t()]
1222 def external_users(opts \\ []) do
1224 external_users_query()
1225 |> select([u], struct(u, [:id, :ap_id, :info]))
1229 do: where(query, [u], u.id > ^opts[:max_id]),
1234 do: limit(query, ^opts[:limit]),
1240 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1241 BackgroundWorker.enqueue("blocks_import", %{
1242 "blocker_id" => blocker.id,
1243 "blocked_identifiers" => blocked_identifiers
1247 def follow_import(%User{} = follower, followed_identifiers)
1248 when is_list(followed_identifiers) do
1249 BackgroundWorker.enqueue("follow_import", %{
1250 "follower_id" => follower.id,
1251 "followed_identifiers" => followed_identifiers
1255 def delete_user_activities(%User{ap_id: ap_id} = user) do
1257 |> Activity.Queries.by_actor()
1258 |> RepoStreamer.chunk_stream(50)
1259 |> Stream.each(fn activities ->
1260 Enum.each(activities, &delete_activity(&1))
1267 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1269 |> Object.normalize()
1270 |> ActivityPub.delete()
1273 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1274 user = get_cached_by_ap_id(activity.actor)
1275 object = Object.normalize(activity)
1277 ActivityPub.unlike(user, object)
1280 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1281 user = get_cached_by_ap_id(activity.actor)
1282 object = Object.normalize(activity)
1284 ActivityPub.unannounce(user, object)
1287 defp delete_activity(_activity), do: "Doing nothing"
1289 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1290 Pleroma.HTML.Scrubber.TwitterText
1293 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1295 def fetch_by_ap_id(ap_id) do
1296 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1303 case OStatus.make_user(ap_id) do
1304 {:ok, user} -> {:ok, user}
1305 _ -> {:error, "Could not fetch by AP id"}
1310 def get_or_fetch_by_ap_id(ap_id) do
1311 user = get_cached_by_ap_id(ap_id)
1313 if !is_nil(user) and !User.needs_update?(user) do
1316 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1317 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1319 resp = fetch_by_ap_id(ap_id)
1321 if should_fetch_initial do
1322 with {:ok, %User{} = user} <- resp do
1323 fetch_initial_posts(user)
1331 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1332 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1333 if user = get_cached_by_ap_id(uri) do
1337 %User{info: %User.Info{}}
1338 |> cast(%{}, [:ap_id, :nickname, :local])
1339 |> put_change(:ap_id, uri)
1340 |> put_change(:nickname, nickname)
1341 |> put_change(:local, true)
1342 |> put_change(:follower_address, uri <> "/followers")
1344 {:ok, user} = Repo.insert(changes)
1350 def public_key_from_info(%{
1351 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1355 |> :public_key.pem_decode()
1357 |> :public_key.pem_entry_decode()
1363 def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
1364 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1367 def public_key_from_info(_), do: {:error, "not found key"}
1369 def get_public_key_for_ap_id(ap_id) do
1370 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1371 {:ok, public_key} <- public_key_from_info(user.info) do
1378 defp blank?(""), do: nil
1379 defp blank?(n), do: n
1381 def insert_or_update_user(data) do
1383 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1384 |> remote_user_creation()
1385 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1389 def ap_enabled?(%User{local: true}), do: true
1390 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1391 def ap_enabled?(_), do: false
1393 @doc "Gets or fetch a user by uri or nickname."
1394 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1395 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1396 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1398 # wait a period of time and return newest version of the User structs
1399 # this is because we have synchronous follow APIs and need to simulate them
1400 # with an async handshake
1401 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1402 with %User{} = a <- User.get_cached_by_id(a.id),
1403 %User{} = b <- User.get_cached_by_id(b.id) do
1411 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1412 with :ok <- :timer.sleep(timeout),
1413 %User{} = a <- User.get_cached_by_id(a.id),
1414 %User{} = b <- User.get_cached_by_id(b.id) do
1422 def parse_bio(bio) when is_binary(bio) and bio != "" do
1424 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1428 def parse_bio(_), do: ""
1430 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1431 # TODO: get profile URLs other than user.ap_id
1432 profile_urls = [user.ap_id]
1435 |> CommonUtils.format_input("text/plain",
1436 mentions_format: :full,
1437 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1442 def parse_bio(_, _), do: ""
1444 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1445 Repo.transaction(fn ->
1446 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1450 def tag(nickname, tags) when is_binary(nickname),
1451 do: tag(get_by_nickname(nickname), tags)
1453 def tag(%User{} = user, tags),
1454 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1456 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1457 Repo.transaction(fn ->
1458 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1462 def untag(nickname, tags) when is_binary(nickname),
1463 do: untag(get_by_nickname(nickname), tags)
1465 def untag(%User{} = user, tags),
1466 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1468 defp update_tags(%User{} = user, new_tags) do
1469 {:ok, updated_user} =
1471 |> change(%{tags: new_tags})
1472 |> update_and_set_cache()
1477 defp normalize_tags(tags) do
1480 |> Enum.map(&String.downcase(&1))
1483 defp local_nickname_regex do
1484 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1485 @extended_local_nickname_regex
1487 @strict_local_nickname_regex
1491 def local_nickname(nickname_or_mention) do
1494 |> String.split("@")
1498 def full_nickname(nickname_or_mention),
1499 do: String.trim_leading(nickname_or_mention, "@")
1501 def error_user(ap_id) do
1506 nickname: "erroruser@example.com",
1507 inserted_at: NaiveDateTime.utc_now()
1511 @spec all_superusers() :: [User.t()]
1512 def all_superusers do
1513 User.Query.build(%{super_users: true, local: true, deactivated: false})
1517 def showing_reblogs?(%User{} = user, %User{} = target) do
1518 target.ap_id not in user.info.muted_reblogs
1522 The function returns a query to get users with no activity for given interval of days.
1523 Inactive users are those who didn't read any notification, or had any activity where
1524 the user is the activity's actor, during `inactivity_threshold` days.
1525 Deactivated users will not appear in this list.
1529 iex> Pleroma.User.list_inactive_users()
1532 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1533 def list_inactive_users_query(inactivity_threshold \\ 7) do
1534 negative_inactivity_threshold = -inactivity_threshold
1535 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1536 # Subqueries are not supported in `where` clauses, join gets too complicated.
1537 has_read_notifications =
1538 from(n in Pleroma.Notification,
1539 where: n.seen == true,
1541 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1544 |> Pleroma.Repo.all()
1546 from(u in Pleroma.User,
1547 left_join: a in Pleroma.Activity,
1548 on: u.ap_id == a.actor,
1549 where: not is_nil(u.nickname),
1550 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1551 where: u.id not in ^has_read_notifications,
1554 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1555 is_nil(max(a.inserted_at))
1560 Enable or disable email notifications for user
1564 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1565 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1567 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1568 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1570 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1571 {:ok, t()} | {:error, Ecto.Changeset.t()}
1572 def switch_email_notifications(user, type, status) do
1573 info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})
1576 |> put_embed(:info, info)
1577 |> update_and_set_cache()
1581 Set `last_digest_emailed_at` value for the user to current time
1583 @spec touch_last_digest_emailed_at(t()) :: t()
1584 def touch_last_digest_emailed_at(user) do
1585 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1587 {:ok, updated_user} =
1589 |> change(%{last_digest_emailed_at: now})
1590 |> update_and_set_cache()
1595 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1596 def toggle_confirmation(%User{} = user) do
1597 need_confirmation? = !user.info.confirmation_pending
1600 User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
1604 |> put_embed(:info, info_changeset)
1605 |> update_and_set_cache()
1608 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1612 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1613 # use instance-default
1614 config = Pleroma.Config.get([:assets, :mascots])
1615 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1616 mascot = Keyword.get(config, default_mascot)
1619 "id" => "default-mascot",
1620 "url" => mascot[:url],
1621 "preview_url" => mascot[:url],
1623 "mime_type" => mascot[:mime_type]
1628 def ensure_keys_present(%User{info: info} = user) do
1632 {:ok, pem} = Keys.generate_rsa_pem()
1635 |> Ecto.Changeset.change()
1636 |> Ecto.Changeset.put_embed(:info, User.Info.set_keys(info, pem))
1637 |> update_and_set_cache()
1641 def get_ap_ids_by_nicknames(nicknames) do
1643 where: u.nickname in ^nicknames,
1649 defdelegate search(query, opts \\ []), to: User.Search
1651 defp put_password_hash(
1652 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1654 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1657 defp put_password_hash(changeset), do: changeset
1659 def is_internal_user?(%User{nickname: nil}), do: true
1660 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1661 def is_internal_user?(_), do: false
1663 # A hack because user delete activities have a fake id for whatever reason
1664 # TODO: Get rid of this
1665 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
1667 def get_delivered_users_by_object_id(object_id) do
1669 inner_join: delivery in assoc(u, :deliveries),
1670 where: delivery.object_id == ^object_id
1675 def change_email(user, email) do
1677 |> cast(%{email: email}, [:email])
1678 |> validate_required([:email])
1679 |> unique_constraint(:email)
1680 |> validate_format(:email, @email_regex)
1681 |> update_and_set_cache()