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.Utils, as: CommonUtils
25 alias Pleroma.Web.OAuth
26 alias Pleroma.Web.OStatus
27 alias Pleroma.Web.RelMe
28 alias Pleroma.Web.Websub
32 @type t :: %__MODULE__{}
34 @primary_key {:id, Pleroma.FlakeId, autogenerate: true}
36 # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
37 @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])?)*$/
39 @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
40 @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
44 field(:email, :string)
46 field(:nickname, :string)
47 field(:password_hash, :string)
48 field(:password, :string, virtual: true)
49 field(:password_confirmation, :string, virtual: true)
50 field(:following, {:array, :string}, default: [])
51 field(:ap_id, :string)
53 field(:local, :boolean, default: true)
54 field(:follower_address, :string)
55 field(:following_address, :string)
56 field(:search_rank, :float, virtual: true)
57 field(:search_type, :integer, virtual: true)
58 field(:tags, {:array, :string}, default: [])
59 field(:last_refreshed_at, :naive_datetime_usec)
60 field(:last_digest_emailed_at, :naive_datetime)
61 has_many(:notifications, Notification)
62 has_many(:registrations, Registration)
63 embeds_one(:info, User.Info)
68 def auth_active?(%User{info: %User.Info{confirmation_pending: true}}),
69 do: !Pleroma.Config.get([:instance, :account_activation_required])
71 def auth_active?(%User{}), do: true
73 def visible_for?(user, for_user \\ nil)
75 def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
77 def visible_for?(%User{} = user, for_user) do
78 auth_active?(user) || superuser?(for_user)
81 def visible_for?(_, _), do: false
83 def superuser?(%User{local: true, info: %User.Info{is_admin: true}}), do: true
84 def superuser?(%User{local: true, info: %User.Info{is_moderator: true}}), do: true
85 def superuser?(_), do: false
87 def avatar_url(user, options \\ []) do
89 %{"url" => [%{"href" => href} | _]} -> href
90 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
94 def banner_url(user, options \\ []) do
95 case user.info.banner do
96 %{"url" => [%{"href" => href} | _]} -> href
97 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
101 def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url
102 def profile_url(%User{ap_id: ap_id}), do: ap_id
103 def profile_url(_), do: nil
105 def ap_id(%User{nickname: nickname}) do
106 "#{Web.base_url()}/users/#{nickname}"
109 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
110 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
112 @spec ap_following(User.t()) :: Sring.t()
113 def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
114 def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
116 def user_info(%User{} = user, args \\ %{}) do
118 if args[:following_count],
119 do: args[:following_count],
120 else: user.info.following_count || following_count(user)
123 if args[:follower_count], do: args[:follower_count], else: user.info.follower_count
126 note_count: user.info.note_count,
127 locked: user.info.locked,
128 confirmation_pending: user.info.confirmation_pending,
129 default_scope: user.info.default_scope
131 |> Map.put(:following_count, following_count)
132 |> Map.put(:follower_count, follower_count)
135 def follow_state(%User{} = user, %User{} = target) do
136 follow_activity = Utils.fetch_latest_follow(user, target)
139 do: follow_activity.data["state"],
140 # Ideally this would be nil, but then Cachex does not commit the value
144 def get_cached_follow_state(user, target) do
145 key = "follow_state:#{user.ap_id}|#{target.ap_id}"
146 Cachex.fetch!(:user_cache, key, fn _ -> {:commit, follow_state(user, target)} end)
149 def set_follow_state_cache(user_ap_id, target_ap_id, state) do
152 "follow_state:#{user_ap_id}|#{target_ap_id}",
157 def set_info_cache(user, args) do
158 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user, args))
161 @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
162 def restrict_deactivated(query) do
164 where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info)
168 def following_count(%User{following: []}), do: 0
170 def following_count(%User{} = user) do
172 |> get_friends_query()
173 |> Repo.aggregate(:count, :id)
176 def remote_user_creation(params) do
177 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
178 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
180 params = Map.put(params, :info, params[:info] || %{})
181 info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])
185 |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
186 |> validate_required([:name, :ap_id])
187 |> unique_constraint(:nickname)
188 |> validate_format(:nickname, @email_regex)
189 |> validate_length(:bio, max: bio_limit)
190 |> validate_length(:name, max: name_limit)
191 |> put_change(:local, false)
192 |> put_embed(:info, info_cng)
195 case info_cng.changes[:source_data] do
196 %{"followers" => followers, "following" => following} ->
198 |> put_change(:follower_address, followers)
199 |> put_change(:following_address, following)
202 followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
205 |> put_change(:follower_address, followers)
212 def update_changeset(struct, params \\ %{}) do
213 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
214 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
217 |> cast(params, [:bio, :name, :avatar, :following])
218 |> unique_constraint(:nickname)
219 |> validate_format(:nickname, local_nickname_regex())
220 |> validate_length(:bio, max: bio_limit)
221 |> validate_length(:name, min: 1, max: name_limit)
224 def upgrade_changeset(struct, params \\ %{}) do
225 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
226 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
228 params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
229 info_cng = User.Info.user_upgrade(struct.info, params[:info])
240 |> unique_constraint(:nickname)
241 |> validate_format(:nickname, local_nickname_regex())
242 |> validate_length(:bio, max: bio_limit)
243 |> validate_length(:name, max: name_limit)
244 |> put_embed(:info, info_cng)
247 def password_update_changeset(struct, params) do
249 |> cast(params, [:password, :password_confirmation])
250 |> validate_required([:password, :password_confirmation])
251 |> validate_confirmation(:password)
255 @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
256 def reset_password(%User{id: user_id} = user, data) do
259 |> Multi.update(:user, password_update_changeset(user, data))
260 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
261 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
263 case Repo.transaction(multi) do
264 {:ok, %{user: user} = _} -> set_cache(user)
265 {:error, _, changeset, _} -> {:error, changeset}
269 def register_changeset(struct, params \\ %{}, opts \\ []) do
270 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
271 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
274 if is_nil(opts[:need_confirmation]) do
275 Pleroma.Config.get([:instance, :account_activation_required])
277 opts[:need_confirmation]
281 User.Info.confirmation_changeset(%User.Info{}, need_confirmation: need_confirmation?)
285 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
286 |> validate_required([:name, :nickname, :password, :password_confirmation])
287 |> validate_confirmation(:password)
288 |> unique_constraint(:email)
289 |> unique_constraint(:nickname)
290 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
291 |> validate_format(:nickname, local_nickname_regex())
292 |> validate_format(:email, @email_regex)
293 |> validate_length(:bio, max: bio_limit)
294 |> validate_length(:name, min: 1, max: name_limit)
295 |> put_change(:info, info_change)
298 if opts[:external] do
301 validate_required(changeset, [:email])
304 if changeset.valid? do
305 ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
306 followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
310 |> put_change(:ap_id, ap_id)
311 |> unique_constraint(:ap_id)
312 |> put_change(:following, [followers])
313 |> put_change(:follower_address, followers)
319 defp autofollow_users(user) do
320 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
323 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
326 follow_all(user, autofollowed_users)
329 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
330 def register(%Ecto.Changeset{} = changeset) do
331 with {:ok, user} <- Repo.insert(changeset),
332 {:ok, user} <- autofollow_users(user),
333 {:ok, user} <- set_cache(user),
334 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
335 {:ok, _} <- try_send_confirmation_email(user) do
340 def try_send_confirmation_email(%User{} = user) do
341 if user.info.confirmation_pending &&
342 Pleroma.Config.get([:instance, :account_activation_required]) do
344 |> Pleroma.Emails.UserEmail.account_confirmation_email()
345 |> Pleroma.Emails.Mailer.deliver_async()
353 def needs_update?(%User{local: true}), do: false
355 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
357 def needs_update?(%User{local: false} = user) do
358 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
361 def needs_update?(_), do: true
363 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
364 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
368 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
369 follow(follower, followed)
372 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
373 if not User.ap_enabled?(followed) do
374 follow(follower, followed)
380 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
381 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
382 def follow_all(follower, followeds) do
385 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
386 |> Enum.map(fn %{follower_address: fa} -> fa end)
390 where: u.id == ^follower.id,
395 "array(select distinct unnest (array_cat(?, ?)))",
404 {1, [follower]} = Repo.update_all(q, [])
406 Enum.each(followeds, fn followed ->
407 update_follower_count(followed)
413 def follow(%User{} = follower, %User{info: info} = followed) do
414 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
415 ap_followers = followed.follower_address
419 {:error, "Could not follow user: You are deactivated."}
421 deny_follow_blocked and blocks?(followed, follower) ->
422 {:error, "Could not follow user: #{followed.nickname} blocked you."}
425 if !followed.local && follower.local && !ap_enabled?(followed) do
426 Websub.subscribe(follower, followed)
431 where: u.id == ^follower.id,
432 update: [push: [following: ^ap_followers]],
436 {1, [follower]} = Repo.update_all(q, [])
438 follower = maybe_update_following_count(follower)
440 {:ok, _} = update_follower_count(followed)
446 def unfollow(%User{} = follower, %User{} = followed) do
447 ap_followers = followed.follower_address
449 if following?(follower, followed) and follower.ap_id != followed.ap_id do
452 where: u.id == ^follower.id,
453 update: [pull: [following: ^ap_followers]],
457 {1, [follower]} = Repo.update_all(q, [])
459 follower = maybe_update_following_count(follower)
461 {:ok, followed} = update_follower_count(followed)
465 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
467 {:error, "Not subscribed!"}
471 @spec following?(User.t(), User.t()) :: boolean
472 def following?(%User{} = follower, %User{} = followed) do
473 Enum.member?(follower.following, followed.follower_address)
476 def locked?(%User{} = user) do
477 user.info.locked || false
481 Repo.get_by(User, id: id)
484 def get_by_ap_id(ap_id) do
485 Repo.get_by(User, ap_id: ap_id)
488 def get_all_by_ap_id(ap_ids) do
489 from(u in __MODULE__,
490 where: u.ap_id in ^ap_ids
495 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
496 # of the ap_id and the domain and tries to get that user
497 def get_by_guessed_nickname(ap_id) do
498 domain = URI.parse(ap_id).host
499 name = List.last(String.split(ap_id, "/"))
500 nickname = "#{name}@#{domain}"
502 get_cached_by_nickname(nickname)
505 def set_cache({:ok, user}), do: set_cache(user)
506 def set_cache({:error, err}), do: {:error, err}
508 def set_cache(%User{} = user) do
509 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
510 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
511 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
515 def update_and_set_cache(changeset) do
516 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
523 def invalidate_cache(user) do
524 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
525 Cachex.del(:user_cache, "nickname:#{user.nickname}")
526 Cachex.del(:user_cache, "user_info:#{user.id}")
529 def get_cached_by_ap_id(ap_id) do
530 key = "ap_id:#{ap_id}"
531 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
534 def get_cached_by_id(id) do
538 Cachex.fetch!(:user_cache, key, fn _ ->
542 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
543 {:commit, user.ap_id}
549 get_cached_by_ap_id(ap_id)
552 def get_cached_by_nickname(nickname) do
553 key = "nickname:#{nickname}"
555 Cachex.fetch!(:user_cache, key, fn ->
556 user_result = get_or_fetch_by_nickname(nickname)
559 {:ok, user} -> {:commit, user}
560 {:error, _error} -> {:ignore, nil}
565 def get_cached_by_nickname_or_id(nickname_or_id) do
566 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
569 def get_by_nickname(nickname) do
570 Repo.get_by(User, nickname: nickname) ||
571 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
572 Repo.get_by(User, nickname: local_nickname(nickname))
576 def get_by_email(email), do: Repo.get_by(User, email: email)
578 def get_by_nickname_or_email(nickname_or_email) do
579 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
582 def get_cached_user_info(user) do
583 key = "user_info:#{user.id}"
584 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
587 def fetch_by_nickname(nickname) do
588 ap_try = ActivityPub.make_user_from_nickname(nickname)
591 {:ok, user} -> {:ok, user}
592 _ -> OStatus.make_user(nickname)
596 def get_or_fetch_by_nickname(nickname) do
597 with %User{} = user <- get_by_nickname(nickname) do
601 with [_nick, _domain] <- String.split(nickname, "@"),
602 {:ok, user} <- fetch_by_nickname(nickname) do
603 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
604 fetch_initial_posts(user)
609 _e -> {:error, "not found " <> nickname}
614 @doc "Fetch some posts when the user has just been federated with"
615 def fetch_initial_posts(user),
616 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:fetch_initial_posts, user])
618 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
619 def get_followers_query(%User{} = user, nil) do
620 User.Query.build(%{followers: user, deactivated: false})
623 def get_followers_query(user, page) do
624 from(u in get_followers_query(user, nil))
625 |> User.Query.paginate(page, 20)
628 @spec get_followers_query(User.t()) :: Ecto.Query.t()
629 def get_followers_query(user), do: get_followers_query(user, nil)
631 @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
632 def get_followers(user, page \\ nil) do
633 q = get_followers_query(user, page)
638 @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
639 def get_external_followers(user, page \\ nil) do
642 |> get_followers_query(page)
643 |> User.Query.build(%{external: true})
648 def get_followers_ids(user, page \\ nil) do
649 q = get_followers_query(user, page)
651 Repo.all(from(u in q, select: u.id))
654 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
655 def get_friends_query(%User{} = user, nil) do
656 User.Query.build(%{friends: user, deactivated: false})
659 def get_friends_query(user, page) do
660 from(u in get_friends_query(user, nil))
661 |> User.Query.paginate(page, 20)
664 @spec get_friends_query(User.t()) :: Ecto.Query.t()
665 def get_friends_query(user), do: get_friends_query(user, nil)
667 def get_friends(user, page \\ nil) do
668 q = get_friends_query(user, page)
673 def get_friends_ids(user, page \\ nil) do
674 q = get_friends_query(user, page)
676 Repo.all(from(u in q, select: u.id))
679 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
680 def get_follow_requests(%User{} = user) do
682 Activity.follow_requests_for_actor(user)
683 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
684 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
685 |> group_by([a, u], u.id)
692 def increase_note_count(%User{} = user) do
694 |> where(id: ^user.id)
699 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
706 |> Repo.update_all([])
708 {1, [user]} -> set_cache(user)
713 def decrease_note_count(%User{} = user) do
715 |> where(id: ^user.id)
720 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
727 |> Repo.update_all([])
729 {1, [user]} -> set_cache(user)
734 def update_note_count(%User{} = user) do
738 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
742 note_count = Repo.one(note_count_query)
744 info_cng = User.Info.set_note_count(user.info, note_count)
748 |> put_embed(:info, info_cng)
749 |> update_and_set_cache()
752 def maybe_fetch_follow_information(user) do
753 with {:ok, user} <- fetch_follow_information(user) do
757 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
763 def fetch_follow_information(user) do
764 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
765 info_cng = User.Info.follow_information_update(user.info, info)
770 |> put_embed(:info, info_cng)
772 update_and_set_cache(changeset)
779 def update_follower_count(%User{} = user) do
780 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
781 follower_count_query =
782 User.Query.build(%{followers: user, deactivated: false})
783 |> select([u], %{count: count(u.id)})
786 |> where(id: ^user.id)
787 |> join(:inner, [u], s in subquery(follower_count_query))
792 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
799 |> Repo.update_all([])
801 {1, [user]} -> set_cache(user)
805 {:ok, maybe_fetch_follow_information(user)}
809 def maybe_update_following_count(%User{local: false} = user) do
810 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
811 {:ok, maybe_fetch_follow_information(user)}
817 def maybe_update_following_count(user), do: user
819 def remove_duplicated_following(%User{following: following} = user) do
820 uniq_following = Enum.uniq(following)
822 if length(following) == length(uniq_following) do
826 |> update_changeset(%{following: uniq_following})
827 |> update_and_set_cache()
831 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
832 def get_users_from_set(ap_ids, local_only \\ true) do
833 criteria = %{ap_id: ap_ids, deactivated: false}
834 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
836 User.Query.build(criteria)
840 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
841 def get_recipients_from_activity(%Activity{recipients: to}) do
842 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
846 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
847 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
851 User.Info.add_to_mutes(info, ap_id)
852 |> User.Info.add_to_muted_notifications(info, ap_id, notifications?)
856 |> put_embed(:info, info_cng)
858 update_and_set_cache(cng)
861 def unmute(muter, %{ap_id: ap_id}) do
865 User.Info.remove_from_mutes(info, ap_id)
866 |> User.Info.remove_from_muted_notifications(info, ap_id)
870 |> put_embed(:info, info_cng)
872 update_and_set_cache(cng)
875 def subscribe(subscriber, %{ap_id: ap_id}) do
876 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
878 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
879 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
882 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
886 |> User.Info.add_to_subscribers(subscriber.ap_id)
889 |> put_embed(:info, info_cng)
890 |> update_and_set_cache()
895 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
896 with %User{} = user <- get_cached_by_ap_id(ap_id) do
899 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
902 |> put_embed(:info, info_cng)
903 |> update_and_set_cache()
907 def block(blocker, %User{ap_id: ap_id} = blocked) do
908 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
910 if following?(blocker, blocked) do
911 {:ok, blocker, _} = unfollow(blocker, blocked)
918 if subscribed_to?(blocked, blocker) do
919 {:ok, blocker} = unsubscribe(blocked, blocker)
925 if following?(blocked, blocker) do
926 unfollow(blocked, blocker)
929 {:ok, blocker} = update_follower_count(blocker)
933 |> User.Info.add_to_block(ap_id)
937 |> put_embed(:info, info_cng)
939 update_and_set_cache(cng)
942 # helper to handle the block given only an actor's AP id
943 def block(blocker, %{ap_id: ap_id}) do
944 block(blocker, get_cached_by_ap_id(ap_id))
947 def unblock(blocker, %{ap_id: ap_id}) do
950 |> User.Info.remove_from_block(ap_id)
954 |> put_embed(:info, info_cng)
956 update_and_set_cache(cng)
959 def mutes?(nil, _), do: false
960 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
962 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
963 def muted_notifications?(nil, _), do: false
965 def muted_notifications?(user, %{ap_id: ap_id}),
966 do: Enum.member?(user.info.muted_notifications, ap_id)
968 def blocks?(%User{} = user, %User{} = target) do
969 blocks_ap_id?(user, target) || blocks_domain?(user, target)
972 def blocks?(nil, _), do: false
974 def blocks_ap_id?(%User{} = user, %User{} = target) do
975 Enum.member?(user.info.blocks, target.ap_id)
978 def blocks_ap_id?(_, _), do: false
980 def blocks_domain?(%User{} = user, %User{} = target) do
981 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
982 %{host: host} = URI.parse(target.ap_id)
983 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
986 def blocks_domain?(_, _), do: false
988 def subscribed_to?(user, %{ap_id: ap_id}) do
989 with %User{} = target <- get_cached_by_ap_id(ap_id) do
990 Enum.member?(target.info.subscribers, user.ap_id)
994 @spec muted_users(User.t()) :: [User.t()]
995 def muted_users(user) do
996 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
1000 @spec blocked_users(User.t()) :: [User.t()]
1001 def blocked_users(user) do
1002 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
1006 @spec subscribers(User.t()) :: [User.t()]
1007 def subscribers(user) do
1008 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
1012 def block_domain(user, domain) do
1015 |> User.Info.add_to_domain_block(domain)
1019 |> put_embed(:info, info_cng)
1021 update_and_set_cache(cng)
1024 def unblock_domain(user, domain) do
1027 |> User.Info.remove_from_domain_block(domain)
1031 |> put_embed(:info, info_cng)
1033 update_and_set_cache(cng)
1036 def deactivate_async(user, status \\ true) do
1037 PleromaJobQueue.enqueue(:background, __MODULE__, [:deactivate_async, user, status])
1040 def deactivate(%User{} = user, status \\ true) do
1041 info_cng = User.Info.set_activation_status(user.info, status)
1043 with {:ok, friends} <- User.get_friends(user),
1044 {:ok, followers} <- User.get_followers(user),
1048 |> put_embed(:info, info_cng)
1049 |> update_and_set_cache() do
1050 Enum.each(followers, &invalidate_cache(&1))
1051 Enum.each(friends, &update_follower_count(&1))
1057 def update_notification_settings(%User{} = user, settings \\ %{}) do
1058 info_changeset = User.Info.update_notification_settings(user.info, settings)
1061 |> put_embed(:info, info_changeset)
1062 |> update_and_set_cache()
1065 @spec delete(User.t()) :: :ok
1066 def delete(%User{} = user),
1067 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:delete, user])
1069 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1070 def perform(:delete, %User{} = user) do
1071 {:ok, _user} = ActivityPub.delete(user)
1073 # Remove all relationships
1074 {:ok, followers} = User.get_followers(user)
1076 Enum.each(followers, fn follower ->
1077 ActivityPub.unfollow(follower, user)
1078 User.unfollow(follower, user)
1081 {:ok, friends} = User.get_friends(user)
1083 Enum.each(friends, fn followed ->
1084 ActivityPub.unfollow(user, followed)
1085 User.unfollow(user, followed)
1088 delete_user_activities(user)
1089 invalidate_cache(user)
1093 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1094 def perform(:fetch_initial_posts, %User{} = user) do
1095 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1098 # Insert all the posts in reverse order, so they're in the right order on the timeline
1099 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
1100 &Pleroma.Web.Federator.incoming_ap_doc/1
1106 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1108 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1109 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1110 when is_list(blocked_identifiers) do
1112 blocked_identifiers,
1113 fn blocked_identifier ->
1114 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1115 {:ok, blocker} <- block(blocker, blocked),
1116 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1120 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1127 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1128 def perform(:follow_import, %User{} = follower, followed_identifiers)
1129 when is_list(followed_identifiers) do
1131 followed_identifiers,
1132 fn followed_identifier ->
1133 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1134 {:ok, follower} <- maybe_direct_follow(follower, followed),
1135 {:ok, _} <- ActivityPub.follow(follower, followed) do
1139 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1146 @spec external_users_query() :: Ecto.Query.t()
1147 def external_users_query do
1155 @spec external_users(keyword()) :: [User.t()]
1156 def external_users(opts \\ []) do
1158 external_users_query()
1159 |> select([u], struct(u, [:id, :ap_id, :info]))
1163 do: where(query, [u], u.id > ^opts[:max_id]),
1168 do: limit(query, ^opts[:limit]),
1174 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers),
1176 PleromaJobQueue.enqueue(:background, __MODULE__, [
1182 def follow_import(%User{} = follower, followed_identifiers) when is_list(followed_identifiers),
1184 PleromaJobQueue.enqueue(:background, __MODULE__, [
1187 followed_identifiers
1190 def delete_user_activities(%User{ap_id: ap_id} = user) do
1192 |> Activity.query_by_actor()
1193 |> RepoStreamer.chunk_stream(50)
1194 |> Stream.each(fn activities ->
1195 Enum.each(activities, &delete_activity(&1))
1202 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1204 |> Object.normalize()
1205 |> ActivityPub.delete()
1208 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1209 user = get_cached_by_ap_id(activity.actor)
1210 object = Object.normalize(activity)
1212 ActivityPub.unlike(user, object)
1215 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1216 user = get_cached_by_ap_id(activity.actor)
1217 object = Object.normalize(activity)
1219 ActivityPub.unannounce(user, object)
1222 defp delete_activity(_activity), do: "Doing nothing"
1224 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1225 Pleroma.HTML.Scrubber.TwitterText
1228 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1230 def fetch_by_ap_id(ap_id) do
1231 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1238 case OStatus.make_user(ap_id) do
1239 {:ok, user} -> {:ok, user}
1240 _ -> {:error, "Could not fetch by AP id"}
1245 def get_or_fetch_by_ap_id(ap_id) do
1246 user = get_cached_by_ap_id(ap_id)
1248 if !is_nil(user) and !User.needs_update?(user) do
1251 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1252 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1254 resp = fetch_by_ap_id(ap_id)
1256 if should_fetch_initial do
1257 with {:ok, %User{} = user} <- resp do
1258 fetch_initial_posts(user)
1266 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1267 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1268 if user = get_cached_by_ap_id(uri) do
1272 %User{info: %User.Info{}}
1273 |> cast(%{}, [:ap_id, :nickname, :local])
1274 |> put_change(:ap_id, uri)
1275 |> put_change(:nickname, nickname)
1276 |> put_change(:local, true)
1277 |> put_change(:follower_address, uri <> "/followers")
1279 {:ok, user} = Repo.insert(changes)
1285 def public_key_from_info(%{
1286 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1290 |> :public_key.pem_decode()
1292 |> :public_key.pem_entry_decode()
1298 def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
1299 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1302 def public_key_from_info(_), do: {:error, "not found key"}
1304 def get_public_key_for_ap_id(ap_id) do
1305 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1306 {:ok, public_key} <- public_key_from_info(user.info) do
1313 defp blank?(""), do: nil
1314 defp blank?(n), do: n
1316 def insert_or_update_user(data) do
1318 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1319 |> remote_user_creation()
1320 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1324 def ap_enabled?(%User{local: true}), do: true
1325 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1326 def ap_enabled?(_), do: false
1328 @doc "Gets or fetch a user by uri or nickname."
1329 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1330 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1331 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1333 # wait a period of time and return newest version of the User structs
1334 # this is because we have synchronous follow APIs and need to simulate them
1335 # with an async handshake
1336 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1337 with %User{} = a <- User.get_cached_by_id(a.id),
1338 %User{} = b <- User.get_cached_by_id(b.id) do
1346 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1347 with :ok <- :timer.sleep(timeout),
1348 %User{} = a <- User.get_cached_by_id(a.id),
1349 %User{} = b <- User.get_cached_by_id(b.id) do
1357 def parse_bio(bio) when is_binary(bio) and bio != "" do
1359 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1363 def parse_bio(_), do: ""
1365 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1366 # TODO: get profile URLs other than user.ap_id
1367 profile_urls = [user.ap_id]
1370 |> CommonUtils.format_input("text/plain",
1371 mentions_format: :full,
1372 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1377 def parse_bio(_, _), do: ""
1379 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1380 Repo.transaction(fn ->
1381 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1385 def tag(nickname, tags) when is_binary(nickname),
1386 do: tag(get_by_nickname(nickname), tags)
1388 def tag(%User{} = user, tags),
1389 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1391 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1392 Repo.transaction(fn ->
1393 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1397 def untag(nickname, tags) when is_binary(nickname),
1398 do: untag(get_by_nickname(nickname), tags)
1400 def untag(%User{} = user, tags),
1401 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1403 defp update_tags(%User{} = user, new_tags) do
1404 {:ok, updated_user} =
1406 |> change(%{tags: new_tags})
1407 |> update_and_set_cache()
1412 defp normalize_tags(tags) do
1415 |> Enum.map(&String.downcase(&1))
1418 defp local_nickname_regex do
1419 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1420 @extended_local_nickname_regex
1422 @strict_local_nickname_regex
1426 def local_nickname(nickname_or_mention) do
1429 |> String.split("@")
1433 def full_nickname(nickname_or_mention),
1434 do: String.trim_leading(nickname_or_mention, "@")
1436 def error_user(ap_id) do
1441 nickname: "erroruser@example.com",
1442 inserted_at: NaiveDateTime.utc_now()
1446 @spec all_superusers() :: [User.t()]
1447 def all_superusers do
1448 User.Query.build(%{super_users: true, local: true, deactivated: false})
1452 def showing_reblogs?(%User{} = user, %User{} = target) do
1453 target.ap_id not in user.info.muted_reblogs
1457 The function returns a query to get users with no activity for given interval of days.
1458 Inactive users are those who didn't read any notification, or had any activity where
1459 the user is the activity's actor, during `inactivity_threshold` days.
1460 Deactivated users will not appear in this list.
1464 iex> Pleroma.User.list_inactive_users()
1467 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1468 def list_inactive_users_query(inactivity_threshold \\ 7) do
1469 negative_inactivity_threshold = -inactivity_threshold
1470 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1471 # Subqueries are not supported in `where` clauses, join gets too complicated.
1472 has_read_notifications =
1473 from(n in Pleroma.Notification,
1474 where: n.seen == true,
1476 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1479 |> Pleroma.Repo.all()
1481 from(u in Pleroma.User,
1482 left_join: a in Pleroma.Activity,
1483 on: u.ap_id == a.actor,
1484 where: not is_nil(u.nickname),
1485 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1486 where: u.id not in ^has_read_notifications,
1489 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1490 is_nil(max(a.inserted_at))
1495 Enable or disable email notifications for user
1499 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1500 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1502 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1503 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1505 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1506 {:ok, t()} | {:error, Ecto.Changeset.t()}
1507 def switch_email_notifications(user, type, status) do
1508 info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})
1511 |> put_embed(:info, info)
1512 |> update_and_set_cache()
1516 Set `last_digest_emailed_at` value for the user to current time
1518 @spec touch_last_digest_emailed_at(t()) :: t()
1519 def touch_last_digest_emailed_at(user) do
1520 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1522 {:ok, updated_user} =
1524 |> change(%{last_digest_emailed_at: now})
1525 |> update_and_set_cache()
1530 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1531 def toggle_confirmation(%User{} = user) do
1532 need_confirmation? = !user.info.confirmation_pending
1535 User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
1539 |> put_embed(:info, info_changeset)
1540 |> update_and_set_cache()
1543 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1547 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1548 # use instance-default
1549 config = Pleroma.Config.get([:assets, :mascots])
1550 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1551 mascot = Keyword.get(config, default_mascot)
1554 "id" => "default-mascot",
1555 "url" => mascot[:url],
1556 "preview_url" => mascot[:url],
1558 "mime_type" => mascot[:mime_type]
1563 def ensure_keys_present(%User{info: info} = user) do
1567 {:ok, pem} = Keys.generate_rsa_pem()
1570 |> Ecto.Changeset.change()
1571 |> Ecto.Changeset.put_embed(:info, User.Info.set_keys(info, pem))
1572 |> update_and_set_cache()
1576 def get_ap_ids_by_nicknames(nicknames) do
1578 where: u.nickname in ^nicknames,
1584 defdelegate search(query, opts \\ []), to: User.Search
1586 defp put_password_hash(
1587 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1589 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1592 defp put_password_hash(changeset), do: changeset
1594 def is_internal_user?(%User{nickname: nil}), do: true
1595 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1596 def is_internal_user?(_), do: false