1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.User do
13 alias Pleroma.Activity
15 alias Pleroma.Notification
17 alias Pleroma.Registration
19 alias Pleroma.RepoStreamer
22 alias Pleroma.Web.ActivityPub.ActivityPub
23 alias Pleroma.Web.ActivityPub.Utils
24 alias Pleroma.Web.CommonAPI
25 alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
26 alias Pleroma.Web.OAuth
27 alias Pleroma.Web.OStatus
28 alias Pleroma.Web.RelMe
29 alias Pleroma.Web.Websub
33 @type t :: %__MODULE__{}
35 @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
37 # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
38 @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])?)*$/
40 @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
41 @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
45 field(:email, :string)
47 field(:nickname, :string)
48 field(:password_hash, :string)
49 field(:password, :string, virtual: true)
50 field(:password_confirmation, :string, virtual: true)
52 field(:following, {:array, :string}, default: [])
53 field(:ap_id, :string)
55 field(:local, :boolean, default: true)
56 field(:follower_address, :string)
57 field(:following_address, :string)
58 field(:search_rank, :float, virtual: true)
59 field(:search_type, :integer, virtual: true)
60 field(:tags, {:array, :string}, default: [])
61 field(:last_refreshed_at, :naive_datetime_usec)
62 field(:last_digest_emailed_at, :naive_datetime)
63 has_many(:notifications, Notification)
64 has_many(:registrations, Registration)
65 embeds_one(:info, User.Info)
70 def auth_active?(%User{info: %User.Info{confirmation_pending: true}}),
71 do: !Pleroma.Config.get([:instance, :account_activation_required])
73 def auth_active?(%User{info: %User.Info{deactivated: true}}), do: false
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 def remote_user_creation(params) do
181 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
182 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
184 params = Map.put(params, :info, params[:info] || %{})
185 info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])
189 |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
190 |> validate_required([:name, :ap_id])
191 |> unique_constraint(:nickname)
192 |> validate_format(:nickname, @email_regex)
193 |> validate_length(:bio, max: bio_limit)
194 |> validate_length(:name, max: name_limit)
195 |> put_change(:local, false)
196 |> put_embed(:info, info_cng)
199 case info_cng.changes[:source_data] do
200 %{"followers" => followers, "following" => following} ->
202 |> put_change(:follower_address, followers)
203 |> put_change(:following_address, following)
206 followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
209 |> put_change(:follower_address, followers)
216 def update_changeset(struct, params \\ %{}) do
217 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
218 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
221 |> cast(params, [:bio, :name, :avatar, :following])
222 |> unique_constraint(:nickname)
223 |> validate_format(:nickname, local_nickname_regex())
224 |> validate_length(:bio, max: bio_limit)
225 |> validate_length(:name, min: 1, max: name_limit)
228 def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
229 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
230 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
232 params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
233 info_cng = User.Info.user_upgrade(struct.info, params[:info], remote?)
244 |> unique_constraint(:nickname)
245 |> validate_format(:nickname, local_nickname_regex())
246 |> validate_length(:bio, max: bio_limit)
247 |> validate_length(:name, max: name_limit)
248 |> put_embed(:info, info_cng)
251 def password_update_changeset(struct, params) do
253 |> cast(params, [:password, :password_confirmation])
254 |> validate_required([:password, :password_confirmation])
255 |> validate_confirmation(:password)
259 @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
260 def reset_password(%User{id: user_id} = user, data) do
263 |> Multi.update(:user, password_update_changeset(user, data))
264 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
265 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
267 case Repo.transaction(multi) do
268 {:ok, %{user: user} = _} -> set_cache(user)
269 {:error, _, changeset, _} -> {:error, changeset}
273 def register_changeset(struct, params \\ %{}, opts \\ []) do
274 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
275 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
278 if is_nil(opts[:need_confirmation]) do
279 Pleroma.Config.get([:instance, :account_activation_required])
281 opts[:need_confirmation]
285 User.Info.confirmation_changeset(%User.Info{}, need_confirmation: need_confirmation?)
289 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
290 |> validate_required([:name, :nickname, :password, :password_confirmation])
291 |> validate_confirmation(:password)
292 |> unique_constraint(:email)
293 |> unique_constraint(:nickname)
294 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
295 |> validate_format(:nickname, local_nickname_regex())
296 |> validate_format(:email, @email_regex)
297 |> validate_length(:bio, max: bio_limit)
298 |> validate_length(:name, min: 1, max: name_limit)
299 |> put_change(:info, info_change)
302 if opts[:external] do
305 validate_required(changeset, [:email])
308 if changeset.valid? do
309 ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
310 followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
314 |> put_change(:ap_id, ap_id)
315 |> unique_constraint(:ap_id)
316 |> put_change(:following, [followers])
317 |> put_change(:follower_address, followers)
323 defp autofollow_users(user) do
324 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
327 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
330 follow_all(user, autofollowed_users)
333 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
334 def register(%Ecto.Changeset{} = changeset) do
335 with {:ok, user} <- Repo.insert(changeset),
336 {:ok, user} <- post_register_action(user) do
341 def post_register_action(%User{} = user) do
342 with {:ok, user} <- autofollow_users(user),
343 {:ok, user} <- set_cache(user),
344 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
345 {:ok, _} <- try_send_confirmation_email(user) do
350 def try_send_confirmation_email(%User{} = user) do
351 if user.info.confirmation_pending &&
352 Pleroma.Config.get([:instance, :account_activation_required]) do
354 |> Pleroma.Emails.UserEmail.account_confirmation_email()
355 |> Pleroma.Emails.Mailer.deliver_async()
363 def needs_update?(%User{local: true}), do: false
365 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
367 def needs_update?(%User{local: false} = user) do
368 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
371 def needs_update?(_), do: true
373 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
374 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
378 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
379 follow(follower, followed)
382 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
383 if not User.ap_enabled?(followed) do
384 follow(follower, followed)
390 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
391 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
392 def follow_all(follower, followeds) do
395 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
396 |> Enum.map(fn %{follower_address: fa} -> fa end)
400 where: u.id == ^follower.id,
405 "array(select distinct unnest (array_cat(?, ?)))",
414 {1, [follower]} = Repo.update_all(q, [])
416 Enum.each(followeds, fn followed ->
417 update_follower_count(followed)
423 def follow(%User{} = follower, %User{info: info} = followed) do
424 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
425 ap_followers = followed.follower_address
429 {:error, "Could not follow user: You are deactivated."}
431 deny_follow_blocked and blocks?(followed, follower) ->
432 {:error, "Could not follow user: #{followed.nickname} blocked you."}
435 if !followed.local && follower.local && !ap_enabled?(followed) do
436 Websub.subscribe(follower, followed)
441 where: u.id == ^follower.id,
442 update: [push: [following: ^ap_followers]],
446 {1, [follower]} = Repo.update_all(q, [])
448 follower = maybe_update_following_count(follower)
450 {:ok, _} = update_follower_count(followed)
456 def unfollow(%User{} = follower, %User{} = followed) do
457 ap_followers = followed.follower_address
459 if following?(follower, followed) and follower.ap_id != followed.ap_id do
462 where: u.id == ^follower.id,
463 update: [pull: [following: ^ap_followers]],
467 {1, [follower]} = Repo.update_all(q, [])
469 follower = maybe_update_following_count(follower)
471 {:ok, followed} = update_follower_count(followed)
475 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
477 {:error, "Not subscribed!"}
481 @spec following?(User.t(), User.t()) :: boolean
482 def following?(%User{} = follower, %User{} = followed) do
483 Enum.member?(follower.following, followed.follower_address)
486 def locked?(%User{} = user) do
487 user.info.locked || false
491 Repo.get_by(User, id: id)
494 def get_by_ap_id(ap_id) do
495 Repo.get_by(User, ap_id: ap_id)
498 def get_all_by_ap_id(ap_ids) do
499 from(u in __MODULE__,
500 where: u.ap_id in ^ap_ids
505 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
506 # of the ap_id and the domain and tries to get that user
507 def get_by_guessed_nickname(ap_id) do
508 domain = URI.parse(ap_id).host
509 name = List.last(String.split(ap_id, "/"))
510 nickname = "#{name}@#{domain}"
512 get_cached_by_nickname(nickname)
515 def set_cache({:ok, user}), do: set_cache(user)
516 def set_cache({:error, err}), do: {:error, err}
518 def set_cache(%User{} = user) do
519 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
520 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
521 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
525 def update_and_set_cache(changeset) do
526 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
533 def invalidate_cache(user) do
534 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
535 Cachex.del(:user_cache, "nickname:#{user.nickname}")
536 Cachex.del(:user_cache, "user_info:#{user.id}")
539 def get_cached_by_ap_id(ap_id) do
540 key = "ap_id:#{ap_id}"
541 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
544 def get_cached_by_id(id) do
548 Cachex.fetch!(:user_cache, key, fn _ ->
552 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
553 {:commit, user.ap_id}
559 get_cached_by_ap_id(ap_id)
562 def get_cached_by_nickname(nickname) do
563 key = "nickname:#{nickname}"
565 Cachex.fetch!(:user_cache, key, fn ->
566 user_result = get_or_fetch_by_nickname(nickname)
569 {:ok, user} -> {:commit, user}
570 {:error, _error} -> {:ignore, nil}
575 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
576 restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
579 is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
580 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
582 restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
583 get_cached_by_nickname(nickname_or_id)
585 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
586 get_cached_by_nickname(nickname_or_id)
593 def get_by_nickname(nickname) do
594 Repo.get_by(User, nickname: nickname) ||
595 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
596 Repo.get_by(User, nickname: local_nickname(nickname))
600 def get_by_email(email), do: Repo.get_by(User, email: email)
602 def get_by_nickname_or_email(nickname_or_email) do
603 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
606 def get_cached_user_info(user) do
607 key = "user_info:#{user.id}"
608 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
611 def fetch_by_nickname(nickname) do
612 ap_try = ActivityPub.make_user_from_nickname(nickname)
615 {:ok, user} -> {:ok, user}
616 _ -> OStatus.make_user(nickname)
620 def get_or_fetch_by_nickname(nickname) do
621 with %User{} = user <- get_by_nickname(nickname) do
625 with [_nick, _domain] <- String.split(nickname, "@"),
626 {:ok, user} <- fetch_by_nickname(nickname) do
627 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
628 fetch_initial_posts(user)
633 _e -> {:error, "not found " <> nickname}
638 @doc "Fetch some posts when the user has just been federated with"
639 def fetch_initial_posts(user),
640 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:fetch_initial_posts, user])
642 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
643 def get_followers_query(%User{} = user, nil) do
644 User.Query.build(%{followers: user, deactivated: false})
647 def get_followers_query(user, page) do
648 from(u in get_followers_query(user, nil))
649 |> User.Query.paginate(page, 20)
652 @spec get_followers_query(User.t()) :: Ecto.Query.t()
653 def get_followers_query(user), do: get_followers_query(user, nil)
655 @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
656 def get_followers(user, page \\ nil) do
657 q = get_followers_query(user, page)
662 @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
663 def get_external_followers(user, page \\ nil) do
666 |> get_followers_query(page)
667 |> User.Query.build(%{external: true})
672 def get_followers_ids(user, page \\ nil) do
673 q = get_followers_query(user, page)
675 Repo.all(from(u in q, select: u.id))
678 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
679 def get_friends_query(%User{} = user, nil) do
680 User.Query.build(%{friends: user, deactivated: false})
683 def get_friends_query(user, page) do
684 from(u in get_friends_query(user, nil))
685 |> User.Query.paginate(page, 20)
688 @spec get_friends_query(User.t()) :: Ecto.Query.t()
689 def get_friends_query(user), do: get_friends_query(user, nil)
691 def get_friends(user, page \\ nil) do
692 q = get_friends_query(user, page)
697 def get_friends_ids(user, page \\ nil) do
698 q = get_friends_query(user, page)
700 Repo.all(from(u in q, select: u.id))
703 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
704 def get_follow_requests(%User{} = user) do
706 Activity.follow_requests_for_actor(user)
707 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
708 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
709 |> group_by([a, u], u.id)
716 def increase_note_count(%User{} = user) do
718 |> where(id: ^user.id)
723 "safe_jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
730 |> Repo.update_all([])
732 {1, [user]} -> set_cache(user)
737 def decrease_note_count(%User{} = user) do
739 |> where(id: ^user.id)
744 "safe_jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
751 |> Repo.update_all([])
753 {1, [user]} -> set_cache(user)
758 def update_note_count(%User{} = user) do
762 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
766 note_count = Repo.one(note_count_query)
768 info_cng = User.Info.set_note_count(user.info, note_count)
772 |> put_embed(:info, info_cng)
773 |> update_and_set_cache()
776 @spec maybe_fetch_follow_information(User.t()) :: User.t()
777 def maybe_fetch_follow_information(user) do
778 with {:ok, user} <- fetch_follow_information(user) do
782 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
788 def fetch_follow_information(user) do
789 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
790 info_cng = User.Info.follow_information_update(user.info, info)
795 |> put_embed(:info, info_cng)
797 update_and_set_cache(changeset)
804 def update_follower_count(%User{} = user) do
805 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
806 follower_count_query =
807 User.Query.build(%{followers: user, deactivated: false})
808 |> select([u], %{count: count(u.id)})
811 |> where(id: ^user.id)
812 |> join(:inner, [u], s in subquery(follower_count_query))
817 "safe_jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
824 |> Repo.update_all([])
826 {1, [user]} -> set_cache(user)
830 {:ok, maybe_fetch_follow_information(user)}
834 @spec maybe_update_following_count(User.t()) :: User.t()
835 def maybe_update_following_count(%User{local: false} = user) do
836 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
837 maybe_fetch_follow_information(user)
843 def maybe_update_following_count(user), do: user
845 def remove_duplicated_following(%User{following: following} = user) do
846 uniq_following = Enum.uniq(following)
848 if length(following) == length(uniq_following) do
852 |> update_changeset(%{following: uniq_following})
853 |> update_and_set_cache()
857 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
858 def get_users_from_set(ap_ids, local_only \\ true) do
859 criteria = %{ap_id: ap_ids, deactivated: false}
860 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
862 User.Query.build(criteria)
866 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
867 def get_recipients_from_activity(%Activity{recipients: to}) do
868 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
872 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
873 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
877 User.Info.add_to_mutes(info, ap_id)
878 |> User.Info.add_to_muted_notifications(info, ap_id, notifications?)
882 |> put_embed(:info, info_cng)
884 update_and_set_cache(cng)
887 def unmute(muter, %{ap_id: ap_id}) do
891 User.Info.remove_from_mutes(info, ap_id)
892 |> User.Info.remove_from_muted_notifications(info, ap_id)
896 |> put_embed(:info, info_cng)
898 update_and_set_cache(cng)
901 def subscribe(subscriber, %{ap_id: ap_id}) do
902 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
904 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
905 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
908 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
912 |> User.Info.add_to_subscribers(subscriber.ap_id)
915 |> put_embed(:info, info_cng)
916 |> update_and_set_cache()
921 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
922 with %User{} = user <- get_cached_by_ap_id(ap_id) do
925 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
928 |> put_embed(:info, info_cng)
929 |> update_and_set_cache()
933 def block(blocker, %User{ap_id: ap_id} = blocked) do
934 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
936 if following?(blocker, blocked) do
937 {:ok, blocker, _} = unfollow(blocker, blocked)
943 # clear any requested follows as well
945 case CommonAPI.reject_follow_request(blocked, blocker) do
946 {:ok, %User{} = updated_blocked} -> updated_blocked
951 if subscribed_to?(blocked, blocker) do
952 {:ok, blocker} = unsubscribe(blocked, blocker)
958 if following?(blocked, blocker) do
959 unfollow(blocked, blocker)
962 {:ok, blocker} = update_follower_count(blocker)
966 |> User.Info.add_to_block(ap_id)
970 |> put_embed(:info, info_cng)
972 update_and_set_cache(cng)
975 # helper to handle the block given only an actor's AP id
976 def block(blocker, %{ap_id: ap_id}) do
977 block(blocker, get_cached_by_ap_id(ap_id))
980 def unblock(blocker, %{ap_id: ap_id}) do
983 |> User.Info.remove_from_block(ap_id)
987 |> put_embed(:info, info_cng)
989 update_and_set_cache(cng)
992 def mutes?(nil, _), do: false
993 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
995 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
996 def muted_notifications?(nil, _), do: false
998 def muted_notifications?(user, %{ap_id: ap_id}),
999 do: Enum.member?(user.info.muted_notifications, ap_id)
1001 def blocks?(%User{} = user, %User{} = target) do
1002 blocks_ap_id?(user, target) || blocks_domain?(user, target)
1005 def blocks?(nil, _), do: false
1007 def blocks_ap_id?(%User{} = user, %User{} = target) do
1008 Enum.member?(user.info.blocks, target.ap_id)
1011 def blocks_ap_id?(_, _), do: false
1013 def blocks_domain?(%User{} = user, %User{} = target) do
1014 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
1015 %{host: host} = URI.parse(target.ap_id)
1016 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1019 def blocks_domain?(_, _), do: false
1021 def subscribed_to?(user, %{ap_id: ap_id}) do
1022 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1023 Enum.member?(target.info.subscribers, user.ap_id)
1027 @spec muted_users(User.t()) :: [User.t()]
1028 def muted_users(user) do
1029 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
1033 @spec blocked_users(User.t()) :: [User.t()]
1034 def blocked_users(user) do
1035 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
1039 @spec subscribers(User.t()) :: [User.t()]
1040 def subscribers(user) do
1041 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
1045 def block_domain(user, domain) do
1048 |> User.Info.add_to_domain_block(domain)
1052 |> put_embed(:info, info_cng)
1054 update_and_set_cache(cng)
1057 def unblock_domain(user, domain) do
1060 |> User.Info.remove_from_domain_block(domain)
1064 |> put_embed(:info, info_cng)
1066 update_and_set_cache(cng)
1069 def deactivate_async(user, status \\ true) do
1070 PleromaJobQueue.enqueue(:background, __MODULE__, [:deactivate_async, user, status])
1073 def deactivate(%User{} = user, status \\ true) do
1074 info_cng = User.Info.set_activation_status(user.info, status)
1076 with {:ok, friends} <- User.get_friends(user),
1077 {:ok, followers} <- User.get_followers(user),
1081 |> put_embed(:info, info_cng)
1082 |> update_and_set_cache() do
1083 Enum.each(followers, &invalidate_cache(&1))
1084 Enum.each(friends, &update_follower_count(&1))
1090 def update_notification_settings(%User{} = user, settings \\ %{}) do
1091 info_changeset = User.Info.update_notification_settings(user.info, settings)
1094 |> put_embed(:info, info_changeset)
1095 |> update_and_set_cache()
1098 @spec delete(User.t()) :: :ok
1099 def delete(%User{} = user),
1100 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:delete, user])
1102 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1103 def perform(:delete, %User{} = user) do
1104 {:ok, _user} = ActivityPub.delete(user)
1106 # Remove all relationships
1107 {:ok, followers} = User.get_followers(user)
1109 Enum.each(followers, fn follower ->
1110 ActivityPub.unfollow(follower, user)
1111 User.unfollow(follower, user)
1114 {:ok, friends} = User.get_friends(user)
1116 Enum.each(friends, fn followed ->
1117 ActivityPub.unfollow(user, followed)
1118 User.unfollow(user, followed)
1121 delete_user_activities(user)
1122 invalidate_cache(user)
1126 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1127 def perform(:fetch_initial_posts, %User{} = user) do
1128 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1131 # Insert all the posts in reverse order, so they're in the right order on the timeline
1132 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
1133 &Pleroma.Web.Federator.incoming_ap_doc/1
1139 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1141 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1142 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1143 when is_list(blocked_identifiers) do
1145 blocked_identifiers,
1146 fn blocked_identifier ->
1147 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1148 {:ok, blocker} <- block(blocker, blocked),
1149 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1153 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1160 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1161 def perform(:follow_import, %User{} = follower, followed_identifiers)
1162 when is_list(followed_identifiers) do
1164 followed_identifiers,
1165 fn followed_identifier ->
1166 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1167 {:ok, follower} <- maybe_direct_follow(follower, followed),
1168 {:ok, _} <- ActivityPub.follow(follower, followed) do
1172 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1179 @spec external_users_query() :: Ecto.Query.t()
1180 def external_users_query do
1188 @spec external_users(keyword()) :: [User.t()]
1189 def external_users(opts \\ []) do
1191 external_users_query()
1192 |> select([u], struct(u, [:id, :ap_id, :info]))
1196 do: where(query, [u], u.id > ^opts[:max_id]),
1201 do: limit(query, ^opts[:limit]),
1207 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers),
1209 PleromaJobQueue.enqueue(:background, __MODULE__, [
1215 def follow_import(%User{} = follower, followed_identifiers) when is_list(followed_identifiers),
1217 PleromaJobQueue.enqueue(:background, __MODULE__, [
1220 followed_identifiers
1223 def delete_user_activities(%User{ap_id: ap_id} = user) do
1225 |> Activity.Queries.by_actor()
1226 |> RepoStreamer.chunk_stream(50)
1227 |> Stream.each(fn activities ->
1228 Enum.each(activities, &delete_activity(&1))
1235 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1237 |> Object.normalize()
1238 |> ActivityPub.delete()
1241 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1242 user = get_cached_by_ap_id(activity.actor)
1243 object = Object.normalize(activity)
1245 ActivityPub.unlike(user, object)
1248 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1249 user = get_cached_by_ap_id(activity.actor)
1250 object = Object.normalize(activity)
1252 ActivityPub.unannounce(user, object)
1255 defp delete_activity(_activity), do: "Doing nothing"
1257 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1258 Pleroma.HTML.Scrubber.TwitterText
1261 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1263 def fetch_by_ap_id(ap_id) do
1264 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1271 case OStatus.make_user(ap_id) do
1272 {:ok, user} -> {:ok, user}
1273 _ -> {:error, "Could not fetch by AP id"}
1278 def get_or_fetch_by_ap_id(ap_id) do
1279 user = get_cached_by_ap_id(ap_id)
1281 if !is_nil(user) and !User.needs_update?(user) do
1284 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1285 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1287 resp = fetch_by_ap_id(ap_id)
1289 if should_fetch_initial do
1290 with {:ok, %User{} = user} <- resp do
1291 fetch_initial_posts(user)
1299 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1300 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1301 if user = get_cached_by_ap_id(uri) do
1305 %User{info: %User.Info{}}
1306 |> cast(%{}, [:ap_id, :nickname, :local])
1307 |> put_change(:ap_id, uri)
1308 |> put_change(:nickname, nickname)
1309 |> put_change(:local, true)
1310 |> put_change(:follower_address, uri <> "/followers")
1312 {:ok, user} = Repo.insert(changes)
1318 def public_key_from_info(%{
1319 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1323 |> :public_key.pem_decode()
1325 |> :public_key.pem_entry_decode()
1331 def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
1332 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1335 def public_key_from_info(_), do: {:error, "not found key"}
1337 def get_public_key_for_ap_id(ap_id) do
1338 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1339 {:ok, public_key} <- public_key_from_info(user.info) do
1346 defp blank?(""), do: nil
1347 defp blank?(n), do: n
1349 def insert_or_update_user(data) do
1351 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1352 |> remote_user_creation()
1353 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1357 def ap_enabled?(%User{local: true}), do: true
1358 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1359 def ap_enabled?(_), do: false
1361 @doc "Gets or fetch a user by uri or nickname."
1362 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1363 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1364 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1366 # wait a period of time and return newest version of the User structs
1367 # this is because we have synchronous follow APIs and need to simulate them
1368 # with an async handshake
1369 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1370 with %User{} = a <- User.get_cached_by_id(a.id),
1371 %User{} = b <- User.get_cached_by_id(b.id) do
1379 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1380 with :ok <- :timer.sleep(timeout),
1381 %User{} = a <- User.get_cached_by_id(a.id),
1382 %User{} = b <- User.get_cached_by_id(b.id) do
1390 def parse_bio(bio) when is_binary(bio) and bio != "" do
1392 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1396 def parse_bio(_), do: ""
1398 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1399 # TODO: get profile URLs other than user.ap_id
1400 profile_urls = [user.ap_id]
1403 |> CommonUtils.format_input("text/plain",
1404 mentions_format: :full,
1405 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1410 def parse_bio(_, _), do: ""
1412 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1413 Repo.transaction(fn ->
1414 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1418 def tag(nickname, tags) when is_binary(nickname),
1419 do: tag(get_by_nickname(nickname), tags)
1421 def tag(%User{} = user, tags),
1422 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1424 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1425 Repo.transaction(fn ->
1426 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1430 def untag(nickname, tags) when is_binary(nickname),
1431 do: untag(get_by_nickname(nickname), tags)
1433 def untag(%User{} = user, tags),
1434 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1436 defp update_tags(%User{} = user, new_tags) do
1437 {:ok, updated_user} =
1439 |> change(%{tags: new_tags})
1440 |> update_and_set_cache()
1445 defp normalize_tags(tags) do
1448 |> Enum.map(&String.downcase(&1))
1451 defp local_nickname_regex do
1452 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1453 @extended_local_nickname_regex
1455 @strict_local_nickname_regex
1459 def local_nickname(nickname_or_mention) do
1462 |> String.split("@")
1466 def full_nickname(nickname_or_mention),
1467 do: String.trim_leading(nickname_or_mention, "@")
1469 def error_user(ap_id) do
1474 nickname: "erroruser@example.com",
1475 inserted_at: NaiveDateTime.utc_now()
1479 @spec all_superusers() :: [User.t()]
1480 def all_superusers do
1481 User.Query.build(%{super_users: true, local: true, deactivated: false})
1485 def showing_reblogs?(%User{} = user, %User{} = target) do
1486 target.ap_id not in user.info.muted_reblogs
1490 The function returns a query to get users with no activity for given interval of days.
1491 Inactive users are those who didn't read any notification, or had any activity where
1492 the user is the activity's actor, during `inactivity_threshold` days.
1493 Deactivated users will not appear in this list.
1497 iex> Pleroma.User.list_inactive_users()
1500 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1501 def list_inactive_users_query(inactivity_threshold \\ 7) do
1502 negative_inactivity_threshold = -inactivity_threshold
1503 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1504 # Subqueries are not supported in `where` clauses, join gets too complicated.
1505 has_read_notifications =
1506 from(n in Pleroma.Notification,
1507 where: n.seen == true,
1509 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1512 |> Pleroma.Repo.all()
1514 from(u in Pleroma.User,
1515 left_join: a in Pleroma.Activity,
1516 on: u.ap_id == a.actor,
1517 where: not is_nil(u.nickname),
1518 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1519 where: u.id not in ^has_read_notifications,
1522 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1523 is_nil(max(a.inserted_at))
1528 Enable or disable email notifications for user
1532 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1533 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1535 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1536 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1538 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1539 {:ok, t()} | {:error, Ecto.Changeset.t()}
1540 def switch_email_notifications(user, type, status) do
1541 info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})
1544 |> put_embed(:info, info)
1545 |> update_and_set_cache()
1549 Set `last_digest_emailed_at` value for the user to current time
1551 @spec touch_last_digest_emailed_at(t()) :: t()
1552 def touch_last_digest_emailed_at(user) do
1553 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1555 {:ok, updated_user} =
1557 |> change(%{last_digest_emailed_at: now})
1558 |> update_and_set_cache()
1563 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1564 def toggle_confirmation(%User{} = user) do
1565 need_confirmation? = !user.info.confirmation_pending
1568 User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
1572 |> put_embed(:info, info_changeset)
1573 |> update_and_set_cache()
1576 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1580 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1581 # use instance-default
1582 config = Pleroma.Config.get([:assets, :mascots])
1583 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1584 mascot = Keyword.get(config, default_mascot)
1587 "id" => "default-mascot",
1588 "url" => mascot[:url],
1589 "preview_url" => mascot[:url],
1591 "mime_type" => mascot[:mime_type]
1596 def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
1598 def ensure_keys_present(%User{} = user) do
1599 with {:ok, pem} <- Keys.generate_rsa_pem() do
1601 |> cast(%{keys: pem}, [:keys])
1602 |> validate_required([:keys])
1603 |> update_and_set_cache()
1607 def get_ap_ids_by_nicknames(nicknames) do
1609 where: u.nickname in ^nicknames,
1615 defdelegate search(query, opts \\ []), to: User.Search
1617 defp put_password_hash(
1618 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1620 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1623 defp put_password_hash(changeset), do: changeset
1625 def is_internal_user?(%User{nickname: nil}), do: true
1626 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1627 def is_internal_user?(_), do: false
1629 def change_email(user, email) do
1631 |> cast(%{email: email}, [:email])
1632 |> validate_required([:email])
1633 |> unique_constraint(:email)
1634 |> validate_format(:email, @email_regex)
1635 |> update_and_set_cache()