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
30 alias Pleroma.Workers.BackgroundWorker
34 @type t :: %__MODULE__{}
36 @primary_key {:id, Pleroma.FlakeId, autogenerate: true}
38 # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
39 @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])?)*$/
41 @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
42 @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
46 field(:email, :string)
48 field(:nickname, :string)
49 field(:password_hash, :string)
50 field(:password, :string, virtual: true)
51 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{}), do: true
75 def visible_for?(user, for_user \\ nil)
77 def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
79 def visible_for?(%User{} = user, for_user) do
80 auth_active?(user) || superuser?(for_user)
83 def visible_for?(_, _), do: false
85 def superuser?(%User{local: true, info: %User.Info{is_admin: true}}), do: true
86 def superuser?(%User{local: true, info: %User.Info{is_moderator: true}}), do: true
87 def superuser?(_), do: false
89 def avatar_url(user, options \\ []) do
91 %{"url" => [%{"href" => href} | _]} -> href
92 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
96 def banner_url(user, options \\ []) do
97 case user.info.banner do
98 %{"url" => [%{"href" => href} | _]} -> href
99 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
103 def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url
104 def profile_url(%User{ap_id: ap_id}), do: ap_id
105 def profile_url(_), do: nil
107 def ap_id(%User{nickname: nickname}) do
108 "#{Web.base_url()}/users/#{nickname}"
111 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
112 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
114 @spec ap_following(User.t()) :: Sring.t()
115 def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
116 def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
118 def user_info(%User{} = user, args \\ %{}) do
120 if args[:following_count],
121 do: args[:following_count],
122 else: user.info.following_count || following_count(user)
125 if args[:follower_count], do: args[:follower_count], else: user.info.follower_count
128 note_count: user.info.note_count,
129 locked: user.info.locked,
130 confirmation_pending: user.info.confirmation_pending,
131 default_scope: user.info.default_scope
133 |> Map.put(:following_count, following_count)
134 |> Map.put(:follower_count, follower_count)
137 def follow_state(%User{} = user, %User{} = target) do
138 follow_activity = Utils.fetch_latest_follow(user, target)
141 do: follow_activity.data["state"],
142 # Ideally this would be nil, but then Cachex does not commit the value
146 def get_cached_follow_state(user, target) do
147 key = "follow_state:#{user.ap_id}|#{target.ap_id}"
148 Cachex.fetch!(:user_cache, key, fn _ -> {:commit, follow_state(user, target)} end)
151 def set_follow_state_cache(user_ap_id, target_ap_id, state) do
154 "follow_state:#{user_ap_id}|#{target_ap_id}",
159 def set_info_cache(user, args) do
160 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user, args))
163 @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
164 def restrict_deactivated(query) do
166 where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info)
170 def following_count(%User{following: []}), do: 0
172 def following_count(%User{} = user) do
174 |> get_friends_query()
175 |> Repo.aggregate(:count, :id)
178 defp truncate_if_exists(params, key, max_length) do
179 if Map.has_key?(params, key) and is_binary(params[key]) do
180 {value, _chopped} = String.split_at(params[key], max_length)
181 Map.put(params, key, value)
187 def remote_user_creation(params) do
188 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
189 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
193 |> Map.put(:info, params[:info] || %{})
194 |> truncate_if_exists(:name, name_limit)
195 |> truncate_if_exists(:bio, bio_limit)
197 info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])
201 |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
202 |> validate_required([:name, :ap_id])
203 |> unique_constraint(:nickname)
204 |> validate_format(:nickname, @email_regex)
205 |> validate_length(:bio, max: bio_limit)
206 |> validate_length(:name, max: name_limit)
207 |> put_change(:local, false)
208 |> put_embed(:info, info_cng)
211 case info_cng.changes[:source_data] do
212 %{"followers" => followers, "following" => following} ->
214 |> put_change(:follower_address, followers)
215 |> put_change(:following_address, following)
218 followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
221 |> put_change(:follower_address, followers)
228 def update_changeset(struct, params \\ %{}) do
229 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
230 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
233 |> cast(params, [:bio, :name, :avatar, :following])
234 |> unique_constraint(:nickname)
235 |> validate_format(:nickname, local_nickname_regex())
236 |> validate_length(:bio, max: bio_limit)
237 |> validate_length(:name, min: 1, max: name_limit)
240 def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
241 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
242 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
244 params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
245 info_cng = User.Info.user_upgrade(struct.info, params[:info], remote?)
256 |> unique_constraint(:nickname)
257 |> validate_format(:nickname, local_nickname_regex())
258 |> validate_length(:bio, max: bio_limit)
259 |> validate_length(:name, max: name_limit)
260 |> put_embed(:info, info_cng)
263 def password_update_changeset(struct, params) do
265 |> cast(params, [:password, :password_confirmation])
266 |> validate_required([:password, :password_confirmation])
267 |> validate_confirmation(:password)
271 @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
272 def reset_password(%User{id: user_id} = user, data) do
275 |> Multi.update(:user, password_update_changeset(user, data))
276 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
277 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
279 case Repo.transaction(multi) do
280 {:ok, %{user: user} = _} -> set_cache(user)
281 {:error, _, changeset, _} -> {:error, changeset}
285 def register_changeset(struct, params \\ %{}, opts \\ []) do
286 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
287 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
290 if is_nil(opts[:need_confirmation]) do
291 Pleroma.Config.get([:instance, :account_activation_required])
293 opts[:need_confirmation]
297 User.Info.confirmation_changeset(%User.Info{}, need_confirmation: need_confirmation?)
301 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
302 |> validate_required([:name, :nickname, :password, :password_confirmation])
303 |> validate_confirmation(:password)
304 |> unique_constraint(:email)
305 |> unique_constraint(:nickname)
306 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
307 |> validate_format(:nickname, local_nickname_regex())
308 |> validate_format(:email, @email_regex)
309 |> validate_length(:bio, max: bio_limit)
310 |> validate_length(:name, min: 1, max: name_limit)
311 |> put_change(:info, info_change)
314 if opts[:external] do
317 validate_required(changeset, [:email])
320 if changeset.valid? do
321 ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
322 followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
326 |> put_change(:ap_id, ap_id)
327 |> unique_constraint(:ap_id)
328 |> put_change(:following, [followers])
329 |> put_change(:follower_address, followers)
335 defp autofollow_users(user) do
336 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
339 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
342 follow_all(user, autofollowed_users)
345 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
346 def register(%Ecto.Changeset{} = changeset) do
347 with {:ok, user} <- Repo.insert(changeset),
348 {:ok, user} <- post_register_action(user) do
353 def post_register_action(%User{} = user) do
354 with {:ok, user} <- autofollow_users(user),
355 {:ok, user} <- set_cache(user),
356 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
357 {:ok, _} <- try_send_confirmation_email(user) do
362 def try_send_confirmation_email(%User{} = user) do
363 if user.info.confirmation_pending &&
364 Pleroma.Config.get([:instance, :account_activation_required]) do
366 |> Pleroma.Emails.UserEmail.account_confirmation_email()
367 |> Pleroma.Emails.Mailer.deliver_async()
375 def needs_update?(%User{local: true}), do: false
377 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
379 def needs_update?(%User{local: false} = user) do
380 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
383 def needs_update?(_), do: true
385 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
386 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
390 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
391 follow(follower, followed)
394 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
395 if not User.ap_enabled?(followed) do
396 follow(follower, followed)
402 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
403 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
404 def follow_all(follower, followeds) do
407 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
408 |> Enum.map(fn %{follower_address: fa} -> fa end)
412 where: u.id == ^follower.id,
417 "array(select distinct unnest (array_cat(?, ?)))",
426 {1, [follower]} = Repo.update_all(q, [])
428 Enum.each(followeds, fn followed ->
429 update_follower_count(followed)
435 def follow(%User{} = follower, %User{info: info} = followed) do
436 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
437 ap_followers = followed.follower_address
441 {:error, "Could not follow user: You are deactivated."}
443 deny_follow_blocked and blocks?(followed, follower) ->
444 {:error, "Could not follow user: #{followed.nickname} blocked you."}
447 if !followed.local && follower.local && !ap_enabled?(followed) do
448 Websub.subscribe(follower, followed)
453 where: u.id == ^follower.id,
454 update: [push: [following: ^ap_followers]],
458 {1, [follower]} = Repo.update_all(q, [])
460 follower = maybe_update_following_count(follower)
462 {:ok, _} = update_follower_count(followed)
468 def unfollow(%User{} = follower, %User{} = followed) do
469 ap_followers = followed.follower_address
471 if following?(follower, followed) and follower.ap_id != followed.ap_id do
474 where: u.id == ^follower.id,
475 update: [pull: [following: ^ap_followers]],
479 {1, [follower]} = Repo.update_all(q, [])
481 follower = maybe_update_following_count(follower)
483 {:ok, followed} = update_follower_count(followed)
487 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
489 {:error, "Not subscribed!"}
493 @spec following?(User.t(), User.t()) :: boolean
494 def following?(%User{} = follower, %User{} = followed) do
495 Enum.member?(follower.following, followed.follower_address)
498 def locked?(%User{} = user) do
499 user.info.locked || false
503 Repo.get_by(User, id: id)
506 def get_by_ap_id(ap_id) do
507 Repo.get_by(User, ap_id: ap_id)
510 def get_all_by_ap_id(ap_ids) do
511 from(u in __MODULE__,
512 where: u.ap_id in ^ap_ids
517 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
518 # of the ap_id and the domain and tries to get that user
519 def get_by_guessed_nickname(ap_id) do
520 domain = URI.parse(ap_id).host
521 name = List.last(String.split(ap_id, "/"))
522 nickname = "#{name}@#{domain}"
524 get_cached_by_nickname(nickname)
527 def set_cache({:ok, user}), do: set_cache(user)
528 def set_cache({:error, err}), do: {:error, err}
530 def set_cache(%User{} = user) do
531 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
532 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
533 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
537 def update_and_set_cache(changeset) do
538 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
545 def invalidate_cache(user) do
546 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
547 Cachex.del(:user_cache, "nickname:#{user.nickname}")
548 Cachex.del(:user_cache, "user_info:#{user.id}")
551 def get_cached_by_ap_id(ap_id) do
552 key = "ap_id:#{ap_id}"
553 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
556 def get_cached_by_id(id) do
560 Cachex.fetch!(:user_cache, key, fn _ ->
564 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
565 {:commit, user.ap_id}
571 get_cached_by_ap_id(ap_id)
574 def get_cached_by_nickname(nickname) do
575 key = "nickname:#{nickname}"
577 Cachex.fetch!(:user_cache, key, fn ->
578 user_result = get_or_fetch_by_nickname(nickname)
581 {:ok, user} -> {:commit, user}
582 {:error, _error} -> {:ignore, nil}
587 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
588 restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
591 is_integer(nickname_or_id) or Pleroma.FlakeId.is_flake_id?(nickname_or_id) ->
592 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
594 restrict_to_local == false ->
595 get_cached_by_nickname(nickname_or_id)
597 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
598 get_cached_by_nickname(nickname_or_id)
605 def get_by_nickname(nickname) do
606 Repo.get_by(User, nickname: nickname) ||
607 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
608 Repo.get_by(User, nickname: local_nickname(nickname))
612 def get_by_email(email), do: Repo.get_by(User, email: email)
614 def get_by_nickname_or_email(nickname_or_email) do
615 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
618 def get_cached_user_info(user) do
619 key = "user_info:#{user.id}"
620 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
623 def fetch_by_nickname(nickname) do
624 ap_try = ActivityPub.make_user_from_nickname(nickname)
627 {:ok, user} -> {:ok, user}
628 _ -> OStatus.make_user(nickname)
632 def get_or_fetch_by_nickname(nickname) do
633 with %User{} = user <- get_by_nickname(nickname) do
637 with [_nick, _domain] <- String.split(nickname, "@"),
638 {:ok, user} <- fetch_by_nickname(nickname) do
639 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
640 fetch_initial_posts(user)
645 _e -> {:error, "not found " <> nickname}
650 @doc "Fetch some posts when the user has just been federated with"
651 def fetch_initial_posts(user) do
652 BackgroundWorker.enqueue("fetch_initial_posts", %{"user_id" => user.id})
655 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
656 def get_followers_query(%User{} = user, nil) do
657 User.Query.build(%{followers: user, deactivated: false})
660 def get_followers_query(user, page) do
661 from(u in get_followers_query(user, nil))
662 |> User.Query.paginate(page, 20)
665 @spec get_followers_query(User.t()) :: Ecto.Query.t()
666 def get_followers_query(user), do: get_followers_query(user, nil)
668 @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
669 def get_followers(user, page \\ nil) do
670 q = get_followers_query(user, page)
675 @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
676 def get_external_followers(user, page \\ nil) do
679 |> get_followers_query(page)
680 |> User.Query.build(%{external: true})
685 def get_followers_ids(user, page \\ nil) do
686 q = get_followers_query(user, page)
688 Repo.all(from(u in q, select: u.id))
691 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
692 def get_friends_query(%User{} = user, nil) do
693 User.Query.build(%{friends: user, deactivated: false})
696 def get_friends_query(user, page) do
697 from(u in get_friends_query(user, nil))
698 |> User.Query.paginate(page, 20)
701 @spec get_friends_query(User.t()) :: Ecto.Query.t()
702 def get_friends_query(user), do: get_friends_query(user, nil)
704 def get_friends(user, page \\ nil) do
705 q = get_friends_query(user, page)
710 def get_friends_ids(user, page \\ nil) do
711 q = get_friends_query(user, page)
713 Repo.all(from(u in q, select: u.id))
716 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
717 def get_follow_requests(%User{} = user) do
719 Activity.follow_requests_for_actor(user)
720 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
721 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
722 |> group_by([a, u], u.id)
729 def increase_note_count(%User{} = user) do
731 |> where(id: ^user.id)
736 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
743 |> Repo.update_all([])
745 {1, [user]} -> set_cache(user)
750 def decrease_note_count(%User{} = user) do
752 |> where(id: ^user.id)
757 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
764 |> Repo.update_all([])
766 {1, [user]} -> set_cache(user)
771 def update_note_count(%User{} = user) do
775 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
779 note_count = Repo.one(note_count_query)
781 info_cng = User.Info.set_note_count(user.info, note_count)
785 |> put_embed(:info, info_cng)
786 |> update_and_set_cache()
789 @spec maybe_fetch_follow_information(User.t()) :: User.t()
790 def maybe_fetch_follow_information(user) do
791 with {:ok, user} <- fetch_follow_information(user) do
795 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
801 def fetch_follow_information(user) do
802 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
803 info_cng = User.Info.follow_information_update(user.info, info)
808 |> put_embed(:info, info_cng)
810 update_and_set_cache(changeset)
817 def update_follower_count(%User{} = user) do
818 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
819 follower_count_query =
820 User.Query.build(%{followers: user, deactivated: false})
821 |> select([u], %{count: count(u.id)})
824 |> where(id: ^user.id)
825 |> join(:inner, [u], s in subquery(follower_count_query))
830 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
837 |> Repo.update_all([])
839 {1, [user]} -> set_cache(user)
843 {:ok, maybe_fetch_follow_information(user)}
847 @spec maybe_update_following_count(User.t()) :: User.t()
848 def maybe_update_following_count(%User{local: false} = user) do
849 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
850 maybe_fetch_follow_information(user)
856 def maybe_update_following_count(user), do: user
858 def remove_duplicated_following(%User{following: following} = user) do
859 uniq_following = Enum.uniq(following)
861 if length(following) == length(uniq_following) do
865 |> update_changeset(%{following: uniq_following})
866 |> update_and_set_cache()
870 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
871 def get_users_from_set(ap_ids, local_only \\ true) do
872 criteria = %{ap_id: ap_ids, deactivated: false}
873 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
875 User.Query.build(criteria)
879 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
880 def get_recipients_from_activity(%Activity{recipients: to}) do
881 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
885 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
886 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
890 User.Info.add_to_mutes(info, ap_id)
891 |> User.Info.add_to_muted_notifications(info, ap_id, notifications?)
895 |> put_embed(:info, info_cng)
897 update_and_set_cache(cng)
900 def unmute(muter, %{ap_id: ap_id}) do
904 User.Info.remove_from_mutes(info, ap_id)
905 |> User.Info.remove_from_muted_notifications(info, ap_id)
909 |> put_embed(:info, info_cng)
911 update_and_set_cache(cng)
914 def subscribe(subscriber, %{ap_id: ap_id}) do
915 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
917 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
918 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
921 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
925 |> User.Info.add_to_subscribers(subscriber.ap_id)
928 |> put_embed(:info, info_cng)
929 |> update_and_set_cache()
934 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
935 with %User{} = user <- get_cached_by_ap_id(ap_id) do
938 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
941 |> put_embed(:info, info_cng)
942 |> update_and_set_cache()
946 def block(blocker, %User{ap_id: ap_id} = blocked) do
947 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
949 if following?(blocker, blocked) do
950 {:ok, blocker, _} = unfollow(blocker, blocked)
956 # clear any requested follows as well
958 case CommonAPI.reject_follow_request(blocked, blocker) do
959 {:ok, %User{} = updated_blocked} -> updated_blocked
964 if subscribed_to?(blocked, blocker) do
965 {:ok, blocker} = unsubscribe(blocked, blocker)
971 if following?(blocked, blocker) do
972 unfollow(blocked, blocker)
975 {:ok, blocker} = update_follower_count(blocker)
979 |> User.Info.add_to_block(ap_id)
983 |> put_embed(:info, info_cng)
985 update_and_set_cache(cng)
988 # helper to handle the block given only an actor's AP id
989 def block(blocker, %{ap_id: ap_id}) do
990 block(blocker, get_cached_by_ap_id(ap_id))
993 def unblock(blocker, %{ap_id: ap_id}) do
996 |> User.Info.remove_from_block(ap_id)
1000 |> put_embed(:info, info_cng)
1002 update_and_set_cache(cng)
1005 def mutes?(nil, _), do: false
1006 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
1008 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
1009 def muted_notifications?(nil, _), do: false
1011 def muted_notifications?(user, %{ap_id: ap_id}),
1012 do: Enum.member?(user.info.muted_notifications, ap_id)
1014 def blocks?(%User{} = user, %User{} = target) do
1015 blocks_ap_id?(user, target) || blocks_domain?(user, target)
1018 def blocks?(nil, _), do: false
1020 def blocks_ap_id?(%User{} = user, %User{} = target) do
1021 Enum.member?(user.info.blocks, target.ap_id)
1024 def blocks_ap_id?(_, _), do: false
1026 def blocks_domain?(%User{} = user, %User{} = target) do
1027 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
1028 %{host: host} = URI.parse(target.ap_id)
1029 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1032 def blocks_domain?(_, _), do: false
1034 def subscribed_to?(user, %{ap_id: ap_id}) do
1035 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1036 Enum.member?(target.info.subscribers, user.ap_id)
1040 @spec muted_users(User.t()) :: [User.t()]
1041 def muted_users(user) do
1042 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
1046 @spec blocked_users(User.t()) :: [User.t()]
1047 def blocked_users(user) do
1048 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
1052 @spec subscribers(User.t()) :: [User.t()]
1053 def subscribers(user) do
1054 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
1058 def block_domain(user, domain) do
1061 |> User.Info.add_to_domain_block(domain)
1065 |> put_embed(:info, info_cng)
1067 update_and_set_cache(cng)
1070 def unblock_domain(user, domain) do
1073 |> User.Info.remove_from_domain_block(domain)
1077 |> put_embed(:info, info_cng)
1079 update_and_set_cache(cng)
1082 def deactivate_async(user, status \\ true) do
1083 BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
1086 def deactivate(%User{} = user, status \\ true) do
1087 info_cng = User.Info.set_activation_status(user.info, status)
1089 with {:ok, friends} <- User.get_friends(user),
1090 {:ok, followers} <- User.get_followers(user),
1094 |> put_embed(:info, info_cng)
1095 |> update_and_set_cache() do
1096 Enum.each(followers, &invalidate_cache(&1))
1097 Enum.each(friends, &update_follower_count(&1))
1103 def update_notification_settings(%User{} = user, settings \\ %{}) do
1104 info_changeset = User.Info.update_notification_settings(user.info, settings)
1107 |> put_embed(:info, info_changeset)
1108 |> update_and_set_cache()
1111 def delete(%User{} = user) do
1112 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1115 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1116 def perform(:delete, %User{} = user) do
1117 {:ok, _user} = ActivityPub.delete(user)
1119 # Remove all relationships
1120 {:ok, followers} = User.get_followers(user)
1122 Enum.each(followers, fn follower ->
1123 ActivityPub.unfollow(follower, user)
1124 User.unfollow(follower, user)
1127 {:ok, friends} = User.get_friends(user)
1129 Enum.each(friends, fn followed ->
1130 ActivityPub.unfollow(user, followed)
1131 User.unfollow(user, followed)
1134 delete_user_activities(user)
1135 invalidate_cache(user)
1139 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1140 def perform(:fetch_initial_posts, %User{} = user) do
1141 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1144 # Insert all the posts in reverse order, so they're in the right order on the timeline
1145 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
1146 &Pleroma.Web.Federator.incoming_ap_doc/1
1152 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1154 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1155 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1156 when is_list(blocked_identifiers) do
1158 blocked_identifiers,
1159 fn blocked_identifier ->
1160 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1161 {:ok, blocker} <- block(blocker, blocked),
1162 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1166 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1173 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1174 def perform(:follow_import, %User{} = follower, followed_identifiers)
1175 when is_list(followed_identifiers) do
1177 followed_identifiers,
1178 fn followed_identifier ->
1179 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1180 {:ok, follower} <- maybe_direct_follow(follower, followed),
1181 {:ok, _} <- ActivityPub.follow(follower, followed) do
1185 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1192 @spec external_users_query() :: Ecto.Query.t()
1193 def external_users_query do
1201 @spec external_users(keyword()) :: [User.t()]
1202 def external_users(opts \\ []) do
1204 external_users_query()
1205 |> select([u], struct(u, [:id, :ap_id, :info]))
1209 do: where(query, [u], u.id > ^opts[:max_id]),
1214 do: limit(query, ^opts[:limit]),
1220 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1221 BackgroundWorker.enqueue("blocks_import", %{
1222 "blocker_id" => blocker.id,
1223 "blocked_identifiers" => blocked_identifiers
1227 def follow_import(%User{} = follower, followed_identifiers)
1228 when is_list(followed_identifiers) do
1229 BackgroundWorker.enqueue("follow_import", %{
1230 "follower_id" => follower.id,
1231 "followed_identifiers" => followed_identifiers
1235 def delete_user_activities(%User{ap_id: ap_id} = user) do
1237 |> Activity.Queries.by_actor()
1238 |> RepoStreamer.chunk_stream(50)
1239 |> Stream.each(fn activities ->
1240 Enum.each(activities, &delete_activity(&1))
1247 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1249 |> Object.normalize()
1250 |> ActivityPub.delete()
1253 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1254 user = get_cached_by_ap_id(activity.actor)
1255 object = Object.normalize(activity)
1257 ActivityPub.unlike(user, object)
1260 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1261 user = get_cached_by_ap_id(activity.actor)
1262 object = Object.normalize(activity)
1264 ActivityPub.unannounce(user, object)
1267 defp delete_activity(_activity), do: "Doing nothing"
1269 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1270 Pleroma.HTML.Scrubber.TwitterText
1273 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1275 def fetch_by_ap_id(ap_id) do
1276 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1283 case OStatus.make_user(ap_id) do
1284 {:ok, user} -> {:ok, user}
1285 _ -> {:error, "Could not fetch by AP id"}
1290 def get_or_fetch_by_ap_id(ap_id) do
1291 user = get_cached_by_ap_id(ap_id)
1293 if !is_nil(user) and !User.needs_update?(user) do
1296 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1297 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1299 resp = fetch_by_ap_id(ap_id)
1301 if should_fetch_initial do
1302 with {:ok, %User{} = user} <- resp do
1303 fetch_initial_posts(user)
1311 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1312 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1313 if user = get_cached_by_ap_id(uri) do
1317 %User{info: %User.Info{}}
1318 |> cast(%{}, [:ap_id, :nickname, :local])
1319 |> put_change(:ap_id, uri)
1320 |> put_change(:nickname, nickname)
1321 |> put_change(:local, true)
1322 |> put_change(:follower_address, uri <> "/followers")
1324 {:ok, user} = Repo.insert(changes)
1330 def public_key_from_info(%{
1331 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1335 |> :public_key.pem_decode()
1337 |> :public_key.pem_entry_decode()
1343 def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
1344 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1347 def public_key_from_info(_), do: {:error, "not found key"}
1349 def get_public_key_for_ap_id(ap_id) do
1350 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1351 {:ok, public_key} <- public_key_from_info(user.info) do
1358 defp blank?(""), do: nil
1359 defp blank?(n), do: n
1361 def insert_or_update_user(data) do
1363 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1364 |> remote_user_creation()
1365 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1369 def ap_enabled?(%User{local: true}), do: true
1370 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1371 def ap_enabled?(_), do: false
1373 @doc "Gets or fetch a user by uri or nickname."
1374 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1375 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1376 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1378 # wait a period of time and return newest version of the User structs
1379 # this is because we have synchronous follow APIs and need to simulate them
1380 # with an async handshake
1381 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1382 with %User{} = a <- User.get_cached_by_id(a.id),
1383 %User{} = b <- User.get_cached_by_id(b.id) do
1391 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1392 with :ok <- :timer.sleep(timeout),
1393 %User{} = a <- User.get_cached_by_id(a.id),
1394 %User{} = b <- User.get_cached_by_id(b.id) do
1402 def parse_bio(bio) when is_binary(bio) and bio != "" do
1404 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1408 def parse_bio(_), do: ""
1410 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1411 # TODO: get profile URLs other than user.ap_id
1412 profile_urls = [user.ap_id]
1415 |> CommonUtils.format_input("text/plain",
1416 mentions_format: :full,
1417 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1422 def parse_bio(_, _), do: ""
1424 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1425 Repo.transaction(fn ->
1426 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1430 def tag(nickname, tags) when is_binary(nickname),
1431 do: tag(get_by_nickname(nickname), tags)
1433 def tag(%User{} = user, tags),
1434 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1436 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1437 Repo.transaction(fn ->
1438 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1442 def untag(nickname, tags) when is_binary(nickname),
1443 do: untag(get_by_nickname(nickname), tags)
1445 def untag(%User{} = user, tags),
1446 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1448 defp update_tags(%User{} = user, new_tags) do
1449 {:ok, updated_user} =
1451 |> change(%{tags: new_tags})
1452 |> update_and_set_cache()
1457 defp normalize_tags(tags) do
1460 |> Enum.map(&String.downcase(&1))
1463 defp local_nickname_regex do
1464 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1465 @extended_local_nickname_regex
1467 @strict_local_nickname_regex
1471 def local_nickname(nickname_or_mention) do
1474 |> String.split("@")
1478 def full_nickname(nickname_or_mention),
1479 do: String.trim_leading(nickname_or_mention, "@")
1481 def error_user(ap_id) do
1486 nickname: "erroruser@example.com",
1487 inserted_at: NaiveDateTime.utc_now()
1491 @spec all_superusers() :: [User.t()]
1492 def all_superusers do
1493 User.Query.build(%{super_users: true, local: true, deactivated: false})
1497 def showing_reblogs?(%User{} = user, %User{} = target) do
1498 target.ap_id not in user.info.muted_reblogs
1502 The function returns a query to get users with no activity for given interval of days.
1503 Inactive users are those who didn't read any notification, or had any activity where
1504 the user is the activity's actor, during `inactivity_threshold` days.
1505 Deactivated users will not appear in this list.
1509 iex> Pleroma.User.list_inactive_users()
1512 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1513 def list_inactive_users_query(inactivity_threshold \\ 7) do
1514 negative_inactivity_threshold = -inactivity_threshold
1515 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1516 # Subqueries are not supported in `where` clauses, join gets too complicated.
1517 has_read_notifications =
1518 from(n in Pleroma.Notification,
1519 where: n.seen == true,
1521 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1524 |> Pleroma.Repo.all()
1526 from(u in Pleroma.User,
1527 left_join: a in Pleroma.Activity,
1528 on: u.ap_id == a.actor,
1529 where: not is_nil(u.nickname),
1530 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1531 where: u.id not in ^has_read_notifications,
1534 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1535 is_nil(max(a.inserted_at))
1540 Enable or disable email notifications for user
1544 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1545 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1547 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1548 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1550 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1551 {:ok, t()} | {:error, Ecto.Changeset.t()}
1552 def switch_email_notifications(user, type, status) do
1553 info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})
1556 |> put_embed(:info, info)
1557 |> update_and_set_cache()
1561 Set `last_digest_emailed_at` value for the user to current time
1563 @spec touch_last_digest_emailed_at(t()) :: t()
1564 def touch_last_digest_emailed_at(user) do
1565 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1567 {:ok, updated_user} =
1569 |> change(%{last_digest_emailed_at: now})
1570 |> update_and_set_cache()
1575 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1576 def toggle_confirmation(%User{} = user) do
1577 need_confirmation? = !user.info.confirmation_pending
1580 User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
1584 |> put_embed(:info, info_changeset)
1585 |> update_and_set_cache()
1588 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1592 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1593 # use instance-default
1594 config = Pleroma.Config.get([:assets, :mascots])
1595 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1596 mascot = Keyword.get(config, default_mascot)
1599 "id" => "default-mascot",
1600 "url" => mascot[:url],
1601 "preview_url" => mascot[:url],
1603 "mime_type" => mascot[:mime_type]
1608 def ensure_keys_present(%User{info: info} = user) do
1612 {:ok, pem} = Keys.generate_rsa_pem()
1615 |> Ecto.Changeset.change()
1616 |> Ecto.Changeset.put_embed(:info, User.Info.set_keys(info, pem))
1617 |> update_and_set_cache()
1621 def get_ap_ids_by_nicknames(nicknames) do
1623 where: u.nickname in ^nicknames,
1629 defdelegate search(query, opts \\ []), to: User.Search
1631 defp put_password_hash(
1632 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1634 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1637 defp put_password_hash(changeset), do: changeset
1639 def is_internal_user?(%User{nickname: nil}), do: true
1640 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1641 def is_internal_user?(_), do: false
1643 def change_email(user, email) do
1645 |> cast(%{email: email}, [:email])
1646 |> validate_required([:email])
1647 |> unique_constraint(:email)
1648 |> validate_format(:email, @email_regex)
1649 |> update_and_set_cache()