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)
274 @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
275 def reset_password(%User{id: user_id} = user, data) do
278 |> Multi.update(:user, password_update_changeset(user, data))
279 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
280 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
282 case Repo.transaction(multi) do
283 {:ok, %{user: user} = _} -> set_cache(user)
284 {:error, _, changeset, _} -> {:error, changeset}
288 def register_changeset(struct, params \\ %{}, opts \\ []) do
289 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
290 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
293 if is_nil(opts[:need_confirmation]) do
294 Pleroma.Config.get([:instance, :account_activation_required])
296 opts[:need_confirmation]
300 User.Info.confirmation_changeset(%User.Info{}, need_confirmation: need_confirmation?)
304 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
305 |> validate_required([:name, :nickname, :password, :password_confirmation])
306 |> validate_confirmation(:password)
307 |> unique_constraint(:email)
308 |> unique_constraint(:nickname)
309 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
310 |> validate_format(:nickname, local_nickname_regex())
311 |> validate_format(:email, @email_regex)
312 |> validate_length(:bio, max: bio_limit)
313 |> validate_length(:name, min: 1, max: name_limit)
314 |> put_change(:info, info_change)
317 if opts[:external] do
320 validate_required(changeset, [:email])
323 if changeset.valid? do
324 ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
325 followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
329 |> put_change(:ap_id, ap_id)
330 |> unique_constraint(:ap_id)
331 |> put_change(:following, [followers])
332 |> put_change(:follower_address, followers)
338 defp autofollow_users(user) do
339 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
342 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
345 follow_all(user, autofollowed_users)
348 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
349 def register(%Ecto.Changeset{} = changeset) do
350 with {:ok, user} <- Repo.insert(changeset),
351 {:ok, user} <- post_register_action(user) do
356 def post_register_action(%User{} = user) do
357 with {:ok, user} <- autofollow_users(user),
358 {:ok, user} <- set_cache(user),
359 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
360 {:ok, _} <- try_send_confirmation_email(user) do
365 def try_send_confirmation_email(%User{} = user) do
366 if user.info.confirmation_pending &&
367 Pleroma.Config.get([:instance, :account_activation_required]) do
369 |> Pleroma.Emails.UserEmail.account_confirmation_email()
370 |> Pleroma.Emails.Mailer.deliver_async()
378 def needs_update?(%User{local: true}), do: false
380 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
382 def needs_update?(%User{local: false} = user) do
383 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
386 def needs_update?(_), do: true
388 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
389 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
393 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
394 follow(follower, followed)
397 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
398 if not User.ap_enabled?(followed) do
399 follow(follower, followed)
405 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
406 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
407 def follow_all(follower, followeds) do
410 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
411 |> Enum.map(fn %{follower_address: fa} -> fa end)
415 where: u.id == ^follower.id,
420 "array(select distinct unnest (array_cat(?, ?)))",
429 {1, [follower]} = Repo.update_all(q, [])
431 Enum.each(followeds, fn followed ->
432 update_follower_count(followed)
438 def follow(%User{} = follower, %User{info: info} = followed) do
439 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
440 ap_followers = followed.follower_address
444 {:error, "Could not follow user: You are deactivated."}
446 deny_follow_blocked and blocks?(followed, follower) ->
447 {:error, "Could not follow user: #{followed.nickname} blocked you."}
450 if !followed.local && follower.local && !ap_enabled?(followed) do
451 Websub.subscribe(follower, followed)
456 where: u.id == ^follower.id,
457 update: [push: [following: ^ap_followers]],
461 {1, [follower]} = Repo.update_all(q, [])
463 follower = maybe_update_following_count(follower)
465 {:ok, _} = update_follower_count(followed)
471 def unfollow(%User{} = follower, %User{} = followed) do
472 ap_followers = followed.follower_address
474 if following?(follower, followed) and follower.ap_id != followed.ap_id do
477 where: u.id == ^follower.id,
478 update: [pull: [following: ^ap_followers]],
482 {1, [follower]} = Repo.update_all(q, [])
484 follower = maybe_update_following_count(follower)
486 {:ok, followed} = update_follower_count(followed)
490 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
492 {:error, "Not subscribed!"}
496 @spec following?(User.t(), User.t()) :: boolean
497 def following?(%User{} = follower, %User{} = followed) do
498 Enum.member?(follower.following, followed.follower_address)
501 def locked?(%User{} = user) do
502 user.info.locked || false
506 Repo.get_by(User, id: id)
509 def get_by_ap_id(ap_id) do
510 Repo.get_by(User, ap_id: ap_id)
513 def get_all_by_ap_id(ap_ids) do
514 from(u in __MODULE__,
515 where: u.ap_id in ^ap_ids
520 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
521 # of the ap_id and the domain and tries to get that user
522 def get_by_guessed_nickname(ap_id) do
523 domain = URI.parse(ap_id).host
524 name = List.last(String.split(ap_id, "/"))
525 nickname = "#{name}@#{domain}"
527 get_cached_by_nickname(nickname)
530 def set_cache({:ok, user}), do: set_cache(user)
531 def set_cache({:error, err}), do: {:error, err}
533 def set_cache(%User{} = user) do
534 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
535 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
536 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
540 def update_and_set_cache(changeset) do
541 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
548 def invalidate_cache(user) do
549 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
550 Cachex.del(:user_cache, "nickname:#{user.nickname}")
551 Cachex.del(:user_cache, "user_info:#{user.id}")
554 def get_cached_by_ap_id(ap_id) do
555 key = "ap_id:#{ap_id}"
556 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
559 def get_cached_by_id(id) do
563 Cachex.fetch!(:user_cache, key, fn _ ->
567 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
568 {:commit, user.ap_id}
574 get_cached_by_ap_id(ap_id)
577 def get_cached_by_nickname(nickname) do
578 key = "nickname:#{nickname}"
580 Cachex.fetch!(:user_cache, key, fn ->
581 user_result = get_or_fetch_by_nickname(nickname)
584 {:ok, user} -> {:commit, user}
585 {:error, _error} -> {:ignore, nil}
590 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
591 restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
594 is_integer(nickname_or_id) or Pleroma.FlakeId.is_flake_id?(nickname_or_id) ->
595 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
597 restrict_to_local == false ->
598 get_cached_by_nickname(nickname_or_id)
600 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
601 get_cached_by_nickname(nickname_or_id)
608 def get_by_nickname(nickname) do
609 Repo.get_by(User, nickname: nickname) ||
610 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
611 Repo.get_by(User, nickname: local_nickname(nickname))
615 def get_by_email(email), do: Repo.get_by(User, email: email)
617 def get_by_nickname_or_email(nickname_or_email) do
618 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
621 def get_cached_user_info(user) do
622 key = "user_info:#{user.id}"
623 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
626 def fetch_by_nickname(nickname) do
627 ap_try = ActivityPub.make_user_from_nickname(nickname)
630 {:ok, user} -> {:ok, user}
631 _ -> OStatus.make_user(nickname)
635 def get_or_fetch_by_nickname(nickname) do
636 with %User{} = user <- get_by_nickname(nickname) do
640 with [_nick, _domain] <- String.split(nickname, "@"),
641 {:ok, user} <- fetch_by_nickname(nickname) do
642 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
643 fetch_initial_posts(user)
648 _e -> {:error, "not found " <> nickname}
653 @doc "Fetch some posts when the user has just been federated with"
654 def fetch_initial_posts(user) do
655 BackgroundWorker.enqueue("fetch_initial_posts", %{"user_id" => user.id})
658 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
659 def get_followers_query(%User{} = user, nil) do
660 User.Query.build(%{followers: user, deactivated: false})
663 def get_followers_query(user, page) do
664 from(u in get_followers_query(user, nil))
665 |> User.Query.paginate(page, 20)
668 @spec get_followers_query(User.t()) :: Ecto.Query.t()
669 def get_followers_query(user), do: get_followers_query(user, nil)
671 @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
672 def get_followers(user, page \\ nil) do
673 q = get_followers_query(user, page)
678 @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
679 def get_external_followers(user, page \\ nil) do
682 |> get_followers_query(page)
683 |> User.Query.build(%{external: true})
688 def get_followers_ids(user, page \\ nil) do
689 q = get_followers_query(user, page)
691 Repo.all(from(u in q, select: u.id))
694 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
695 def get_friends_query(%User{} = user, nil) do
696 User.Query.build(%{friends: user, deactivated: false})
699 def get_friends_query(user, page) do
700 from(u in get_friends_query(user, nil))
701 |> User.Query.paginate(page, 20)
704 @spec get_friends_query(User.t()) :: Ecto.Query.t()
705 def get_friends_query(user), do: get_friends_query(user, nil)
707 def get_friends(user, page \\ nil) do
708 q = get_friends_query(user, page)
713 def get_friends_ids(user, page \\ nil) do
714 q = get_friends_query(user, page)
716 Repo.all(from(u in q, select: u.id))
719 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
720 def get_follow_requests(%User{} = user) do
722 Activity.follow_requests_for_actor(user)
723 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
724 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
725 |> group_by([a, u], u.id)
732 def increase_note_count(%User{} = user) do
734 |> where(id: ^user.id)
739 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
746 |> Repo.update_all([])
748 {1, [user]} -> set_cache(user)
753 def decrease_note_count(%User{} = user) do
755 |> where(id: ^user.id)
760 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
767 |> Repo.update_all([])
769 {1, [user]} -> set_cache(user)
774 def update_note_count(%User{} = user) do
778 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
782 note_count = Repo.one(note_count_query)
784 info_cng = User.Info.set_note_count(user.info, note_count)
788 |> put_embed(:info, info_cng)
789 |> update_and_set_cache()
792 @spec maybe_fetch_follow_information(User.t()) :: User.t()
793 def maybe_fetch_follow_information(user) do
794 with {:ok, user} <- fetch_follow_information(user) do
798 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
804 def fetch_follow_information(user) do
805 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
806 info_cng = User.Info.follow_information_update(user.info, info)
811 |> put_embed(:info, info_cng)
813 update_and_set_cache(changeset)
820 def update_follower_count(%User{} = user) do
821 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
822 follower_count_query =
823 User.Query.build(%{followers: user, deactivated: false})
824 |> select([u], %{count: count(u.id)})
827 |> where(id: ^user.id)
828 |> join(:inner, [u], s in subquery(follower_count_query))
833 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
840 |> Repo.update_all([])
842 {1, [user]} -> set_cache(user)
846 {:ok, maybe_fetch_follow_information(user)}
850 @spec maybe_update_following_count(User.t()) :: User.t()
851 def maybe_update_following_count(%User{local: false} = user) do
852 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
853 maybe_fetch_follow_information(user)
859 def maybe_update_following_count(user), do: user
861 def remove_duplicated_following(%User{following: following} = user) do
862 uniq_following = Enum.uniq(following)
864 if length(following) == length(uniq_following) do
868 |> update_changeset(%{following: uniq_following})
869 |> update_and_set_cache()
873 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
874 def get_users_from_set(ap_ids, local_only \\ true) do
875 criteria = %{ap_id: ap_ids, deactivated: false}
876 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
878 User.Query.build(criteria)
882 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
883 def get_recipients_from_activity(%Activity{recipients: to}) do
884 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
888 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
889 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
893 User.Info.add_to_mutes(info, ap_id)
894 |> User.Info.add_to_muted_notifications(info, ap_id, notifications?)
898 |> put_embed(:info, info_cng)
900 update_and_set_cache(cng)
903 def unmute(muter, %{ap_id: ap_id}) do
907 User.Info.remove_from_mutes(info, ap_id)
908 |> User.Info.remove_from_muted_notifications(info, ap_id)
912 |> put_embed(:info, info_cng)
914 update_and_set_cache(cng)
917 def subscribe(subscriber, %{ap_id: ap_id}) do
918 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
920 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
921 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
924 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
928 |> User.Info.add_to_subscribers(subscriber.ap_id)
931 |> put_embed(:info, info_cng)
932 |> update_and_set_cache()
937 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
938 with %User{} = user <- get_cached_by_ap_id(ap_id) do
941 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
944 |> put_embed(:info, info_cng)
945 |> update_and_set_cache()
949 def block(blocker, %User{ap_id: ap_id} = blocked) do
950 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
952 if following?(blocker, blocked) do
953 {:ok, blocker, _} = unfollow(blocker, blocked)
959 # clear any requested follows as well
961 case CommonAPI.reject_follow_request(blocked, blocker) do
962 {:ok, %User{} = updated_blocked} -> updated_blocked
967 if subscribed_to?(blocked, blocker) do
968 {:ok, blocker} = unsubscribe(blocked, blocker)
974 if following?(blocked, blocker) do
975 unfollow(blocked, blocker)
978 {:ok, blocker} = update_follower_count(blocker)
982 |> User.Info.add_to_block(ap_id)
986 |> put_embed(:info, info_cng)
988 update_and_set_cache(cng)
991 # helper to handle the block given only an actor's AP id
992 def block(blocker, %{ap_id: ap_id}) do
993 block(blocker, get_cached_by_ap_id(ap_id))
996 def unblock(blocker, %{ap_id: ap_id}) do
999 |> User.Info.remove_from_block(ap_id)
1003 |> put_embed(:info, info_cng)
1005 update_and_set_cache(cng)
1008 def mutes?(nil, _), do: false
1009 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
1011 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
1012 def muted_notifications?(nil, _), do: false
1014 def muted_notifications?(user, %{ap_id: ap_id}),
1015 do: Enum.member?(user.info.muted_notifications, ap_id)
1017 def blocks?(%User{} = user, %User{} = target) do
1018 blocks_ap_id?(user, target) || blocks_domain?(user, target)
1021 def blocks?(nil, _), do: false
1023 def blocks_ap_id?(%User{} = user, %User{} = target) do
1024 Enum.member?(user.info.blocks, target.ap_id)
1027 def blocks_ap_id?(_, _), do: false
1029 def blocks_domain?(%User{} = user, %User{} = target) do
1030 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
1031 %{host: host} = URI.parse(target.ap_id)
1032 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1035 def blocks_domain?(_, _), do: false
1037 def subscribed_to?(user, %{ap_id: ap_id}) do
1038 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1039 Enum.member?(target.info.subscribers, user.ap_id)
1043 @spec muted_users(User.t()) :: [User.t()]
1044 def muted_users(user) do
1045 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
1049 @spec blocked_users(User.t()) :: [User.t()]
1050 def blocked_users(user) do
1051 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
1055 @spec subscribers(User.t()) :: [User.t()]
1056 def subscribers(user) do
1057 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
1061 def block_domain(user, domain) do
1064 |> User.Info.add_to_domain_block(domain)
1068 |> put_embed(:info, info_cng)
1070 update_and_set_cache(cng)
1073 def unblock_domain(user, domain) do
1076 |> User.Info.remove_from_domain_block(domain)
1080 |> put_embed(:info, info_cng)
1082 update_and_set_cache(cng)
1085 def deactivate_async(user, status \\ true) do
1086 BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
1089 def deactivate(%User{} = user, status \\ true) do
1090 info_cng = User.Info.set_activation_status(user.info, status)
1092 with {:ok, friends} <- User.get_friends(user),
1093 {:ok, followers} <- User.get_followers(user),
1097 |> put_embed(:info, info_cng)
1098 |> update_and_set_cache() do
1099 Enum.each(followers, &invalidate_cache(&1))
1100 Enum.each(friends, &update_follower_count(&1))
1106 def update_notification_settings(%User{} = user, settings \\ %{}) do
1107 info_changeset = User.Info.update_notification_settings(user.info, settings)
1110 |> put_embed(:info, info_changeset)
1111 |> update_and_set_cache()
1114 def delete(%User{} = user) do
1115 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1118 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1119 def perform(:delete, %User{} = user) do
1120 {:ok, _user} = ActivityPub.delete(user)
1122 # Remove all relationships
1123 {:ok, followers} = User.get_followers(user)
1125 Enum.each(followers, fn follower ->
1126 ActivityPub.unfollow(follower, user)
1127 User.unfollow(follower, user)
1130 {:ok, friends} = User.get_friends(user)
1132 Enum.each(friends, fn followed ->
1133 ActivityPub.unfollow(user, followed)
1134 User.unfollow(user, followed)
1137 delete_user_activities(user)
1138 invalidate_cache(user)
1142 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1143 def perform(:fetch_initial_posts, %User{} = user) do
1144 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1147 # Insert all the posts in reverse order, so they're in the right order on the timeline
1148 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
1149 &Pleroma.Web.Federator.incoming_ap_doc/1
1155 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1157 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1158 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1159 when is_list(blocked_identifiers) do
1161 blocked_identifiers,
1162 fn blocked_identifier ->
1163 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1164 {:ok, blocker} <- block(blocker, blocked),
1165 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1169 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1176 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1177 def perform(:follow_import, %User{} = follower, followed_identifiers)
1178 when is_list(followed_identifiers) do
1180 followed_identifiers,
1181 fn followed_identifier ->
1182 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1183 {:ok, follower} <- maybe_direct_follow(follower, followed),
1184 {:ok, _} <- ActivityPub.follow(follower, followed) do
1188 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1195 @spec external_users_query() :: Ecto.Query.t()
1196 def external_users_query do
1204 @spec external_users(keyword()) :: [User.t()]
1205 def external_users(opts \\ []) do
1207 external_users_query()
1208 |> select([u], struct(u, [:id, :ap_id, :info]))
1212 do: where(query, [u], u.id > ^opts[:max_id]),
1217 do: limit(query, ^opts[:limit]),
1223 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1224 BackgroundWorker.enqueue("blocks_import", %{
1225 "blocker_id" => blocker.id,
1226 "blocked_identifiers" => blocked_identifiers
1230 def follow_import(%User{} = follower, followed_identifiers)
1231 when is_list(followed_identifiers) do
1232 BackgroundWorker.enqueue("follow_import", %{
1233 "follower_id" => follower.id,
1234 "followed_identifiers" => followed_identifiers
1238 def delete_user_activities(%User{ap_id: ap_id} = user) do
1240 |> Activity.Queries.by_actor()
1241 |> RepoStreamer.chunk_stream(50)
1242 |> Stream.each(fn activities ->
1243 Enum.each(activities, &delete_activity(&1))
1250 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1252 |> Object.normalize()
1253 |> ActivityPub.delete()
1256 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1257 user = get_cached_by_ap_id(activity.actor)
1258 object = Object.normalize(activity)
1260 ActivityPub.unlike(user, object)
1263 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1264 user = get_cached_by_ap_id(activity.actor)
1265 object = Object.normalize(activity)
1267 ActivityPub.unannounce(user, object)
1270 defp delete_activity(_activity), do: "Doing nothing"
1272 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1273 Pleroma.HTML.Scrubber.TwitterText
1276 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1278 def fetch_by_ap_id(ap_id) do
1279 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1286 case OStatus.make_user(ap_id) do
1287 {:ok, user} -> {:ok, user}
1288 _ -> {:error, "Could not fetch by AP id"}
1293 def get_or_fetch_by_ap_id(ap_id) do
1294 user = get_cached_by_ap_id(ap_id)
1296 if !is_nil(user) and !User.needs_update?(user) do
1299 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1300 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1302 resp = fetch_by_ap_id(ap_id)
1304 if should_fetch_initial do
1305 with {:ok, %User{} = user} <- resp do
1306 fetch_initial_posts(user)
1314 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1315 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1316 if user = get_cached_by_ap_id(uri) do
1320 %User{info: %User.Info{}}
1321 |> cast(%{}, [:ap_id, :nickname, :local])
1322 |> put_change(:ap_id, uri)
1323 |> put_change(:nickname, nickname)
1324 |> put_change(:local, true)
1325 |> put_change(:follower_address, uri <> "/followers")
1327 {:ok, user} = Repo.insert(changes)
1333 def public_key_from_info(%{
1334 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1338 |> :public_key.pem_decode()
1340 |> :public_key.pem_entry_decode()
1346 def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
1347 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1350 def public_key_from_info(_), do: {:error, "not found key"}
1352 def get_public_key_for_ap_id(ap_id) do
1353 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1354 {:ok, public_key} <- public_key_from_info(user.info) do
1361 defp blank?(""), do: nil
1362 defp blank?(n), do: n
1364 def insert_or_update_user(data) do
1366 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1367 |> remote_user_creation()
1368 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1372 def ap_enabled?(%User{local: true}), do: true
1373 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1374 def ap_enabled?(_), do: false
1376 @doc "Gets or fetch a user by uri or nickname."
1377 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1378 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1379 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1381 # wait a period of time and return newest version of the User structs
1382 # this is because we have synchronous follow APIs and need to simulate them
1383 # with an async handshake
1384 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1385 with %User{} = a <- User.get_cached_by_id(a.id),
1386 %User{} = b <- User.get_cached_by_id(b.id) do
1394 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1395 with :ok <- :timer.sleep(timeout),
1396 %User{} = a <- User.get_cached_by_id(a.id),
1397 %User{} = b <- User.get_cached_by_id(b.id) do
1405 def parse_bio(bio) when is_binary(bio) and bio != "" do
1407 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1411 def parse_bio(_), do: ""
1413 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1414 # TODO: get profile URLs other than user.ap_id
1415 profile_urls = [user.ap_id]
1418 |> CommonUtils.format_input("text/plain",
1419 mentions_format: :full,
1420 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1425 def parse_bio(_, _), do: ""
1427 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1428 Repo.transaction(fn ->
1429 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1433 def tag(nickname, tags) when is_binary(nickname),
1434 do: tag(get_by_nickname(nickname), tags)
1436 def tag(%User{} = user, tags),
1437 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1439 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1440 Repo.transaction(fn ->
1441 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1445 def untag(nickname, tags) when is_binary(nickname),
1446 do: untag(get_by_nickname(nickname), tags)
1448 def untag(%User{} = user, tags),
1449 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1451 defp update_tags(%User{} = user, new_tags) do
1452 {:ok, updated_user} =
1454 |> change(%{tags: new_tags})
1455 |> update_and_set_cache()
1460 defp normalize_tags(tags) do
1463 |> Enum.map(&String.downcase(&1))
1466 defp local_nickname_regex do
1467 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1468 @extended_local_nickname_regex
1470 @strict_local_nickname_regex
1474 def local_nickname(nickname_or_mention) do
1477 |> String.split("@")
1481 def full_nickname(nickname_or_mention),
1482 do: String.trim_leading(nickname_or_mention, "@")
1484 def error_user(ap_id) do
1489 nickname: "erroruser@example.com",
1490 inserted_at: NaiveDateTime.utc_now()
1494 @spec all_superusers() :: [User.t()]
1495 def all_superusers do
1496 User.Query.build(%{super_users: true, local: true, deactivated: false})
1500 def showing_reblogs?(%User{} = user, %User{} = target) do
1501 target.ap_id not in user.info.muted_reblogs
1505 The function returns a query to get users with no activity for given interval of days.
1506 Inactive users are those who didn't read any notification, or had any activity where
1507 the user is the activity's actor, during `inactivity_threshold` days.
1508 Deactivated users will not appear in this list.
1512 iex> Pleroma.User.list_inactive_users()
1515 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1516 def list_inactive_users_query(inactivity_threshold \\ 7) do
1517 negative_inactivity_threshold = -inactivity_threshold
1518 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1519 # Subqueries are not supported in `where` clauses, join gets too complicated.
1520 has_read_notifications =
1521 from(n in Pleroma.Notification,
1522 where: n.seen == true,
1524 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1527 |> Pleroma.Repo.all()
1529 from(u in Pleroma.User,
1530 left_join: a in Pleroma.Activity,
1531 on: u.ap_id == a.actor,
1532 where: not is_nil(u.nickname),
1533 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1534 where: u.id not in ^has_read_notifications,
1537 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1538 is_nil(max(a.inserted_at))
1543 Enable or disable email notifications for user
1547 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1548 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1550 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1551 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1553 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1554 {:ok, t()} | {:error, Ecto.Changeset.t()}
1555 def switch_email_notifications(user, type, status) do
1556 info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})
1559 |> put_embed(:info, info)
1560 |> update_and_set_cache()
1564 Set `last_digest_emailed_at` value for the user to current time
1566 @spec touch_last_digest_emailed_at(t()) :: t()
1567 def touch_last_digest_emailed_at(user) do
1568 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1570 {:ok, updated_user} =
1572 |> change(%{last_digest_emailed_at: now})
1573 |> update_and_set_cache()
1578 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1579 def toggle_confirmation(%User{} = user) do
1580 need_confirmation? = !user.info.confirmation_pending
1583 User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
1587 |> put_embed(:info, info_changeset)
1588 |> update_and_set_cache()
1591 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1595 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1596 # use instance-default
1597 config = Pleroma.Config.get([:assets, :mascots])
1598 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1599 mascot = Keyword.get(config, default_mascot)
1602 "id" => "default-mascot",
1603 "url" => mascot[:url],
1604 "preview_url" => mascot[:url],
1606 "mime_type" => mascot[:mime_type]
1611 def ensure_keys_present(%User{info: info} = user) do
1615 {:ok, pem} = Keys.generate_rsa_pem()
1618 |> Ecto.Changeset.change()
1619 |> Ecto.Changeset.put_embed(:info, User.Info.set_keys(info, pem))
1620 |> update_and_set_cache()
1624 def get_ap_ids_by_nicknames(nicknames) do
1626 where: u.nickname in ^nicknames,
1632 defdelegate search(query, opts \\ []), to: User.Search
1634 defp put_password_hash(
1635 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1637 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1640 defp put_password_hash(changeset), do: changeset
1642 def is_internal_user?(%User{nickname: nil}), do: true
1643 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1644 def is_internal_user?(_), do: false
1646 # A hack because user delete activities have a fake id for whatever reason
1647 # TODO: Get rid of this
1648 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
1650 def get_delivered_users_by_object_id(object_id) do
1652 inner_join: delivery in assoc(u, :deliveries),
1653 where: delivery.object_id == ^object_id
1658 def change_email(user, email) do
1660 |> cast(%{email: email}, [:email])
1661 |> validate_required([:email])
1662 |> unique_constraint(:email)
1663 |> validate_format(:email, @email_regex)
1664 |> update_and_set_cache()