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) do
573 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
576 def get_by_nickname(nickname) do
577 Repo.get_by(User, nickname: nickname) ||
578 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
579 Repo.get_by(User, nickname: local_nickname(nickname))
583 def get_by_email(email), do: Repo.get_by(User, email: email)
585 def get_by_nickname_or_email(nickname_or_email) do
586 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
589 def get_cached_user_info(user) do
590 key = "user_info:#{user.id}"
591 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
594 def fetch_by_nickname(nickname) do
595 ap_try = ActivityPub.make_user_from_nickname(nickname)
598 {:ok, user} -> {:ok, user}
599 _ -> OStatus.make_user(nickname)
603 def get_or_fetch_by_nickname(nickname) do
604 with %User{} = user <- get_by_nickname(nickname) do
608 with [_nick, _domain] <- String.split(nickname, "@"),
609 {:ok, user} <- fetch_by_nickname(nickname) do
610 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
611 fetch_initial_posts(user)
616 _e -> {:error, "not found " <> nickname}
621 @doc "Fetch some posts when the user has just been federated with"
622 def fetch_initial_posts(user),
623 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:fetch_initial_posts, user])
625 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
626 def get_followers_query(%User{} = user, nil) do
627 User.Query.build(%{followers: user, deactivated: false})
630 def get_followers_query(user, page) do
631 from(u in get_followers_query(user, nil))
632 |> User.Query.paginate(page, 20)
635 @spec get_followers_query(User.t()) :: Ecto.Query.t()
636 def get_followers_query(user), do: get_followers_query(user, nil)
638 @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
639 def get_followers(user, page \\ nil) do
640 q = get_followers_query(user, page)
645 @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
646 def get_external_followers(user, page \\ nil) do
649 |> get_followers_query(page)
650 |> User.Query.build(%{external: true})
655 def get_followers_ids(user, page \\ nil) do
656 q = get_followers_query(user, page)
658 Repo.all(from(u in q, select: u.id))
661 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
662 def get_friends_query(%User{} = user, nil) do
663 User.Query.build(%{friends: user, deactivated: false})
666 def get_friends_query(user, page) do
667 from(u in get_friends_query(user, nil))
668 |> User.Query.paginate(page, 20)
671 @spec get_friends_query(User.t()) :: Ecto.Query.t()
672 def get_friends_query(user), do: get_friends_query(user, nil)
674 def get_friends(user, page \\ nil) do
675 q = get_friends_query(user, page)
680 def get_friends_ids(user, page \\ nil) do
681 q = get_friends_query(user, page)
683 Repo.all(from(u in q, select: u.id))
686 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
687 def get_follow_requests(%User{} = user) do
689 Activity.follow_requests_for_actor(user)
690 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
691 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
692 |> group_by([a, u], u.id)
699 def increase_note_count(%User{} = user) do
701 |> where(id: ^user.id)
706 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
713 |> Repo.update_all([])
715 {1, [user]} -> set_cache(user)
720 def decrease_note_count(%User{} = user) do
722 |> where(id: ^user.id)
727 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
734 |> Repo.update_all([])
736 {1, [user]} -> set_cache(user)
741 def update_note_count(%User{} = user) do
745 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
749 note_count = Repo.one(note_count_query)
751 info_cng = User.Info.set_note_count(user.info, note_count)
755 |> put_embed(:info, info_cng)
756 |> update_and_set_cache()
759 @spec maybe_fetch_follow_information(User.t()) :: User.t()
760 def maybe_fetch_follow_information(user) do
761 with {:ok, user} <- fetch_follow_information(user) do
765 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
771 def fetch_follow_information(user) do
772 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
773 info_cng = User.Info.follow_information_update(user.info, info)
778 |> put_embed(:info, info_cng)
780 update_and_set_cache(changeset)
787 def update_follower_count(%User{} = user) do
788 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
789 follower_count_query =
790 User.Query.build(%{followers: user, deactivated: false})
791 |> select([u], %{count: count(u.id)})
794 |> where(id: ^user.id)
795 |> join(:inner, [u], s in subquery(follower_count_query))
800 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
807 |> Repo.update_all([])
809 {1, [user]} -> set_cache(user)
813 {:ok, maybe_fetch_follow_information(user)}
817 @spec maybe_update_following_count(User.t()) :: User.t()
818 def maybe_update_following_count(%User{local: false} = user) do
819 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
820 maybe_fetch_follow_information(user)
826 def maybe_update_following_count(user), do: user
828 def remove_duplicated_following(%User{following: following} = user) do
829 uniq_following = Enum.uniq(following)
831 if length(following) == length(uniq_following) do
835 |> update_changeset(%{following: uniq_following})
836 |> update_and_set_cache()
840 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
841 def get_users_from_set(ap_ids, local_only \\ true) do
842 criteria = %{ap_id: ap_ids, deactivated: false}
843 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
845 User.Query.build(criteria)
849 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
850 def get_recipients_from_activity(%Activity{recipients: to}) do
851 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
855 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
856 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
860 User.Info.add_to_mutes(info, ap_id)
861 |> User.Info.add_to_muted_notifications(info, ap_id, notifications?)
865 |> put_embed(:info, info_cng)
867 update_and_set_cache(cng)
870 def unmute(muter, %{ap_id: ap_id}) do
874 User.Info.remove_from_mutes(info, ap_id)
875 |> User.Info.remove_from_muted_notifications(info, ap_id)
879 |> put_embed(:info, info_cng)
881 update_and_set_cache(cng)
884 def subscribe(subscriber, %{ap_id: ap_id}) do
885 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
887 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
888 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
891 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
895 |> User.Info.add_to_subscribers(subscriber.ap_id)
898 |> put_embed(:info, info_cng)
899 |> update_and_set_cache()
904 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
905 with %User{} = user <- get_cached_by_ap_id(ap_id) do
908 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
911 |> put_embed(:info, info_cng)
912 |> update_and_set_cache()
916 def block(blocker, %User{ap_id: ap_id} = blocked) do
917 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
919 if following?(blocker, blocked) do
920 {:ok, blocker, _} = unfollow(blocker, blocked)
926 # clear any requested follows as well
928 case CommonAPI.reject_follow_request(blocked, blocker) do
929 {:ok, %User{} = updated_blocked} -> updated_blocked
934 if subscribed_to?(blocked, blocker) do
935 {:ok, blocker} = unsubscribe(blocked, blocker)
941 if following?(blocked, blocker) do
942 unfollow(blocked, blocker)
945 {:ok, blocker} = update_follower_count(blocker)
949 |> User.Info.add_to_block(ap_id)
953 |> put_embed(:info, info_cng)
955 update_and_set_cache(cng)
958 # helper to handle the block given only an actor's AP id
959 def block(blocker, %{ap_id: ap_id}) do
960 block(blocker, get_cached_by_ap_id(ap_id))
963 def unblock(blocker, %{ap_id: ap_id}) do
966 |> User.Info.remove_from_block(ap_id)
970 |> put_embed(:info, info_cng)
972 update_and_set_cache(cng)
975 def mutes?(nil, _), do: false
976 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
978 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
979 def muted_notifications?(nil, _), do: false
981 def muted_notifications?(user, %{ap_id: ap_id}),
982 do: Enum.member?(user.info.muted_notifications, ap_id)
984 def blocks?(%User{} = user, %User{} = target) do
985 blocks_ap_id?(user, target) || blocks_domain?(user, target)
988 def blocks?(nil, _), do: false
990 def blocks_ap_id?(%User{} = user, %User{} = target) do
991 Enum.member?(user.info.blocks, target.ap_id)
994 def blocks_ap_id?(_, _), do: false
996 def blocks_domain?(%User{} = user, %User{} = target) do
997 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
998 %{host: host} = URI.parse(target.ap_id)
999 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1002 def blocks_domain?(_, _), do: false
1004 def subscribed_to?(user, %{ap_id: ap_id}) do
1005 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1006 Enum.member?(target.info.subscribers, user.ap_id)
1010 @spec muted_users(User.t()) :: [User.t()]
1011 def muted_users(user) do
1012 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
1016 @spec blocked_users(User.t()) :: [User.t()]
1017 def blocked_users(user) do
1018 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
1022 @spec subscribers(User.t()) :: [User.t()]
1023 def subscribers(user) do
1024 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
1028 def block_domain(user, domain) do
1031 |> User.Info.add_to_domain_block(domain)
1035 |> put_embed(:info, info_cng)
1037 update_and_set_cache(cng)
1040 def unblock_domain(user, domain) do
1043 |> User.Info.remove_from_domain_block(domain)
1047 |> put_embed(:info, info_cng)
1049 update_and_set_cache(cng)
1052 def deactivate_async(user, status \\ true) do
1053 PleromaJobQueue.enqueue(:background, __MODULE__, [:deactivate_async, user, status])
1056 def deactivate(%User{} = user, status \\ true) do
1057 info_cng = User.Info.set_activation_status(user.info, status)
1059 with {:ok, friends} <- User.get_friends(user),
1060 {:ok, followers} <- User.get_followers(user),
1064 |> put_embed(:info, info_cng)
1065 |> update_and_set_cache() do
1066 Enum.each(followers, &invalidate_cache(&1))
1067 Enum.each(friends, &update_follower_count(&1))
1073 def update_notification_settings(%User{} = user, settings \\ %{}) do
1074 info_changeset = User.Info.update_notification_settings(user.info, settings)
1077 |> put_embed(:info, info_changeset)
1078 |> update_and_set_cache()
1081 @spec delete(User.t()) :: :ok
1082 def delete(%User{} = user),
1083 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:delete, user])
1085 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1086 def perform(:delete, %User{} = user) do
1087 {:ok, _user} = ActivityPub.delete(user)
1089 # Remove all relationships
1090 {:ok, followers} = User.get_followers(user)
1092 Enum.each(followers, fn follower ->
1093 ActivityPub.unfollow(follower, user)
1094 User.unfollow(follower, user)
1097 {:ok, friends} = User.get_friends(user)
1099 Enum.each(friends, fn followed ->
1100 ActivityPub.unfollow(user, followed)
1101 User.unfollow(user, followed)
1104 delete_user_activities(user)
1105 invalidate_cache(user)
1109 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1110 def perform(:fetch_initial_posts, %User{} = user) do
1111 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1114 # Insert all the posts in reverse order, so they're in the right order on the timeline
1115 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
1116 &Pleroma.Web.Federator.incoming_ap_doc/1
1122 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1124 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1125 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1126 when is_list(blocked_identifiers) do
1128 blocked_identifiers,
1129 fn blocked_identifier ->
1130 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1131 {:ok, blocker} <- block(blocker, blocked),
1132 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1136 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1143 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1144 def perform(:follow_import, %User{} = follower, followed_identifiers)
1145 when is_list(followed_identifiers) do
1147 followed_identifiers,
1148 fn followed_identifier ->
1149 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1150 {:ok, follower} <- maybe_direct_follow(follower, followed),
1151 {:ok, _} <- ActivityPub.follow(follower, followed) do
1155 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1162 @spec external_users_query() :: Ecto.Query.t()
1163 def external_users_query do
1171 @spec external_users(keyword()) :: [User.t()]
1172 def external_users(opts \\ []) do
1174 external_users_query()
1175 |> select([u], struct(u, [:id, :ap_id, :info]))
1179 do: where(query, [u], u.id > ^opts[:max_id]),
1184 do: limit(query, ^opts[:limit]),
1190 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers),
1192 PleromaJobQueue.enqueue(:background, __MODULE__, [
1198 def follow_import(%User{} = follower, followed_identifiers) when is_list(followed_identifiers),
1200 PleromaJobQueue.enqueue(:background, __MODULE__, [
1203 followed_identifiers
1206 def delete_user_activities(%User{ap_id: ap_id} = user) do
1208 |> Activity.query_by_actor()
1209 |> RepoStreamer.chunk_stream(50)
1210 |> Stream.each(fn activities ->
1211 Enum.each(activities, &delete_activity(&1))
1218 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1220 |> Object.normalize()
1221 |> ActivityPub.delete()
1224 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1225 user = get_cached_by_ap_id(activity.actor)
1226 object = Object.normalize(activity)
1228 ActivityPub.unlike(user, object)
1231 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1232 user = get_cached_by_ap_id(activity.actor)
1233 object = Object.normalize(activity)
1235 ActivityPub.unannounce(user, object)
1238 defp delete_activity(_activity), do: "Doing nothing"
1240 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1241 Pleroma.HTML.Scrubber.TwitterText
1244 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1246 def fetch_by_ap_id(ap_id) do
1247 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1254 case OStatus.make_user(ap_id) do
1255 {:ok, user} -> {:ok, user}
1256 _ -> {:error, "Could not fetch by AP id"}
1261 def get_or_fetch_by_ap_id(ap_id) do
1262 user = get_cached_by_ap_id(ap_id)
1264 if !is_nil(user) and !User.needs_update?(user) do
1267 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1268 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1270 resp = fetch_by_ap_id(ap_id)
1272 if should_fetch_initial do
1273 with {:ok, %User{} = user} <- resp do
1274 fetch_initial_posts(user)
1282 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1283 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1284 if user = get_cached_by_ap_id(uri) do
1288 %User{info: %User.Info{}}
1289 |> cast(%{}, [:ap_id, :nickname, :local])
1290 |> put_change(:ap_id, uri)
1291 |> put_change(:nickname, nickname)
1292 |> put_change(:local, true)
1293 |> put_change(:follower_address, uri <> "/followers")
1295 {:ok, user} = Repo.insert(changes)
1301 def public_key_from_info(%{
1302 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1306 |> :public_key.pem_decode()
1308 |> :public_key.pem_entry_decode()
1314 def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
1315 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1318 def public_key_from_info(_), do: {:error, "not found key"}
1320 def get_public_key_for_ap_id(ap_id) do
1321 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1322 {:ok, public_key} <- public_key_from_info(user.info) do
1329 defp blank?(""), do: nil
1330 defp blank?(n), do: n
1332 def insert_or_update_user(data) do
1334 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1335 |> remote_user_creation()
1336 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1340 def ap_enabled?(%User{local: true}), do: true
1341 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1342 def ap_enabled?(_), do: false
1344 @doc "Gets or fetch a user by uri or nickname."
1345 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1346 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1347 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1349 # wait a period of time and return newest version of the User structs
1350 # this is because we have synchronous follow APIs and need to simulate them
1351 # with an async handshake
1352 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1353 with %User{} = a <- User.get_cached_by_id(a.id),
1354 %User{} = b <- User.get_cached_by_id(b.id) do
1362 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1363 with :ok <- :timer.sleep(timeout),
1364 %User{} = a <- User.get_cached_by_id(a.id),
1365 %User{} = b <- User.get_cached_by_id(b.id) do
1373 def parse_bio(bio) when is_binary(bio) and bio != "" do
1375 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1379 def parse_bio(_), do: ""
1381 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1382 # TODO: get profile URLs other than user.ap_id
1383 profile_urls = [user.ap_id]
1386 |> CommonUtils.format_input("text/plain",
1387 mentions_format: :full,
1388 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1393 def parse_bio(_, _), do: ""
1395 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1396 Repo.transaction(fn ->
1397 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1401 def tag(nickname, tags) when is_binary(nickname),
1402 do: tag(get_by_nickname(nickname), tags)
1404 def tag(%User{} = user, tags),
1405 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1407 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1408 Repo.transaction(fn ->
1409 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1413 def untag(nickname, tags) when is_binary(nickname),
1414 do: untag(get_by_nickname(nickname), tags)
1416 def untag(%User{} = user, tags),
1417 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1419 defp update_tags(%User{} = user, new_tags) do
1420 {:ok, updated_user} =
1422 |> change(%{tags: new_tags})
1423 |> update_and_set_cache()
1428 defp normalize_tags(tags) do
1431 |> Enum.map(&String.downcase(&1))
1434 defp local_nickname_regex do
1435 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1436 @extended_local_nickname_regex
1438 @strict_local_nickname_regex
1442 def local_nickname(nickname_or_mention) do
1445 |> String.split("@")
1449 def full_nickname(nickname_or_mention),
1450 do: String.trim_leading(nickname_or_mention, "@")
1452 def error_user(ap_id) do
1457 nickname: "erroruser@example.com",
1458 inserted_at: NaiveDateTime.utc_now()
1462 @spec all_superusers() :: [User.t()]
1463 def all_superusers do
1464 User.Query.build(%{super_users: true, local: true, deactivated: false})
1468 def showing_reblogs?(%User{} = user, %User{} = target) do
1469 target.ap_id not in user.info.muted_reblogs
1473 The function returns a query to get users with no activity for given interval of days.
1474 Inactive users are those who didn't read any notification, or had any activity where
1475 the user is the activity's actor, during `inactivity_threshold` days.
1476 Deactivated users will not appear in this list.
1480 iex> Pleroma.User.list_inactive_users()
1483 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1484 def list_inactive_users_query(inactivity_threshold \\ 7) do
1485 negative_inactivity_threshold = -inactivity_threshold
1486 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1487 # Subqueries are not supported in `where` clauses, join gets too complicated.
1488 has_read_notifications =
1489 from(n in Pleroma.Notification,
1490 where: n.seen == true,
1492 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1495 |> Pleroma.Repo.all()
1497 from(u in Pleroma.User,
1498 left_join: a in Pleroma.Activity,
1499 on: u.ap_id == a.actor,
1500 where: not is_nil(u.nickname),
1501 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1502 where: u.id not in ^has_read_notifications,
1505 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1506 is_nil(max(a.inserted_at))
1511 Enable or disable email notifications for user
1515 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1516 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1518 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1519 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1521 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1522 {:ok, t()} | {:error, Ecto.Changeset.t()}
1523 def switch_email_notifications(user, type, status) do
1524 info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})
1527 |> put_embed(:info, info)
1528 |> update_and_set_cache()
1532 Set `last_digest_emailed_at` value for the user to current time
1534 @spec touch_last_digest_emailed_at(t()) :: t()
1535 def touch_last_digest_emailed_at(user) do
1536 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1538 {:ok, updated_user} =
1540 |> change(%{last_digest_emailed_at: now})
1541 |> update_and_set_cache()
1546 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1547 def toggle_confirmation(%User{} = user) do
1548 need_confirmation? = !user.info.confirmation_pending
1551 User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
1555 |> put_embed(:info, info_changeset)
1556 |> update_and_set_cache()
1559 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1563 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1564 # use instance-default
1565 config = Pleroma.Config.get([:assets, :mascots])
1566 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1567 mascot = Keyword.get(config, default_mascot)
1570 "id" => "default-mascot",
1571 "url" => mascot[:url],
1572 "preview_url" => mascot[:url],
1574 "mime_type" => mascot[:mime_type]
1579 def ensure_keys_present(%User{info: info} = user) do
1583 {:ok, pem} = Keys.generate_rsa_pem()
1586 |> Ecto.Changeset.change()
1587 |> Ecto.Changeset.put_embed(:info, User.Info.set_keys(info, pem))
1588 |> update_and_set_cache()
1592 def get_ap_ids_by_nicknames(nicknames) do
1594 where: u.nickname in ^nicknames,
1600 defdelegate search(query, opts \\ []), to: User.Search
1602 defp put_password_hash(
1603 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1605 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1608 defp put_password_hash(changeset), do: changeset
1610 def is_internal_user?(%User{nickname: nil}), do: true
1611 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1612 def is_internal_user?(_), do: false