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 set_info_cache(user, args) do
136 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user, args))
139 @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
140 def restrict_deactivated(query) do
142 where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info)
146 def following_count(%User{following: []}), do: 0
148 def following_count(%User{} = user) do
150 |> get_friends_query()
151 |> Repo.aggregate(:count, :id)
154 def remote_user_creation(params) do
155 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
156 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
158 params = Map.put(params, :info, params[:info] || %{})
159 info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])
163 |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
164 |> validate_required([:name, :ap_id])
165 |> unique_constraint(:nickname)
166 |> validate_format(:nickname, @email_regex)
167 |> validate_length(:bio, max: bio_limit)
168 |> validate_length(:name, max: name_limit)
169 |> put_change(:local, false)
170 |> put_embed(:info, info_cng)
173 case info_cng.changes[:source_data] do
174 %{"followers" => followers, "following" => following} ->
176 |> put_change(:follower_address, followers)
177 |> put_change(:following_address, following)
180 followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
183 |> put_change(:follower_address, followers)
190 def update_changeset(struct, params \\ %{}) do
191 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
192 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
195 |> cast(params, [:bio, :name, :avatar, :following])
196 |> unique_constraint(:nickname)
197 |> validate_format(:nickname, local_nickname_regex())
198 |> validate_length(:bio, max: bio_limit)
199 |> validate_length(:name, min: 1, max: name_limit)
202 def upgrade_changeset(struct, params \\ %{}) do
203 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
204 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
206 params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
207 info_cng = User.Info.user_upgrade(struct.info, params[:info])
218 |> unique_constraint(:nickname)
219 |> validate_format(:nickname, local_nickname_regex())
220 |> validate_length(:bio, max: bio_limit)
221 |> validate_length(:name, max: name_limit)
222 |> put_embed(:info, info_cng)
225 def password_update_changeset(struct, params) do
227 |> cast(params, [:password, :password_confirmation])
228 |> validate_required([:password, :password_confirmation])
229 |> validate_confirmation(:password)
233 @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
234 def reset_password(%User{id: user_id} = user, data) do
237 |> Multi.update(:user, password_update_changeset(user, data))
238 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
239 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
241 case Repo.transaction(multi) do
242 {:ok, %{user: user} = _} -> set_cache(user)
243 {:error, _, changeset, _} -> {:error, changeset}
247 def register_changeset(struct, params \\ %{}, opts \\ []) do
248 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
249 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
252 if is_nil(opts[:need_confirmation]) do
253 Pleroma.Config.get([:instance, :account_activation_required])
255 opts[:need_confirmation]
259 User.Info.confirmation_changeset(%User.Info{}, need_confirmation: need_confirmation?)
263 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
264 |> validate_required([:name, :nickname, :password, :password_confirmation])
265 |> validate_confirmation(:password)
266 |> unique_constraint(:email)
267 |> unique_constraint(:nickname)
268 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
269 |> validate_format(:nickname, local_nickname_regex())
270 |> validate_format(:email, @email_regex)
271 |> validate_length(:bio, max: bio_limit)
272 |> validate_length(:name, min: 1, max: name_limit)
273 |> put_change(:info, info_change)
276 if opts[:external] do
279 validate_required(changeset, [:email])
282 if changeset.valid? do
283 ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
284 followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
288 |> put_change(:ap_id, ap_id)
289 |> unique_constraint(:ap_id)
290 |> put_change(:following, [followers])
291 |> put_change(:follower_address, followers)
297 defp autofollow_users(user) do
298 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
301 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
304 follow_all(user, autofollowed_users)
307 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
308 def register(%Ecto.Changeset{} = changeset) do
309 with {:ok, user} <- Repo.insert(changeset),
310 {:ok, user} <- autofollow_users(user),
311 {:ok, user} <- set_cache(user),
312 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
313 {:ok, _} <- try_send_confirmation_email(user) do
318 def try_send_confirmation_email(%User{} = user) do
319 if user.info.confirmation_pending &&
320 Pleroma.Config.get([:instance, :account_activation_required]) do
322 |> Pleroma.Emails.UserEmail.account_confirmation_email()
323 |> Pleroma.Emails.Mailer.deliver_async()
331 def needs_update?(%User{local: true}), do: false
333 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
335 def needs_update?(%User{local: false} = user) do
336 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
339 def needs_update?(_), do: true
341 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
342 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
346 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
347 follow(follower, followed)
350 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
351 if not User.ap_enabled?(followed) do
352 follow(follower, followed)
358 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
359 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
360 def follow_all(follower, followeds) do
363 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
364 |> Enum.map(fn %{follower_address: fa} -> fa end)
368 where: u.id == ^follower.id,
373 "array(select distinct unnest (array_cat(?, ?)))",
382 {1, [follower]} = Repo.update_all(q, [])
384 Enum.each(followeds, fn followed ->
385 update_follower_count(followed)
391 def follow(%User{} = follower, %User{info: info} = followed) do
392 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
393 ap_followers = followed.follower_address
397 {:error, "Could not follow user: You are deactivated."}
399 deny_follow_blocked and blocks?(followed, follower) ->
400 {:error, "Could not follow user: #{followed.nickname} blocked you."}
403 if !followed.local && follower.local && !ap_enabled?(followed) do
404 Websub.subscribe(follower, followed)
409 where: u.id == ^follower.id,
410 update: [push: [following: ^ap_followers]],
414 {1, [follower]} = Repo.update_all(q, [])
416 follower = maybe_update_following_count(follower)
418 {:ok, _} = update_follower_count(followed)
424 def unfollow(%User{} = follower, %User{} = followed) do
425 ap_followers = followed.follower_address
427 if following?(follower, followed) and follower.ap_id != followed.ap_id do
430 where: u.id == ^follower.id,
431 update: [pull: [following: ^ap_followers]],
435 {1, [follower]} = Repo.update_all(q, [])
437 follower = maybe_update_following_count(follower)
439 {:ok, followed} = update_follower_count(followed)
443 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
445 {:error, "Not subscribed!"}
449 @spec following?(User.t(), User.t()) :: boolean
450 def following?(%User{} = follower, %User{} = followed) do
451 Enum.member?(follower.following, followed.follower_address)
454 def locked?(%User{} = user) do
455 user.info.locked || false
459 Repo.get_by(User, id: id)
462 def get_by_ap_id(ap_id) do
463 Repo.get_by(User, ap_id: ap_id)
466 def get_all_by_ap_id(ap_ids) do
467 from(u in __MODULE__,
468 where: u.ap_id in ^ap_ids
473 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
474 # of the ap_id and the domain and tries to get that user
475 def get_by_guessed_nickname(ap_id) do
476 domain = URI.parse(ap_id).host
477 name = List.last(String.split(ap_id, "/"))
478 nickname = "#{name}@#{domain}"
480 get_cached_by_nickname(nickname)
483 def set_cache({:ok, user}), do: set_cache(user)
484 def set_cache({:error, err}), do: {:error, err}
486 def set_cache(%User{} = user) do
487 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
488 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
489 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
493 def update_and_set_cache(changeset) do
494 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
501 def invalidate_cache(user) do
502 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
503 Cachex.del(:user_cache, "nickname:#{user.nickname}")
504 Cachex.del(:user_cache, "user_info:#{user.id}")
507 def get_cached_by_ap_id(ap_id) do
508 key = "ap_id:#{ap_id}"
509 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
512 def get_cached_by_id(id) do
516 Cachex.fetch!(:user_cache, key, fn _ ->
520 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
521 {:commit, user.ap_id}
527 get_cached_by_ap_id(ap_id)
530 def get_cached_by_nickname(nickname) do
531 key = "nickname:#{nickname}"
533 Cachex.fetch!(:user_cache, key, fn ->
534 user_result = get_or_fetch_by_nickname(nickname)
537 {:ok, user} -> {:commit, user}
538 {:error, _error} -> {:ignore, nil}
543 def get_cached_by_nickname_or_id(nickname_or_id) do
544 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
547 def get_by_nickname(nickname) do
548 Repo.get_by(User, nickname: nickname) ||
549 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
550 Repo.get_by(User, nickname: local_nickname(nickname))
554 def get_by_email(email), do: Repo.get_by(User, email: email)
556 def get_by_nickname_or_email(nickname_or_email) do
557 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
560 def get_cached_user_info(user) do
561 key = "user_info:#{user.id}"
562 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
565 def fetch_by_nickname(nickname) do
566 ap_try = ActivityPub.make_user_from_nickname(nickname)
569 {:ok, user} -> {:ok, user}
570 _ -> OStatus.make_user(nickname)
574 def get_or_fetch_by_nickname(nickname) do
575 with %User{} = user <- get_by_nickname(nickname) do
579 with [_nick, _domain] <- String.split(nickname, "@"),
580 {:ok, user} <- fetch_by_nickname(nickname) do
581 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
582 fetch_initial_posts(user)
587 _e -> {:error, "not found " <> nickname}
592 @doc "Fetch some posts when the user has just been federated with"
593 def fetch_initial_posts(user),
594 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:fetch_initial_posts, user])
596 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
597 def get_followers_query(%User{} = user, nil) do
598 User.Query.build(%{followers: user, deactivated: false})
601 def get_followers_query(user, page) do
602 from(u in get_followers_query(user, nil))
603 |> User.Query.paginate(page, 20)
606 @spec get_followers_query(User.t()) :: Ecto.Query.t()
607 def get_followers_query(user), do: get_followers_query(user, nil)
609 @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
610 def get_followers(user, page \\ nil) do
611 q = get_followers_query(user, page)
616 @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
617 def get_external_followers(user, page \\ nil) do
620 |> get_followers_query(page)
621 |> User.Query.build(%{external: true})
626 def get_followers_ids(user, page \\ nil) do
627 q = get_followers_query(user, page)
629 Repo.all(from(u in q, select: u.id))
632 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
633 def get_friends_query(%User{} = user, nil) do
634 User.Query.build(%{friends: user, deactivated: false})
637 def get_friends_query(user, page) do
638 from(u in get_friends_query(user, nil))
639 |> User.Query.paginate(page, 20)
642 @spec get_friends_query(User.t()) :: Ecto.Query.t()
643 def get_friends_query(user), do: get_friends_query(user, nil)
645 def get_friends(user, page \\ nil) do
646 q = get_friends_query(user, page)
651 def get_friends_ids(user, page \\ nil) do
652 q = get_friends_query(user, page)
654 Repo.all(from(u in q, select: u.id))
657 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
658 def get_follow_requests(%User{} = user) do
660 Activity.follow_requests_for_actor(user)
661 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
662 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
663 |> group_by([a, u], u.id)
670 def increase_note_count(%User{} = user) do
672 |> where(id: ^user.id)
677 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
684 |> Repo.update_all([])
686 {1, [user]} -> set_cache(user)
691 def decrease_note_count(%User{} = user) do
693 |> where(id: ^user.id)
698 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
705 |> Repo.update_all([])
707 {1, [user]} -> set_cache(user)
712 def update_note_count(%User{} = user) do
716 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
720 note_count = Repo.one(note_count_query)
722 info_cng = User.Info.set_note_count(user.info, note_count)
726 |> put_embed(:info, info_cng)
727 |> update_and_set_cache()
730 def maybe_fetch_follow_information(user) do
731 with {:ok, user} <- fetch_follow_information(user) do
735 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
741 def fetch_follow_information(user) do
742 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
743 info_cng = User.Info.follow_information_update(user.info, info)
748 |> put_embed(:info, info_cng)
750 update_and_set_cache(changeset)
757 def update_follower_count(%User{} = user) do
758 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
759 follower_count_query =
760 User.Query.build(%{followers: user, deactivated: false})
761 |> select([u], %{count: count(u.id)})
764 |> where(id: ^user.id)
765 |> join(:inner, [u], s in subquery(follower_count_query))
770 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
777 |> Repo.update_all([])
779 {1, [user]} -> set_cache(user)
783 {:ok, maybe_fetch_follow_information(user)}
787 def maybe_update_following_count(%User{local: false} = user) do
788 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
789 {:ok, maybe_fetch_follow_information(user)}
795 def maybe_update_following_count(user), do: user
797 def remove_duplicated_following(%User{following: following} = user) do
798 uniq_following = Enum.uniq(following)
800 if length(following) == length(uniq_following) do
804 |> update_changeset(%{following: uniq_following})
805 |> update_and_set_cache()
809 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
810 def get_users_from_set(ap_ids, local_only \\ true) do
811 criteria = %{ap_id: ap_ids, deactivated: false}
812 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
814 User.Query.build(criteria)
818 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
819 def get_recipients_from_activity(%Activity{recipients: to}) do
820 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
824 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
825 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
829 User.Info.add_to_mutes(info, ap_id)
830 |> User.Info.add_to_muted_notifications(info, ap_id, notifications?)
834 |> put_embed(:info, info_cng)
836 update_and_set_cache(cng)
839 def unmute(muter, %{ap_id: ap_id}) do
843 User.Info.remove_from_mutes(info, ap_id)
844 |> User.Info.remove_from_muted_notifications(info, ap_id)
848 |> put_embed(:info, info_cng)
850 update_and_set_cache(cng)
853 def subscribe(subscriber, %{ap_id: ap_id}) do
854 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
856 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
857 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
860 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
864 |> User.Info.add_to_subscribers(subscriber.ap_id)
867 |> put_embed(:info, info_cng)
868 |> update_and_set_cache()
873 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
874 with %User{} = user <- get_cached_by_ap_id(ap_id) do
877 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
880 |> put_embed(:info, info_cng)
881 |> update_and_set_cache()
885 def block(blocker, %User{ap_id: ap_id} = blocked) do
886 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
888 if following?(blocker, blocked) do
889 {:ok, blocker, _} = unfollow(blocker, blocked)
896 if subscribed_to?(blocked, blocker) do
897 {:ok, blocker} = unsubscribe(blocked, blocker)
903 if following?(blocked, blocker) do
904 unfollow(blocked, blocker)
907 {:ok, blocker} = update_follower_count(blocker)
911 |> User.Info.add_to_block(ap_id)
915 |> put_embed(:info, info_cng)
917 update_and_set_cache(cng)
920 # helper to handle the block given only an actor's AP id
921 def block(blocker, %{ap_id: ap_id}) do
922 block(blocker, get_cached_by_ap_id(ap_id))
925 def unblock(blocker, %{ap_id: ap_id}) do
928 |> User.Info.remove_from_block(ap_id)
932 |> put_embed(:info, info_cng)
934 update_and_set_cache(cng)
937 def mutes?(nil, _), do: false
938 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
940 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
941 def muted_notifications?(nil, _), do: false
943 def muted_notifications?(user, %{ap_id: ap_id}),
944 do: Enum.member?(user.info.muted_notifications, ap_id)
946 def blocks?(%User{} = user, %User{} = target) do
947 blocks_ap_id?(user, target) || blocks_domain?(user, target)
950 def blocks?(nil, _), do: false
952 def blocks_ap_id?(%User{} = user, %User{} = target) do
953 Enum.member?(user.info.blocks, target.ap_id)
956 def blocks_ap_id?(_, _), do: false
958 def blocks_domain?(%User{} = user, %User{} = target) do
959 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
960 %{host: host} = URI.parse(target.ap_id)
961 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
964 def blocks_domain?(_, _), do: false
966 def subscribed_to?(user, %{ap_id: ap_id}) do
967 with %User{} = target <- get_cached_by_ap_id(ap_id) do
968 Enum.member?(target.info.subscribers, user.ap_id)
972 @spec muted_users(User.t()) :: [User.t()]
973 def muted_users(user) do
974 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
978 @spec blocked_users(User.t()) :: [User.t()]
979 def blocked_users(user) do
980 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
984 @spec subscribers(User.t()) :: [User.t()]
985 def subscribers(user) do
986 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
990 def block_domain(user, domain) do
993 |> User.Info.add_to_domain_block(domain)
997 |> put_embed(:info, info_cng)
999 update_and_set_cache(cng)
1002 def unblock_domain(user, domain) do
1005 |> User.Info.remove_from_domain_block(domain)
1009 |> put_embed(:info, info_cng)
1011 update_and_set_cache(cng)
1014 def deactivate_async(user, status \\ true) do
1015 PleromaJobQueue.enqueue(:background, __MODULE__, [:deactivate_async, user, status])
1018 def deactivate(%User{} = user, status \\ true) do
1019 info_cng = User.Info.set_activation_status(user.info, status)
1021 with {:ok, friends} <- User.get_friends(user),
1022 {:ok, followers} <- User.get_followers(user),
1026 |> put_embed(:info, info_cng)
1027 |> update_and_set_cache() do
1028 Enum.each(followers, &invalidate_cache(&1))
1029 Enum.each(friends, &update_follower_count(&1))
1035 def update_notification_settings(%User{} = user, settings \\ %{}) do
1036 info_changeset = User.Info.update_notification_settings(user.info, settings)
1039 |> put_embed(:info, info_changeset)
1040 |> update_and_set_cache()
1043 @spec delete(User.t()) :: :ok
1044 def delete(%User{} = user),
1045 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:delete, user])
1047 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1048 def perform(:delete, %User{} = user) do
1049 {:ok, _user} = ActivityPub.delete(user)
1051 # Remove all relationships
1052 {:ok, followers} = User.get_followers(user)
1054 Enum.each(followers, fn follower ->
1055 ActivityPub.unfollow(follower, user)
1056 User.unfollow(follower, user)
1059 {:ok, friends} = User.get_friends(user)
1061 Enum.each(friends, fn followed ->
1062 ActivityPub.unfollow(user, followed)
1063 User.unfollow(user, followed)
1066 delete_user_activities(user)
1067 invalidate_cache(user)
1071 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1072 def perform(:fetch_initial_posts, %User{} = user) do
1073 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1076 # Insert all the posts in reverse order, so they're in the right order on the timeline
1077 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
1078 &Pleroma.Web.Federator.incoming_ap_doc/1
1084 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1086 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1087 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1088 when is_list(blocked_identifiers) do
1090 blocked_identifiers,
1091 fn blocked_identifier ->
1092 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1093 {:ok, blocker} <- block(blocker, blocked),
1094 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1098 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1105 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1106 def perform(:follow_import, %User{} = follower, followed_identifiers)
1107 when is_list(followed_identifiers) do
1109 followed_identifiers,
1110 fn followed_identifier ->
1111 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1112 {:ok, follower} <- maybe_direct_follow(follower, followed),
1113 {:ok, _} <- ActivityPub.follow(follower, followed) do
1117 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1124 @spec external_users_query() :: Ecto.Query.t()
1125 def external_users_query do
1133 @spec external_users(keyword()) :: [User.t()]
1134 def external_users(opts \\ []) do
1136 external_users_query()
1137 |> select([u], struct(u, [:id, :ap_id, :info]))
1141 do: where(query, [u], u.id > ^opts[:max_id]),
1146 do: limit(query, ^opts[:limit]),
1152 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers),
1154 PleromaJobQueue.enqueue(:background, __MODULE__, [
1160 def follow_import(%User{} = follower, followed_identifiers) when is_list(followed_identifiers),
1162 PleromaJobQueue.enqueue(:background, __MODULE__, [
1165 followed_identifiers
1168 def delete_user_activities(%User{ap_id: ap_id} = user) do
1170 |> Activity.query_by_actor()
1171 |> RepoStreamer.chunk_stream(50)
1172 |> Stream.each(fn activities ->
1173 Enum.each(activities, &delete_activity(&1))
1180 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1182 |> Object.normalize()
1183 |> ActivityPub.delete()
1186 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1187 user = get_cached_by_ap_id(activity.actor)
1188 object = Object.normalize(activity)
1190 ActivityPub.unlike(user, object)
1193 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1194 user = get_cached_by_ap_id(activity.actor)
1195 object = Object.normalize(activity)
1197 ActivityPub.unannounce(user, object)
1200 defp delete_activity(_activity), do: "Doing nothing"
1202 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1203 Pleroma.HTML.Scrubber.TwitterText
1206 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1208 def fetch_by_ap_id(ap_id) do
1209 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1216 case OStatus.make_user(ap_id) do
1217 {:ok, user} -> {:ok, user}
1218 _ -> {:error, "Could not fetch by AP id"}
1223 def get_or_fetch_by_ap_id(ap_id) do
1224 user = get_cached_by_ap_id(ap_id)
1226 if !is_nil(user) and !User.needs_update?(user) do
1229 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1230 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1232 resp = fetch_by_ap_id(ap_id)
1234 if should_fetch_initial do
1235 with {:ok, %User{} = user} <- resp do
1236 fetch_initial_posts(user)
1244 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1245 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1246 if user = get_cached_by_ap_id(uri) do
1250 %User{info: %User.Info{}}
1251 |> cast(%{}, [:ap_id, :nickname, :local])
1252 |> put_change(:ap_id, uri)
1253 |> put_change(:nickname, nickname)
1254 |> put_change(:local, true)
1255 |> put_change(:follower_address, uri <> "/followers")
1257 {:ok, user} = Repo.insert(changes)
1263 def public_key_from_info(%{
1264 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1268 |> :public_key.pem_decode()
1270 |> :public_key.pem_entry_decode()
1276 def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
1277 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1280 def public_key_from_info(_), do: {:error, "not found key"}
1282 def get_public_key_for_ap_id(ap_id) do
1283 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1284 {:ok, public_key} <- public_key_from_info(user.info) do
1291 defp blank?(""), do: nil
1292 defp blank?(n), do: n
1294 def insert_or_update_user(data) do
1296 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1297 |> remote_user_creation()
1298 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1302 def ap_enabled?(%User{local: true}), do: true
1303 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1304 def ap_enabled?(_), do: false
1306 @doc "Gets or fetch a user by uri or nickname."
1307 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1308 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1309 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1311 # wait a period of time and return newest version of the User structs
1312 # this is because we have synchronous follow APIs and need to simulate them
1313 # with an async handshake
1314 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1315 with %User{} = a <- User.get_cached_by_id(a.id),
1316 %User{} = b <- User.get_cached_by_id(b.id) do
1324 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1325 with :ok <- :timer.sleep(timeout),
1326 %User{} = a <- User.get_cached_by_id(a.id),
1327 %User{} = b <- User.get_cached_by_id(b.id) do
1335 def parse_bio(bio) when is_binary(bio) and bio != "" do
1337 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1341 def parse_bio(_), do: ""
1343 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1344 # TODO: get profile URLs other than user.ap_id
1345 profile_urls = [user.ap_id]
1348 |> CommonUtils.format_input("text/plain",
1349 mentions_format: :full,
1350 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1355 def parse_bio(_, _), do: ""
1357 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1358 Repo.transaction(fn ->
1359 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1363 def tag(nickname, tags) when is_binary(nickname),
1364 do: tag(get_by_nickname(nickname), tags)
1366 def tag(%User{} = user, tags),
1367 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1369 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1370 Repo.transaction(fn ->
1371 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1375 def untag(nickname, tags) when is_binary(nickname),
1376 do: untag(get_by_nickname(nickname), tags)
1378 def untag(%User{} = user, tags),
1379 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1381 defp update_tags(%User{} = user, new_tags) do
1382 {:ok, updated_user} =
1384 |> change(%{tags: new_tags})
1385 |> update_and_set_cache()
1390 defp normalize_tags(tags) do
1393 |> Enum.map(&String.downcase(&1))
1396 defp local_nickname_regex do
1397 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1398 @extended_local_nickname_regex
1400 @strict_local_nickname_regex
1404 def local_nickname(nickname_or_mention) do
1407 |> String.split("@")
1411 def full_nickname(nickname_or_mention),
1412 do: String.trim_leading(nickname_or_mention, "@")
1414 def error_user(ap_id) do
1419 nickname: "erroruser@example.com",
1420 inserted_at: NaiveDateTime.utc_now()
1424 @spec all_superusers() :: [User.t()]
1425 def all_superusers do
1426 User.Query.build(%{super_users: true, local: true, deactivated: false})
1430 def showing_reblogs?(%User{} = user, %User{} = target) do
1431 target.ap_id not in user.info.muted_reblogs
1435 The function returns a query to get users with no activity for given interval of days.
1436 Inactive users are those who didn't read any notification, or had any activity where
1437 the user is the activity's actor, during `inactivity_threshold` days.
1438 Deactivated users will not appear in this list.
1442 iex> Pleroma.User.list_inactive_users()
1445 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1446 def list_inactive_users_query(inactivity_threshold \\ 7) do
1447 negative_inactivity_threshold = -inactivity_threshold
1448 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1449 # Subqueries are not supported in `where` clauses, join gets too complicated.
1450 has_read_notifications =
1451 from(n in Pleroma.Notification,
1452 where: n.seen == true,
1454 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1457 |> Pleroma.Repo.all()
1459 from(u in Pleroma.User,
1460 left_join: a in Pleroma.Activity,
1461 on: u.ap_id == a.actor,
1462 where: not is_nil(u.nickname),
1463 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1464 where: u.id not in ^has_read_notifications,
1467 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1468 is_nil(max(a.inserted_at))
1473 Enable or disable email notifications for user
1477 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1478 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1480 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1481 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1483 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1484 {:ok, t()} | {:error, Ecto.Changeset.t()}
1485 def switch_email_notifications(user, type, status) do
1486 info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})
1489 |> put_embed(:info, info)
1490 |> update_and_set_cache()
1494 Set `last_digest_emailed_at` value for the user to current time
1496 @spec touch_last_digest_emailed_at(t()) :: t()
1497 def touch_last_digest_emailed_at(user) do
1498 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1500 {:ok, updated_user} =
1502 |> change(%{last_digest_emailed_at: now})
1503 |> update_and_set_cache()
1508 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1509 def toggle_confirmation(%User{} = user) do
1510 need_confirmation? = !user.info.confirmation_pending
1513 User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
1517 |> put_embed(:info, info_changeset)
1518 |> update_and_set_cache()
1521 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1525 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1526 # use instance-default
1527 config = Pleroma.Config.get([:assets, :mascots])
1528 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1529 mascot = Keyword.get(config, default_mascot)
1532 "id" => "default-mascot",
1533 "url" => mascot[:url],
1534 "preview_url" => mascot[:url],
1536 "mime_type" => mascot[:mime_type]
1541 def ensure_keys_present(%User{info: info} = user) do
1545 {:ok, pem} = Keys.generate_rsa_pem()
1548 |> Ecto.Changeset.change()
1549 |> Ecto.Changeset.put_embed(:info, User.Info.set_keys(info, pem))
1550 |> update_and_set_cache()
1554 def get_ap_ids_by_nicknames(nicknames) do
1556 where: u.nickname in ^nicknames,
1562 defdelegate search(query, opts \\ []), to: User.Search
1564 defp put_password_hash(
1565 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1567 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1570 defp put_password_hash(changeset), do: changeset
1572 def is_internal_user?(%User{nickname: nil}), do: true
1573 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1574 def is_internal_user?(_), do: false