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, Pleroma.FlakeId, 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)
51 field(:following, {:array, :string}, default: [])
52 field(:ap_id, :string)
54 field(:local, :boolean, default: true)
55 field(:follower_address, :string)
56 field(:following_address, :string)
57 field(:search_rank, :float, virtual: true)
58 field(:search_type, :integer, virtual: true)
59 field(:tags, {:array, :string}, default: [])
60 field(:last_refreshed_at, :naive_datetime_usec)
61 field(:last_digest_emailed_at, :naive_datetime)
62 has_many(:notifications, Notification)
63 has_many(:registrations, Registration)
64 embeds_one(:info, User.Info)
69 def auth_active?(%User{info: %User.Info{confirmation_pending: true}}),
70 do: !Pleroma.Config.get([:instance, :account_activation_required])
72 def auth_active?(%User{}), do: true
74 def visible_for?(user, for_user \\ nil)
76 def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
78 def visible_for?(%User{} = user, for_user) do
79 auth_active?(user) || superuser?(for_user)
82 def visible_for?(_, _), do: false
84 def superuser?(%User{local: true, info: %User.Info{is_admin: true}}), do: true
85 def superuser?(%User{local: true, info: %User.Info{is_moderator: true}}), do: true
86 def superuser?(_), do: false
88 def avatar_url(user, options \\ []) do
90 %{"url" => [%{"href" => href} | _]} -> href
91 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
95 def banner_url(user, options \\ []) do
96 case user.info.banner do
97 %{"url" => [%{"href" => href} | _]} -> href
98 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
102 def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url
103 def profile_url(%User{ap_id: ap_id}), do: ap_id
104 def profile_url(_), do: nil
106 def ap_id(%User{nickname: nickname}) do
107 "#{Web.base_url()}/users/#{nickname}"
110 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
111 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
113 @spec ap_following(User.t()) :: Sring.t()
114 def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
115 def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
117 def user_info(%User{} = user, args \\ %{}) do
119 if args[:following_count],
120 do: args[:following_count],
121 else: user.info.following_count || following_count(user)
124 if args[:follower_count], do: args[:follower_count], else: user.info.follower_count
127 note_count: user.info.note_count,
128 locked: user.info.locked,
129 confirmation_pending: user.info.confirmation_pending,
130 default_scope: user.info.default_scope
132 |> Map.put(:following_count, following_count)
133 |> Map.put(:follower_count, follower_count)
136 def follow_state(%User{} = user, %User{} = target) do
137 follow_activity = Utils.fetch_latest_follow(user, target)
140 do: follow_activity.data["state"],
141 # Ideally this would be nil, but then Cachex does not commit the value
145 def get_cached_follow_state(user, target) do
146 key = "follow_state:#{user.ap_id}|#{target.ap_id}"
147 Cachex.fetch!(:user_cache, key, fn _ -> {:commit, follow_state(user, target)} end)
150 def set_follow_state_cache(user_ap_id, target_ap_id, state) do
153 "follow_state:#{user_ap_id}|#{target_ap_id}",
158 def set_info_cache(user, args) do
159 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user, args))
162 @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
163 def restrict_deactivated(query) do
165 where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info)
169 def following_count(%User{following: []}), do: 0
171 def following_count(%User{} = user) do
173 |> get_friends_query()
174 |> Repo.aggregate(:count, :id)
177 def remote_user_creation(params) do
178 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
179 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
181 params = Map.put(params, :info, params[:info] || %{})
182 info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])
186 |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
187 |> validate_required([:name, :ap_id])
188 |> unique_constraint(:nickname)
189 |> validate_format(:nickname, @email_regex)
190 |> validate_length(:bio, max: bio_limit)
191 |> validate_length(:name, max: name_limit)
192 |> put_change(:local, false)
193 |> put_embed(:info, info_cng)
196 case info_cng.changes[:source_data] do
197 %{"followers" => followers, "following" => following} ->
199 |> put_change(:follower_address, followers)
200 |> put_change(:following_address, following)
203 followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
206 |> put_change(:follower_address, followers)
213 def update_changeset(struct, params \\ %{}) do
214 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
215 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
218 |> cast(params, [:bio, :name, :avatar, :following])
219 |> unique_constraint(:nickname)
220 |> validate_format(:nickname, local_nickname_regex())
221 |> validate_length(:bio, max: bio_limit)
222 |> validate_length(:name, min: 1, max: name_limit)
225 def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
226 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
227 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
229 params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
230 info_cng = User.Info.user_upgrade(struct.info, params[:info], remote?)
241 |> unique_constraint(:nickname)
242 |> validate_format(:nickname, local_nickname_regex())
243 |> validate_length(:bio, max: bio_limit)
244 |> validate_length(:name, max: name_limit)
245 |> put_embed(:info, info_cng)
248 def password_update_changeset(struct, params) do
250 |> cast(params, [:password, :password_confirmation])
251 |> validate_required([:password, :password_confirmation])
252 |> validate_confirmation(:password)
256 @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
257 def reset_password(%User{id: user_id} = user, data) do
260 |> Multi.update(:user, password_update_changeset(user, data))
261 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
262 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
264 case Repo.transaction(multi) do
265 {:ok, %{user: user} = _} -> set_cache(user)
266 {:error, _, changeset, _} -> {:error, changeset}
270 def register_changeset(struct, params \\ %{}, opts \\ []) do
271 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
272 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
275 if is_nil(opts[:need_confirmation]) do
276 Pleroma.Config.get([:instance, :account_activation_required])
278 opts[:need_confirmation]
282 User.Info.confirmation_changeset(%User.Info{}, need_confirmation: need_confirmation?)
286 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
287 |> validate_required([:name, :nickname, :password, :password_confirmation])
288 |> validate_confirmation(:password)
289 |> unique_constraint(:email)
290 |> unique_constraint(:nickname)
291 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
292 |> validate_format(:nickname, local_nickname_regex())
293 |> validate_format(:email, @email_regex)
294 |> validate_length(:bio, max: bio_limit)
295 |> validate_length(:name, min: 1, max: name_limit)
296 |> put_change(:info, info_change)
299 if opts[:external] do
302 validate_required(changeset, [:email])
305 if changeset.valid? do
306 ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
307 followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
311 |> put_change(:ap_id, ap_id)
312 |> unique_constraint(:ap_id)
313 |> put_change(:following, [followers])
314 |> put_change(:follower_address, followers)
320 defp autofollow_users(user) do
321 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
324 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
327 follow_all(user, autofollowed_users)
330 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
331 def register(%Ecto.Changeset{} = changeset) do
332 with {:ok, user} <- Repo.insert(changeset),
333 {:ok, user} <- post_register_action(user) do
338 def post_register_action(%User{} = user) do
339 with {:ok, user} <- autofollow_users(user),
340 {:ok, user} <- set_cache(user),
341 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
342 {:ok, _} <- try_send_confirmation_email(user) do
347 def try_send_confirmation_email(%User{} = user) do
348 if user.info.confirmation_pending &&
349 Pleroma.Config.get([:instance, :account_activation_required]) do
351 |> Pleroma.Emails.UserEmail.account_confirmation_email()
352 |> Pleroma.Emails.Mailer.deliver_async()
360 def needs_update?(%User{local: true}), do: false
362 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
364 def needs_update?(%User{local: false} = user) do
365 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
368 def needs_update?(_), do: true
370 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
371 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
375 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
376 follow(follower, followed)
379 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
380 if not User.ap_enabled?(followed) do
381 follow(follower, followed)
387 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
388 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
389 def follow_all(follower, followeds) do
392 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
393 |> Enum.map(fn %{follower_address: fa} -> fa end)
397 where: u.id == ^follower.id,
402 "array(select distinct unnest (array_cat(?, ?)))",
411 {1, [follower]} = Repo.update_all(q, [])
413 Enum.each(followeds, fn followed ->
414 update_follower_count(followed)
420 def follow(%User{} = follower, %User{info: info} = followed) do
421 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
422 ap_followers = followed.follower_address
426 {:error, "Could not follow user: You are deactivated."}
428 deny_follow_blocked and blocks?(followed, follower) ->
429 {:error, "Could not follow user: #{followed.nickname} blocked you."}
432 if !followed.local && follower.local && !ap_enabled?(followed) do
433 Websub.subscribe(follower, followed)
438 where: u.id == ^follower.id,
439 update: [push: [following: ^ap_followers]],
443 {1, [follower]} = Repo.update_all(q, [])
445 follower = maybe_update_following_count(follower)
447 {:ok, _} = update_follower_count(followed)
453 def unfollow(%User{} = follower, %User{} = followed) do
454 ap_followers = followed.follower_address
456 if following?(follower, followed) and follower.ap_id != followed.ap_id do
459 where: u.id == ^follower.id,
460 update: [pull: [following: ^ap_followers]],
464 {1, [follower]} = Repo.update_all(q, [])
466 follower = maybe_update_following_count(follower)
468 {:ok, followed} = update_follower_count(followed)
472 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
474 {:error, "Not subscribed!"}
478 @spec following?(User.t(), User.t()) :: boolean
479 def following?(%User{} = follower, %User{} = followed) do
480 Enum.member?(follower.following, followed.follower_address)
483 def locked?(%User{} = user) do
484 user.info.locked || false
488 Repo.get_by(User, id: id)
491 def get_by_ap_id(ap_id) do
492 Repo.get_by(User, ap_id: ap_id)
495 def get_all_by_ap_id(ap_ids) do
496 from(u in __MODULE__,
497 where: u.ap_id in ^ap_ids
502 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
503 # of the ap_id and the domain and tries to get that user
504 def get_by_guessed_nickname(ap_id) do
505 domain = URI.parse(ap_id).host
506 name = List.last(String.split(ap_id, "/"))
507 nickname = "#{name}@#{domain}"
509 get_cached_by_nickname(nickname)
512 def set_cache({:ok, user}), do: set_cache(user)
513 def set_cache({:error, err}), do: {:error, err}
515 def set_cache(%User{} = user) do
516 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
517 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
518 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
522 def update_and_set_cache(changeset) do
523 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
530 def invalidate_cache(user) do
531 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
532 Cachex.del(:user_cache, "nickname:#{user.nickname}")
533 Cachex.del(:user_cache, "user_info:#{user.id}")
536 def get_cached_by_ap_id(ap_id) do
537 key = "ap_id:#{ap_id}"
538 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
541 def get_cached_by_id(id) do
545 Cachex.fetch!(:user_cache, key, fn _ ->
549 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
550 {:commit, user.ap_id}
556 get_cached_by_ap_id(ap_id)
559 def get_cached_by_nickname(nickname) do
560 key = "nickname:#{nickname}"
562 Cachex.fetch!(:user_cache, key, fn ->
563 user_result = get_or_fetch_by_nickname(nickname)
566 {:ok, user} -> {:commit, user}
567 {:error, _error} -> {:ignore, nil}
572 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
573 if is_integer(nickname_or_id) or Pleroma.FlakeId.is_flake_id?(nickname_or_id) do
574 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
576 unless opts[:restrict_remote_nicknames], do: get_cached_by_nickname(nickname_or_id)
580 def get_by_nickname(nickname) do
581 Repo.get_by(User, nickname: nickname) ||
582 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
583 Repo.get_by(User, nickname: local_nickname(nickname))
587 def get_by_email(email), do: Repo.get_by(User, email: email)
589 def get_by_nickname_or_email(nickname_or_email) do
590 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
593 def get_cached_user_info(user) do
594 key = "user_info:#{user.id}"
595 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
598 def fetch_by_nickname(nickname) do
599 ap_try = ActivityPub.make_user_from_nickname(nickname)
602 {:ok, user} -> {:ok, user}
603 _ -> OStatus.make_user(nickname)
607 def get_or_fetch_by_nickname(nickname) do
608 with %User{} = user <- get_by_nickname(nickname) do
612 with [_nick, _domain] <- String.split(nickname, "@"),
613 {:ok, user} <- fetch_by_nickname(nickname) do
614 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
615 fetch_initial_posts(user)
620 _e -> {:error, "not found " <> nickname}
625 @doc "Fetch some posts when the user has just been federated with"
626 def fetch_initial_posts(user),
627 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:fetch_initial_posts, user])
629 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
630 def get_followers_query(%User{} = user, nil) do
631 User.Query.build(%{followers: user, deactivated: false})
634 def get_followers_query(user, page) do
635 from(u in get_followers_query(user, nil))
636 |> User.Query.paginate(page, 20)
639 @spec get_followers_query(User.t()) :: Ecto.Query.t()
640 def get_followers_query(user), do: get_followers_query(user, nil)
642 @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
643 def get_followers(user, page \\ nil) do
644 q = get_followers_query(user, page)
649 @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
650 def get_external_followers(user, page \\ nil) do
653 |> get_followers_query(page)
654 |> User.Query.build(%{external: true})
659 def get_followers_ids(user, page \\ nil) do
660 q = get_followers_query(user, page)
662 Repo.all(from(u in q, select: u.id))
665 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
666 def get_friends_query(%User{} = user, nil) do
667 User.Query.build(%{friends: user, deactivated: false})
670 def get_friends_query(user, page) do
671 from(u in get_friends_query(user, nil))
672 |> User.Query.paginate(page, 20)
675 @spec get_friends_query(User.t()) :: Ecto.Query.t()
676 def get_friends_query(user), do: get_friends_query(user, nil)
678 def get_friends(user, page \\ nil) do
679 q = get_friends_query(user, page)
684 def get_friends_ids(user, page \\ nil) do
685 q = get_friends_query(user, page)
687 Repo.all(from(u in q, select: u.id))
690 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
691 def get_follow_requests(%User{} = user) do
693 Activity.follow_requests_for_actor(user)
694 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
695 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
696 |> group_by([a, u], u.id)
703 def increase_note_count(%User{} = user) do
705 |> where(id: ^user.id)
710 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
717 |> Repo.update_all([])
719 {1, [user]} -> set_cache(user)
724 def decrease_note_count(%User{} = user) do
726 |> where(id: ^user.id)
731 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
738 |> Repo.update_all([])
740 {1, [user]} -> set_cache(user)
745 def update_note_count(%User{} = user) do
749 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
753 note_count = Repo.one(note_count_query)
755 info_cng = User.Info.set_note_count(user.info, note_count)
759 |> put_embed(:info, info_cng)
760 |> update_and_set_cache()
763 @spec maybe_fetch_follow_information(User.t()) :: User.t()
764 def maybe_fetch_follow_information(user) do
765 with {:ok, user} <- fetch_follow_information(user) do
769 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
775 def fetch_follow_information(user) do
776 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
777 info_cng = User.Info.follow_information_update(user.info, info)
782 |> put_embed(:info, info_cng)
784 update_and_set_cache(changeset)
791 def update_follower_count(%User{} = user) do
792 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
793 follower_count_query =
794 User.Query.build(%{followers: user, deactivated: false})
795 |> select([u], %{count: count(u.id)})
798 |> where(id: ^user.id)
799 |> join(:inner, [u], s in subquery(follower_count_query))
804 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
811 |> Repo.update_all([])
813 {1, [user]} -> set_cache(user)
817 {:ok, maybe_fetch_follow_information(user)}
821 @spec maybe_update_following_count(User.t()) :: User.t()
822 def maybe_update_following_count(%User{local: false} = user) do
823 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
824 maybe_fetch_follow_information(user)
830 def maybe_update_following_count(user), do: user
832 def remove_duplicated_following(%User{following: following} = user) do
833 uniq_following = Enum.uniq(following)
835 if length(following) == length(uniq_following) do
839 |> update_changeset(%{following: uniq_following})
840 |> update_and_set_cache()
844 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
845 def get_users_from_set(ap_ids, local_only \\ true) do
846 criteria = %{ap_id: ap_ids, deactivated: false}
847 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
849 User.Query.build(criteria)
853 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
854 def get_recipients_from_activity(%Activity{recipients: to}) do
855 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
859 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
860 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
864 User.Info.add_to_mutes(info, ap_id)
865 |> User.Info.add_to_muted_notifications(info, ap_id, notifications?)
869 |> put_embed(:info, info_cng)
871 update_and_set_cache(cng)
874 def unmute(muter, %{ap_id: ap_id}) do
878 User.Info.remove_from_mutes(info, ap_id)
879 |> User.Info.remove_from_muted_notifications(info, ap_id)
883 |> put_embed(:info, info_cng)
885 update_and_set_cache(cng)
888 def subscribe(subscriber, %{ap_id: ap_id}) do
889 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
891 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
892 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
895 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
899 |> User.Info.add_to_subscribers(subscriber.ap_id)
902 |> put_embed(:info, info_cng)
903 |> update_and_set_cache()
908 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
909 with %User{} = user <- get_cached_by_ap_id(ap_id) do
912 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
915 |> put_embed(:info, info_cng)
916 |> update_and_set_cache()
920 def block(blocker, %User{ap_id: ap_id} = blocked) do
921 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
923 if following?(blocker, blocked) do
924 {:ok, blocker, _} = unfollow(blocker, blocked)
930 # clear any requested follows as well
932 case CommonAPI.reject_follow_request(blocked, blocker) do
933 {:ok, %User{} = updated_blocked} -> updated_blocked
938 if subscribed_to?(blocked, blocker) do
939 {:ok, blocker} = unsubscribe(blocked, blocker)
945 if following?(blocked, blocker) do
946 unfollow(blocked, blocker)
949 {:ok, blocker} = update_follower_count(blocker)
953 |> User.Info.add_to_block(ap_id)
957 |> put_embed(:info, info_cng)
959 update_and_set_cache(cng)
962 # helper to handle the block given only an actor's AP id
963 def block(blocker, %{ap_id: ap_id}) do
964 block(blocker, get_cached_by_ap_id(ap_id))
967 def unblock(blocker, %{ap_id: ap_id}) do
970 |> User.Info.remove_from_block(ap_id)
974 |> put_embed(:info, info_cng)
976 update_and_set_cache(cng)
979 def mutes?(nil, _), do: false
980 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
982 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
983 def muted_notifications?(nil, _), do: false
985 def muted_notifications?(user, %{ap_id: ap_id}),
986 do: Enum.member?(user.info.muted_notifications, ap_id)
988 def blocks?(%User{} = user, %User{} = target) do
989 blocks_ap_id?(user, target) || blocks_domain?(user, target)
992 def blocks?(nil, _), do: false
994 def blocks_ap_id?(%User{} = user, %User{} = target) do
995 Enum.member?(user.info.blocks, target.ap_id)
998 def blocks_ap_id?(_, _), do: false
1000 def blocks_domain?(%User{} = user, %User{} = target) do
1001 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
1002 %{host: host} = URI.parse(target.ap_id)
1003 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1006 def blocks_domain?(_, _), do: false
1008 def subscribed_to?(user, %{ap_id: ap_id}) do
1009 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1010 Enum.member?(target.info.subscribers, user.ap_id)
1014 @spec muted_users(User.t()) :: [User.t()]
1015 def muted_users(user) do
1016 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
1020 @spec blocked_users(User.t()) :: [User.t()]
1021 def blocked_users(user) do
1022 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
1026 @spec subscribers(User.t()) :: [User.t()]
1027 def subscribers(user) do
1028 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
1032 def block_domain(user, domain) do
1035 |> User.Info.add_to_domain_block(domain)
1039 |> put_embed(:info, info_cng)
1041 update_and_set_cache(cng)
1044 def unblock_domain(user, domain) do
1047 |> User.Info.remove_from_domain_block(domain)
1051 |> put_embed(:info, info_cng)
1053 update_and_set_cache(cng)
1056 def deactivate_async(user, status \\ true) do
1057 PleromaJobQueue.enqueue(:background, __MODULE__, [:deactivate_async, user, status])
1060 def deactivate(%User{} = user, status \\ true) do
1061 info_cng = User.Info.set_activation_status(user.info, status)
1063 with {:ok, friends} <- User.get_friends(user),
1064 {:ok, followers} <- User.get_followers(user),
1068 |> put_embed(:info, info_cng)
1069 |> update_and_set_cache() do
1070 Enum.each(followers, &invalidate_cache(&1))
1071 Enum.each(friends, &update_follower_count(&1))
1077 def update_notification_settings(%User{} = user, settings \\ %{}) do
1078 info_changeset = User.Info.update_notification_settings(user.info, settings)
1081 |> put_embed(:info, info_changeset)
1082 |> update_and_set_cache()
1085 @spec delete(User.t()) :: :ok
1086 def delete(%User{} = user),
1087 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:delete, user])
1089 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1090 def perform(:delete, %User{} = user) do
1091 {:ok, _user} = ActivityPub.delete(user)
1093 # Remove all relationships
1094 {:ok, followers} = User.get_followers(user)
1096 Enum.each(followers, fn follower ->
1097 ActivityPub.unfollow(follower, user)
1098 User.unfollow(follower, user)
1101 {:ok, friends} = User.get_friends(user)
1103 Enum.each(friends, fn followed ->
1104 ActivityPub.unfollow(user, followed)
1105 User.unfollow(user, followed)
1108 delete_user_activities(user)
1109 invalidate_cache(user)
1113 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1114 def perform(:fetch_initial_posts, %User{} = user) do
1115 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1118 # Insert all the posts in reverse order, so they're in the right order on the timeline
1119 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
1120 &Pleroma.Web.Federator.incoming_ap_doc/1
1126 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1128 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1129 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1130 when is_list(blocked_identifiers) do
1132 blocked_identifiers,
1133 fn blocked_identifier ->
1134 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1135 {:ok, blocker} <- block(blocker, blocked),
1136 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1140 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1147 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1148 def perform(:follow_import, %User{} = follower, followed_identifiers)
1149 when is_list(followed_identifiers) do
1151 followed_identifiers,
1152 fn followed_identifier ->
1153 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1154 {:ok, follower} <- maybe_direct_follow(follower, followed),
1155 {:ok, _} <- ActivityPub.follow(follower, followed) do
1159 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1166 @spec external_users_query() :: Ecto.Query.t()
1167 def external_users_query do
1175 @spec external_users(keyword()) :: [User.t()]
1176 def external_users(opts \\ []) do
1178 external_users_query()
1179 |> select([u], struct(u, [:id, :ap_id, :info]))
1183 do: where(query, [u], u.id > ^opts[:max_id]),
1188 do: limit(query, ^opts[:limit]),
1194 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers),
1196 PleromaJobQueue.enqueue(:background, __MODULE__, [
1202 def follow_import(%User{} = follower, followed_identifiers) when is_list(followed_identifiers),
1204 PleromaJobQueue.enqueue(:background, __MODULE__, [
1207 followed_identifiers
1210 def delete_user_activities(%User{ap_id: ap_id} = user) do
1212 |> Activity.query_by_actor()
1213 |> RepoStreamer.chunk_stream(50)
1214 |> Stream.each(fn activities ->
1215 Enum.each(activities, &delete_activity(&1))
1222 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1224 |> Object.normalize()
1225 |> ActivityPub.delete()
1228 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1229 user = get_cached_by_ap_id(activity.actor)
1230 object = Object.normalize(activity)
1232 ActivityPub.unlike(user, object)
1235 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1236 user = get_cached_by_ap_id(activity.actor)
1237 object = Object.normalize(activity)
1239 ActivityPub.unannounce(user, object)
1242 defp delete_activity(_activity), do: "Doing nothing"
1244 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1245 Pleroma.HTML.Scrubber.TwitterText
1248 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1250 def fetch_by_ap_id(ap_id) do
1251 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1258 case OStatus.make_user(ap_id) do
1259 {:ok, user} -> {:ok, user}
1260 _ -> {:error, "Could not fetch by AP id"}
1265 def get_or_fetch_by_ap_id(ap_id) do
1266 user = get_cached_by_ap_id(ap_id)
1268 if !is_nil(user) and !User.needs_update?(user) do
1271 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1272 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1274 resp = fetch_by_ap_id(ap_id)
1276 if should_fetch_initial do
1277 with {:ok, %User{} = user} <- resp do
1278 fetch_initial_posts(user)
1286 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1287 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1288 if user = get_cached_by_ap_id(uri) do
1292 %User{info: %User.Info{}}
1293 |> cast(%{}, [:ap_id, :nickname, :local])
1294 |> put_change(:ap_id, uri)
1295 |> put_change(:nickname, nickname)
1296 |> put_change(:local, true)
1297 |> put_change(:follower_address, uri <> "/followers")
1299 {:ok, user} = Repo.insert(changes)
1305 def public_key_from_info(%{
1306 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1310 |> :public_key.pem_decode()
1312 |> :public_key.pem_entry_decode()
1318 def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
1319 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1322 def public_key_from_info(_), do: {:error, "not found key"}
1324 def get_public_key_for_ap_id(ap_id) do
1325 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1326 {:ok, public_key} <- public_key_from_info(user.info) do
1333 defp blank?(""), do: nil
1334 defp blank?(n), do: n
1336 def insert_or_update_user(data) do
1338 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1339 |> remote_user_creation()
1340 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1344 def ap_enabled?(%User{local: true}), do: true
1345 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1346 def ap_enabled?(_), do: false
1348 @doc "Gets or fetch a user by uri or nickname."
1349 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1350 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1351 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1353 # wait a period of time and return newest version of the User structs
1354 # this is because we have synchronous follow APIs and need to simulate them
1355 # with an async handshake
1356 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1357 with %User{} = a <- User.get_cached_by_id(a.id),
1358 %User{} = b <- User.get_cached_by_id(b.id) do
1366 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1367 with :ok <- :timer.sleep(timeout),
1368 %User{} = a <- User.get_cached_by_id(a.id),
1369 %User{} = b <- User.get_cached_by_id(b.id) do
1377 def parse_bio(bio) when is_binary(bio) and bio != "" do
1379 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1383 def parse_bio(_), do: ""
1385 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1386 # TODO: get profile URLs other than user.ap_id
1387 profile_urls = [user.ap_id]
1390 |> CommonUtils.format_input("text/plain",
1391 mentions_format: :full,
1392 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1397 def parse_bio(_, _), do: ""
1399 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1400 Repo.transaction(fn ->
1401 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1405 def tag(nickname, tags) when is_binary(nickname),
1406 do: tag(get_by_nickname(nickname), tags)
1408 def tag(%User{} = user, tags),
1409 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1411 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1412 Repo.transaction(fn ->
1413 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1417 def untag(nickname, tags) when is_binary(nickname),
1418 do: untag(get_by_nickname(nickname), tags)
1420 def untag(%User{} = user, tags),
1421 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1423 defp update_tags(%User{} = user, new_tags) do
1424 {:ok, updated_user} =
1426 |> change(%{tags: new_tags})
1427 |> update_and_set_cache()
1432 defp normalize_tags(tags) do
1435 |> Enum.map(&String.downcase(&1))
1438 defp local_nickname_regex do
1439 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1440 @extended_local_nickname_regex
1442 @strict_local_nickname_regex
1446 def local_nickname(nickname_or_mention) do
1449 |> String.split("@")
1453 def full_nickname(nickname_or_mention),
1454 do: String.trim_leading(nickname_or_mention, "@")
1456 def error_user(ap_id) do
1461 nickname: "erroruser@example.com",
1462 inserted_at: NaiveDateTime.utc_now()
1466 @spec all_superusers() :: [User.t()]
1467 def all_superusers do
1468 User.Query.build(%{super_users: true, local: true, deactivated: false})
1472 def showing_reblogs?(%User{} = user, %User{} = target) do
1473 target.ap_id not in user.info.muted_reblogs
1477 The function returns a query to get users with no activity for given interval of days.
1478 Inactive users are those who didn't read any notification, or had any activity where
1479 the user is the activity's actor, during `inactivity_threshold` days.
1480 Deactivated users will not appear in this list.
1484 iex> Pleroma.User.list_inactive_users()
1487 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1488 def list_inactive_users_query(inactivity_threshold \\ 7) do
1489 negative_inactivity_threshold = -inactivity_threshold
1490 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1491 # Subqueries are not supported in `where` clauses, join gets too complicated.
1492 has_read_notifications =
1493 from(n in Pleroma.Notification,
1494 where: n.seen == true,
1496 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1499 |> Pleroma.Repo.all()
1501 from(u in Pleroma.User,
1502 left_join: a in Pleroma.Activity,
1503 on: u.ap_id == a.actor,
1504 where: not is_nil(u.nickname),
1505 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1506 where: u.id not in ^has_read_notifications,
1509 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1510 is_nil(max(a.inserted_at))
1515 Enable or disable email notifications for user
1519 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1520 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1522 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1523 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1525 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1526 {:ok, t()} | {:error, Ecto.Changeset.t()}
1527 def switch_email_notifications(user, type, status) do
1528 info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})
1531 |> put_embed(:info, info)
1532 |> update_and_set_cache()
1536 Set `last_digest_emailed_at` value for the user to current time
1538 @spec touch_last_digest_emailed_at(t()) :: t()
1539 def touch_last_digest_emailed_at(user) do
1540 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1542 {:ok, updated_user} =
1544 |> change(%{last_digest_emailed_at: now})
1545 |> update_and_set_cache()
1550 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1551 def toggle_confirmation(%User{} = user) do
1552 need_confirmation? = !user.info.confirmation_pending
1555 User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
1559 |> put_embed(:info, info_changeset)
1560 |> update_and_set_cache()
1563 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1567 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1568 # use instance-default
1569 config = Pleroma.Config.get([:assets, :mascots])
1570 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1571 mascot = Keyword.get(config, default_mascot)
1574 "id" => "default-mascot",
1575 "url" => mascot[:url],
1576 "preview_url" => mascot[:url],
1578 "mime_type" => mascot[:mime_type]
1583 def ensure_keys_present(%User{info: info} = user) do
1587 {:ok, pem} = Keys.generate_rsa_pem()
1590 |> Ecto.Changeset.change()
1591 |> Ecto.Changeset.put_embed(:info, User.Info.set_keys(info, pem))
1592 |> update_and_set_cache()
1596 def get_ap_ids_by_nicknames(nicknames) do
1598 where: u.nickname in ^nicknames,
1604 defdelegate search(query, opts \\ []), to: User.Search
1606 defp put_password_hash(
1607 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1609 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1612 defp put_password_hash(changeset), do: changeset
1614 def is_internal_user?(%User{nickname: nil}), do: true
1615 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1616 def is_internal_user?(_), do: false