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, FlakeId.Ecto.CompatType, 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)
52 field(:following, {:array, :string}, default: [])
53 field(:ap_id, :string)
55 field(:local, :boolean, default: true)
56 field(:follower_address, :string)
57 field(:following_address, :string)
58 field(:search_rank, :float, virtual: true)
59 field(:search_type, :integer, virtual: true)
60 field(:tags, {:array, :string}, default: [])
61 field(:last_refreshed_at, :naive_datetime_usec)
62 field(:last_digest_emailed_at, :naive_datetime)
63 has_many(:notifications, Notification)
64 has_many(:registrations, Registration)
65 embeds_one(:info, User.Info)
70 def auth_active?(%User{info: %User.Info{confirmation_pending: true}}),
71 do: !Pleroma.Config.get([:instance, :account_activation_required])
73 def auth_active?(%User{}), do: true
75 def visible_for?(user, for_user \\ nil)
77 def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
79 def visible_for?(%User{} = user, for_user) do
80 auth_active?(user) || superuser?(for_user)
83 def visible_for?(_, _), do: false
85 def superuser?(%User{local: true, info: %User.Info{is_admin: true}}), do: true
86 def superuser?(%User{local: true, info: %User.Info{is_moderator: true}}), do: true
87 def superuser?(_), do: false
89 def avatar_url(user, options \\ []) do
91 %{"url" => [%{"href" => href} | _]} -> href
92 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
96 def banner_url(user, options \\ []) do
97 case user.info.banner do
98 %{"url" => [%{"href" => href} | _]} -> href
99 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
103 def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url
104 def profile_url(%User{ap_id: ap_id}), do: ap_id
105 def profile_url(_), do: nil
107 def ap_id(%User{nickname: nickname}) do
108 "#{Web.base_url()}/users/#{nickname}"
111 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
112 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
114 @spec ap_following(User.t()) :: Sring.t()
115 def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
116 def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
118 def user_info(%User{} = user, args \\ %{}) do
120 if args[:following_count],
121 do: args[:following_count],
122 else: user.info.following_count || following_count(user)
125 if args[:follower_count], do: args[:follower_count], else: user.info.follower_count
128 note_count: user.info.note_count,
129 locked: user.info.locked,
130 confirmation_pending: user.info.confirmation_pending,
131 default_scope: user.info.default_scope
133 |> Map.put(:following_count, following_count)
134 |> Map.put(:follower_count, follower_count)
137 def follow_state(%User{} = user, %User{} = target) do
138 follow_activity = Utils.fetch_latest_follow(user, target)
141 do: follow_activity.data["state"],
142 # Ideally this would be nil, but then Cachex does not commit the value
146 def get_cached_follow_state(user, target) do
147 key = "follow_state:#{user.ap_id}|#{target.ap_id}"
148 Cachex.fetch!(:user_cache, key, fn _ -> {:commit, follow_state(user, target)} end)
151 def set_follow_state_cache(user_ap_id, target_ap_id, state) do
154 "follow_state:#{user_ap_id}|#{target_ap_id}",
159 def set_info_cache(user, args) do
160 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user, args))
163 @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
164 def restrict_deactivated(query) do
166 where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info)
170 def following_count(%User{following: []}), do: 0
172 def following_count(%User{} = user) do
174 |> get_friends_query()
175 |> Repo.aggregate(:count, :id)
178 def remote_user_creation(params) do
179 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
180 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
182 params = Map.put(params, :info, params[:info] || %{})
183 info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])
187 |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
188 |> validate_required([:name, :ap_id])
189 |> unique_constraint(:nickname)
190 |> validate_format(:nickname, @email_regex)
191 |> validate_length(:bio, max: bio_limit)
192 |> validate_length(:name, max: name_limit)
193 |> put_change(:local, false)
194 |> put_embed(:info, info_cng)
197 case info_cng.changes[:source_data] do
198 %{"followers" => followers, "following" => following} ->
200 |> put_change(:follower_address, followers)
201 |> put_change(:following_address, following)
204 followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
207 |> put_change(:follower_address, followers)
214 def update_changeset(struct, params \\ %{}) do
215 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
216 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
219 |> cast(params, [:bio, :name, :avatar, :following])
220 |> unique_constraint(:nickname)
221 |> validate_format(:nickname, local_nickname_regex())
222 |> validate_length(:bio, max: bio_limit)
223 |> validate_length(:name, min: 1, max: name_limit)
226 def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
227 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
228 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
230 params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
231 info_cng = User.Info.user_upgrade(struct.info, params[:info], remote?)
242 |> unique_constraint(:nickname)
243 |> validate_format(:nickname, local_nickname_regex())
244 |> validate_length(:bio, max: bio_limit)
245 |> validate_length(:name, max: name_limit)
246 |> put_embed(:info, info_cng)
249 def password_update_changeset(struct, params) do
251 |> cast(params, [:password, :password_confirmation])
252 |> validate_required([:password, :password_confirmation])
253 |> validate_confirmation(:password)
257 @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
258 def reset_password(%User{id: user_id} = user, data) do
261 |> Multi.update(:user, password_update_changeset(user, data))
262 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
263 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
265 case Repo.transaction(multi) do
266 {:ok, %{user: user} = _} -> set_cache(user)
267 {:error, _, changeset, _} -> {:error, changeset}
271 def register_changeset(struct, params \\ %{}, opts \\ []) do
272 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
273 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
276 if is_nil(opts[:need_confirmation]) do
277 Pleroma.Config.get([:instance, :account_activation_required])
279 opts[:need_confirmation]
283 User.Info.confirmation_changeset(%User.Info{}, need_confirmation: need_confirmation?)
287 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
288 |> validate_required([:name, :nickname, :password, :password_confirmation])
289 |> validate_confirmation(:password)
290 |> unique_constraint(:email)
291 |> unique_constraint(:nickname)
292 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
293 |> validate_format(:nickname, local_nickname_regex())
294 |> validate_format(:email, @email_regex)
295 |> validate_length(:bio, max: bio_limit)
296 |> validate_length(:name, min: 1, max: name_limit)
297 |> put_change(:info, info_change)
300 if opts[:external] do
303 validate_required(changeset, [:email])
306 if changeset.valid? do
307 ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
308 followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
312 |> put_change(:ap_id, ap_id)
313 |> unique_constraint(:ap_id)
314 |> put_change(:following, [followers])
315 |> put_change(:follower_address, followers)
321 defp autofollow_users(user) do
322 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
325 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
328 follow_all(user, autofollowed_users)
331 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
332 def register(%Ecto.Changeset{} = changeset) do
333 with {:ok, user} <- Repo.insert(changeset),
334 {:ok, user} <- post_register_action(user) do
339 def post_register_action(%User{} = user) do
340 with {:ok, user} <- autofollow_users(user),
341 {:ok, user} <- set_cache(user),
342 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
343 {:ok, _} <- try_send_confirmation_email(user) do
348 def try_send_confirmation_email(%User{} = user) do
349 if user.info.confirmation_pending &&
350 Pleroma.Config.get([:instance, :account_activation_required]) do
352 |> Pleroma.Emails.UserEmail.account_confirmation_email()
353 |> Pleroma.Emails.Mailer.deliver_async()
361 def needs_update?(%User{local: true}), do: false
363 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
365 def needs_update?(%User{local: false} = user) do
366 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
369 def needs_update?(_), do: true
371 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
372 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
376 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
377 follow(follower, followed)
380 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
381 if not User.ap_enabled?(followed) do
382 follow(follower, followed)
388 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
389 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
390 def follow_all(follower, followeds) do
393 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
394 |> Enum.map(fn %{follower_address: fa} -> fa end)
398 where: u.id == ^follower.id,
403 "array(select distinct unnest (array_cat(?, ?)))",
412 {1, [follower]} = Repo.update_all(q, [])
414 Enum.each(followeds, fn followed ->
415 update_follower_count(followed)
421 def follow(%User{} = follower, %User{info: info} = followed) do
422 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
423 ap_followers = followed.follower_address
427 {:error, "Could not follow user: You are deactivated."}
429 deny_follow_blocked and blocks?(followed, follower) ->
430 {:error, "Could not follow user: #{followed.nickname} blocked you."}
433 if !followed.local && follower.local && !ap_enabled?(followed) do
434 Websub.subscribe(follower, followed)
439 where: u.id == ^follower.id,
440 update: [push: [following: ^ap_followers]],
444 {1, [follower]} = Repo.update_all(q, [])
446 follower = maybe_update_following_count(follower)
448 {:ok, _} = update_follower_count(followed)
454 def unfollow(%User{} = follower, %User{} = followed) do
455 ap_followers = followed.follower_address
457 if following?(follower, followed) and follower.ap_id != followed.ap_id do
460 where: u.id == ^follower.id,
461 update: [pull: [following: ^ap_followers]],
465 {1, [follower]} = Repo.update_all(q, [])
467 follower = maybe_update_following_count(follower)
469 {:ok, followed} = update_follower_count(followed)
473 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
475 {:error, "Not subscribed!"}
479 @spec following?(User.t(), User.t()) :: boolean
480 def following?(%User{} = follower, %User{} = followed) do
481 Enum.member?(follower.following, followed.follower_address)
484 def locked?(%User{} = user) do
485 user.info.locked || false
489 Repo.get_by(User, id: id)
492 def get_by_ap_id(ap_id) do
493 Repo.get_by(User, ap_id: ap_id)
496 def get_all_by_ap_id(ap_ids) do
497 from(u in __MODULE__,
498 where: u.ap_id in ^ap_ids
503 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
504 # of the ap_id and the domain and tries to get that user
505 def get_by_guessed_nickname(ap_id) do
506 domain = URI.parse(ap_id).host
507 name = List.last(String.split(ap_id, "/"))
508 nickname = "#{name}@#{domain}"
510 get_cached_by_nickname(nickname)
513 def set_cache({:ok, user}), do: set_cache(user)
514 def set_cache({:error, err}), do: {:error, err}
516 def set_cache(%User{} = user) do
517 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
518 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
519 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
523 def update_and_set_cache(changeset) do
524 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
531 def invalidate_cache(user) do
532 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
533 Cachex.del(:user_cache, "nickname:#{user.nickname}")
534 Cachex.del(:user_cache, "user_info:#{user.id}")
537 def get_cached_by_ap_id(ap_id) do
538 key = "ap_id:#{ap_id}"
539 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
542 def get_cached_by_id(id) do
546 Cachex.fetch!(:user_cache, key, fn _ ->
550 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
551 {:commit, user.ap_id}
557 get_cached_by_ap_id(ap_id)
560 def get_cached_by_nickname(nickname) do
561 key = "nickname:#{nickname}"
563 Cachex.fetch!(:user_cache, key, fn ->
564 user_result = get_or_fetch_by_nickname(nickname)
567 {:ok, user} -> {:commit, user}
568 {:error, _error} -> {:ignore, nil}
573 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
574 restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
577 is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
578 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
580 restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
581 get_cached_by_nickname(nickname_or_id)
583 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
584 get_cached_by_nickname(nickname_or_id)
591 def get_by_nickname(nickname) do
592 Repo.get_by(User, nickname: nickname) ||
593 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
594 Repo.get_by(User, nickname: local_nickname(nickname))
598 def get_by_email(email), do: Repo.get_by(User, email: email)
600 def get_by_nickname_or_email(nickname_or_email) do
601 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
604 def get_cached_user_info(user) do
605 key = "user_info:#{user.id}"
606 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
609 def fetch_by_nickname(nickname) do
610 ap_try = ActivityPub.make_user_from_nickname(nickname)
613 {:ok, user} -> {:ok, user}
614 _ -> OStatus.make_user(nickname)
618 def get_or_fetch_by_nickname(nickname) do
619 with %User{} = user <- get_by_nickname(nickname) do
623 with [_nick, _domain] <- String.split(nickname, "@"),
624 {:ok, user} <- fetch_by_nickname(nickname) do
625 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
626 fetch_initial_posts(user)
631 _e -> {:error, "not found " <> nickname}
636 @doc "Fetch some posts when the user has just been federated with"
637 def fetch_initial_posts(user),
638 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:fetch_initial_posts, user])
640 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
641 def get_followers_query(%User{} = user, nil) do
642 User.Query.build(%{followers: user, deactivated: false})
645 def get_followers_query(user, page) do
646 from(u in get_followers_query(user, nil))
647 |> User.Query.paginate(page, 20)
650 @spec get_followers_query(User.t()) :: Ecto.Query.t()
651 def get_followers_query(user), do: get_followers_query(user, nil)
653 @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
654 def get_followers(user, page \\ nil) do
655 q = get_followers_query(user, page)
660 @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
661 def get_external_followers(user, page \\ nil) do
664 |> get_followers_query(page)
665 |> User.Query.build(%{external: true})
670 def get_followers_ids(user, page \\ nil) do
671 q = get_followers_query(user, page)
673 Repo.all(from(u in q, select: u.id))
676 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
677 def get_friends_query(%User{} = user, nil) do
678 User.Query.build(%{friends: user, deactivated: false})
681 def get_friends_query(user, page) do
682 from(u in get_friends_query(user, nil))
683 |> User.Query.paginate(page, 20)
686 @spec get_friends_query(User.t()) :: Ecto.Query.t()
687 def get_friends_query(user), do: get_friends_query(user, nil)
689 def get_friends(user, page \\ nil) do
690 q = get_friends_query(user, page)
695 def get_friends_ids(user, page \\ nil) do
696 q = get_friends_query(user, page)
698 Repo.all(from(u in q, select: u.id))
701 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
702 def get_follow_requests(%User{} = user) do
704 Activity.follow_requests_for_actor(user)
705 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
706 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
707 |> group_by([a, u], u.id)
714 def increase_note_count(%User{} = user) do
716 |> where(id: ^user.id)
721 "safe_jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
728 |> Repo.update_all([])
730 {1, [user]} -> set_cache(user)
735 def decrease_note_count(%User{} = user) do
737 |> where(id: ^user.id)
742 "safe_jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
749 |> Repo.update_all([])
751 {1, [user]} -> set_cache(user)
756 def update_note_count(%User{} = user) do
760 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
764 note_count = Repo.one(note_count_query)
766 info_cng = User.Info.set_note_count(user.info, note_count)
770 |> put_embed(:info, info_cng)
771 |> update_and_set_cache()
774 @spec maybe_fetch_follow_information(User.t()) :: User.t()
775 def maybe_fetch_follow_information(user) do
776 with {:ok, user} <- fetch_follow_information(user) do
780 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
786 def fetch_follow_information(user) do
787 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
788 info_cng = User.Info.follow_information_update(user.info, info)
793 |> put_embed(:info, info_cng)
795 update_and_set_cache(changeset)
802 def update_follower_count(%User{} = user) do
803 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
804 follower_count_query =
805 User.Query.build(%{followers: user, deactivated: false})
806 |> select([u], %{count: count(u.id)})
809 |> where(id: ^user.id)
810 |> join(:inner, [u], s in subquery(follower_count_query))
815 "safe_jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
822 |> Repo.update_all([])
824 {1, [user]} -> set_cache(user)
828 {:ok, maybe_fetch_follow_information(user)}
832 @spec maybe_update_following_count(User.t()) :: User.t()
833 def maybe_update_following_count(%User{local: false} = user) do
834 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
835 maybe_fetch_follow_information(user)
841 def maybe_update_following_count(user), do: user
843 def remove_duplicated_following(%User{following: following} = user) do
844 uniq_following = Enum.uniq(following)
846 if length(following) == length(uniq_following) do
850 |> update_changeset(%{following: uniq_following})
851 |> update_and_set_cache()
855 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
856 def get_users_from_set(ap_ids, local_only \\ true) do
857 criteria = %{ap_id: ap_ids, deactivated: false}
858 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
860 User.Query.build(criteria)
864 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
865 def get_recipients_from_activity(%Activity{recipients: to}) do
866 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
870 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
871 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
875 User.Info.add_to_mutes(info, ap_id)
876 |> User.Info.add_to_muted_notifications(info, ap_id, notifications?)
880 |> put_embed(:info, info_cng)
882 update_and_set_cache(cng)
885 def unmute(muter, %{ap_id: ap_id}) do
889 User.Info.remove_from_mutes(info, ap_id)
890 |> User.Info.remove_from_muted_notifications(info, ap_id)
894 |> put_embed(:info, info_cng)
896 update_and_set_cache(cng)
899 def subscribe(subscriber, %{ap_id: ap_id}) do
900 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
902 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
903 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
906 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
910 |> User.Info.add_to_subscribers(subscriber.ap_id)
913 |> put_embed(:info, info_cng)
914 |> update_and_set_cache()
919 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
920 with %User{} = user <- get_cached_by_ap_id(ap_id) do
923 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
926 |> put_embed(:info, info_cng)
927 |> update_and_set_cache()
931 def block(blocker, %User{ap_id: ap_id} = blocked) do
932 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
934 if following?(blocker, blocked) do
935 {:ok, blocker, _} = unfollow(blocker, blocked)
941 # clear any requested follows as well
943 case CommonAPI.reject_follow_request(blocked, blocker) do
944 {:ok, %User{} = updated_blocked} -> updated_blocked
949 if subscribed_to?(blocked, blocker) do
950 {:ok, blocker} = unsubscribe(blocked, blocker)
956 if following?(blocked, blocker) do
957 unfollow(blocked, blocker)
960 {:ok, blocker} = update_follower_count(blocker)
964 |> User.Info.add_to_block(ap_id)
968 |> put_embed(:info, info_cng)
970 update_and_set_cache(cng)
973 # helper to handle the block given only an actor's AP id
974 def block(blocker, %{ap_id: ap_id}) do
975 block(blocker, get_cached_by_ap_id(ap_id))
978 def unblock(blocker, %{ap_id: ap_id}) do
981 |> User.Info.remove_from_block(ap_id)
985 |> put_embed(:info, info_cng)
987 update_and_set_cache(cng)
990 def mutes?(nil, _), do: false
991 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
993 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
994 def muted_notifications?(nil, _), do: false
996 def muted_notifications?(user, %{ap_id: ap_id}),
997 do: Enum.member?(user.info.muted_notifications, ap_id)
999 def blocks?(%User{} = user, %User{} = target) do
1000 blocks_ap_id?(user, target) || blocks_domain?(user, target)
1003 def blocks?(nil, _), do: false
1005 def blocks_ap_id?(%User{} = user, %User{} = target) do
1006 Enum.member?(user.info.blocks, target.ap_id)
1009 def blocks_ap_id?(_, _), do: false
1011 def blocks_domain?(%User{} = user, %User{} = target) do
1012 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
1013 %{host: host} = URI.parse(target.ap_id)
1014 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1017 def blocks_domain?(_, _), do: false
1019 def subscribed_to?(user, %{ap_id: ap_id}) do
1020 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1021 Enum.member?(target.info.subscribers, user.ap_id)
1025 @spec muted_users(User.t()) :: [User.t()]
1026 def muted_users(user) do
1027 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
1031 @spec blocked_users(User.t()) :: [User.t()]
1032 def blocked_users(user) do
1033 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
1037 @spec subscribers(User.t()) :: [User.t()]
1038 def subscribers(user) do
1039 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
1043 def block_domain(user, domain) do
1046 |> User.Info.add_to_domain_block(domain)
1050 |> put_embed(:info, info_cng)
1052 update_and_set_cache(cng)
1055 def unblock_domain(user, domain) do
1058 |> User.Info.remove_from_domain_block(domain)
1062 |> put_embed(:info, info_cng)
1064 update_and_set_cache(cng)
1067 def deactivate_async(user, status \\ true) do
1068 PleromaJobQueue.enqueue(:background, __MODULE__, [:deactivate_async, user, status])
1071 def deactivate(%User{} = user, status \\ true) do
1072 info_cng = User.Info.set_activation_status(user.info, status)
1074 with {:ok, friends} <- User.get_friends(user),
1075 {:ok, followers} <- User.get_followers(user),
1079 |> put_embed(:info, info_cng)
1080 |> update_and_set_cache() do
1081 Enum.each(followers, &invalidate_cache(&1))
1082 Enum.each(friends, &update_follower_count(&1))
1088 def update_notification_settings(%User{} = user, settings \\ %{}) do
1089 info_changeset = User.Info.update_notification_settings(user.info, settings)
1092 |> put_embed(:info, info_changeset)
1093 |> update_and_set_cache()
1096 @spec delete(User.t()) :: :ok
1097 def delete(%User{} = user),
1098 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:delete, user])
1100 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1101 def perform(:delete, %User{} = user) do
1102 {:ok, _user} = ActivityPub.delete(user)
1104 # Remove all relationships
1105 {:ok, followers} = User.get_followers(user)
1107 Enum.each(followers, fn follower ->
1108 ActivityPub.unfollow(follower, user)
1109 User.unfollow(follower, user)
1112 {:ok, friends} = User.get_friends(user)
1114 Enum.each(friends, fn followed ->
1115 ActivityPub.unfollow(user, followed)
1116 User.unfollow(user, followed)
1119 delete_user_activities(user)
1120 invalidate_cache(user)
1124 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1125 def perform(:fetch_initial_posts, %User{} = user) do
1126 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1129 # Insert all the posts in reverse order, so they're in the right order on the timeline
1130 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
1131 &Pleroma.Web.Federator.incoming_ap_doc/1
1137 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1139 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1140 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1141 when is_list(blocked_identifiers) do
1143 blocked_identifiers,
1144 fn blocked_identifier ->
1145 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1146 {:ok, blocker} <- block(blocker, blocked),
1147 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1151 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1158 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1159 def perform(:follow_import, %User{} = follower, followed_identifiers)
1160 when is_list(followed_identifiers) do
1162 followed_identifiers,
1163 fn followed_identifier ->
1164 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1165 {:ok, follower} <- maybe_direct_follow(follower, followed),
1166 {:ok, _} <- ActivityPub.follow(follower, followed) do
1170 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1177 @spec external_users_query() :: Ecto.Query.t()
1178 def external_users_query do
1186 @spec external_users(keyword()) :: [User.t()]
1187 def external_users(opts \\ []) do
1189 external_users_query()
1190 |> select([u], struct(u, [:id, :ap_id, :info]))
1194 do: where(query, [u], u.id > ^opts[:max_id]),
1199 do: limit(query, ^opts[:limit]),
1205 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers),
1207 PleromaJobQueue.enqueue(:background, __MODULE__, [
1213 def follow_import(%User{} = follower, followed_identifiers) when is_list(followed_identifiers),
1215 PleromaJobQueue.enqueue(:background, __MODULE__, [
1218 followed_identifiers
1221 def delete_user_activities(%User{ap_id: ap_id} = user) do
1223 |> Activity.Queries.by_actor()
1224 |> RepoStreamer.chunk_stream(50)
1225 |> Stream.each(fn activities ->
1226 Enum.each(activities, &delete_activity(&1))
1233 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1235 |> Object.normalize()
1236 |> ActivityPub.delete()
1239 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1240 user = get_cached_by_ap_id(activity.actor)
1241 object = Object.normalize(activity)
1243 ActivityPub.unlike(user, object)
1246 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1247 user = get_cached_by_ap_id(activity.actor)
1248 object = Object.normalize(activity)
1250 ActivityPub.unannounce(user, object)
1253 defp delete_activity(_activity), do: "Doing nothing"
1255 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1256 Pleroma.HTML.Scrubber.TwitterText
1259 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1261 def fetch_by_ap_id(ap_id) do
1262 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1269 case OStatus.make_user(ap_id) do
1270 {:ok, user} -> {:ok, user}
1271 _ -> {:error, "Could not fetch by AP id"}
1276 def get_or_fetch_by_ap_id(ap_id) do
1277 user = get_cached_by_ap_id(ap_id)
1279 if !is_nil(user) and !User.needs_update?(user) do
1282 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1283 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1285 resp = fetch_by_ap_id(ap_id)
1287 if should_fetch_initial do
1288 with {:ok, %User{} = user} <- resp do
1289 fetch_initial_posts(user)
1297 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1298 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1299 if user = get_cached_by_ap_id(uri) do
1303 %User{info: %User.Info{}}
1304 |> cast(%{}, [:ap_id, :nickname, :local])
1305 |> put_change(:ap_id, uri)
1306 |> put_change(:nickname, nickname)
1307 |> put_change(:local, true)
1308 |> put_change(:follower_address, uri <> "/followers")
1310 {:ok, user} = Repo.insert(changes)
1316 def public_key_from_info(%{
1317 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1321 |> :public_key.pem_decode()
1323 |> :public_key.pem_entry_decode()
1329 def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
1330 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1333 def public_key_from_info(_), do: {:error, "not found key"}
1335 def get_public_key_for_ap_id(ap_id) do
1336 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1337 {:ok, public_key} <- public_key_from_info(user.info) do
1344 defp blank?(""), do: nil
1345 defp blank?(n), do: n
1347 def insert_or_update_user(data) do
1349 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1350 |> remote_user_creation()
1351 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1355 def ap_enabled?(%User{local: true}), do: true
1356 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1357 def ap_enabled?(_), do: false
1359 @doc "Gets or fetch a user by uri or nickname."
1360 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1361 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1362 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1364 # wait a period of time and return newest version of the User structs
1365 # this is because we have synchronous follow APIs and need to simulate them
1366 # with an async handshake
1367 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1368 with %User{} = a <- User.get_cached_by_id(a.id),
1369 %User{} = b <- User.get_cached_by_id(b.id) do
1377 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1378 with :ok <- :timer.sleep(timeout),
1379 %User{} = a <- User.get_cached_by_id(a.id),
1380 %User{} = b <- User.get_cached_by_id(b.id) do
1388 def parse_bio(bio) when is_binary(bio) and bio != "" do
1390 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1394 def parse_bio(_), do: ""
1396 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1397 # TODO: get profile URLs other than user.ap_id
1398 profile_urls = [user.ap_id]
1401 |> CommonUtils.format_input("text/plain",
1402 mentions_format: :full,
1403 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1408 def parse_bio(_, _), do: ""
1410 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1411 Repo.transaction(fn ->
1412 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1416 def tag(nickname, tags) when is_binary(nickname),
1417 do: tag(get_by_nickname(nickname), tags)
1419 def tag(%User{} = user, tags),
1420 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1422 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1423 Repo.transaction(fn ->
1424 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1428 def untag(nickname, tags) when is_binary(nickname),
1429 do: untag(get_by_nickname(nickname), tags)
1431 def untag(%User{} = user, tags),
1432 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1434 defp update_tags(%User{} = user, new_tags) do
1435 {:ok, updated_user} =
1437 |> change(%{tags: new_tags})
1438 |> update_and_set_cache()
1443 defp normalize_tags(tags) do
1446 |> Enum.map(&String.downcase(&1))
1449 defp local_nickname_regex do
1450 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1451 @extended_local_nickname_regex
1453 @strict_local_nickname_regex
1457 def local_nickname(nickname_or_mention) do
1460 |> String.split("@")
1464 def full_nickname(nickname_or_mention),
1465 do: String.trim_leading(nickname_or_mention, "@")
1467 def error_user(ap_id) do
1472 nickname: "erroruser@example.com",
1473 inserted_at: NaiveDateTime.utc_now()
1477 @spec all_superusers() :: [User.t()]
1478 def all_superusers do
1479 User.Query.build(%{super_users: true, local: true, deactivated: false})
1483 def showing_reblogs?(%User{} = user, %User{} = target) do
1484 target.ap_id not in user.info.muted_reblogs
1488 The function returns a query to get users with no activity for given interval of days.
1489 Inactive users are those who didn't read any notification, or had any activity where
1490 the user is the activity's actor, during `inactivity_threshold` days.
1491 Deactivated users will not appear in this list.
1495 iex> Pleroma.User.list_inactive_users()
1498 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1499 def list_inactive_users_query(inactivity_threshold \\ 7) do
1500 negative_inactivity_threshold = -inactivity_threshold
1501 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1502 # Subqueries are not supported in `where` clauses, join gets too complicated.
1503 has_read_notifications =
1504 from(n in Pleroma.Notification,
1505 where: n.seen == true,
1507 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1510 |> Pleroma.Repo.all()
1512 from(u in Pleroma.User,
1513 left_join: a in Pleroma.Activity,
1514 on: u.ap_id == a.actor,
1515 where: not is_nil(u.nickname),
1516 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1517 where: u.id not in ^has_read_notifications,
1520 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1521 is_nil(max(a.inserted_at))
1526 Enable or disable email notifications for user
1530 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1531 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1533 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1534 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1536 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1537 {:ok, t()} | {:error, Ecto.Changeset.t()}
1538 def switch_email_notifications(user, type, status) do
1539 info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})
1542 |> put_embed(:info, info)
1543 |> update_and_set_cache()
1547 Set `last_digest_emailed_at` value for the user to current time
1549 @spec touch_last_digest_emailed_at(t()) :: t()
1550 def touch_last_digest_emailed_at(user) do
1551 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1553 {:ok, updated_user} =
1555 |> change(%{last_digest_emailed_at: now})
1556 |> update_and_set_cache()
1561 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1562 def toggle_confirmation(%User{} = user) do
1563 need_confirmation? = !user.info.confirmation_pending
1566 User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
1570 |> put_embed(:info, info_changeset)
1571 |> update_and_set_cache()
1574 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1578 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1579 # use instance-default
1580 config = Pleroma.Config.get([:assets, :mascots])
1581 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1582 mascot = Keyword.get(config, default_mascot)
1585 "id" => "default-mascot",
1586 "url" => mascot[:url],
1587 "preview_url" => mascot[:url],
1589 "mime_type" => mascot[:mime_type]
1594 def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
1596 def ensure_keys_present(%User{} = user) do
1597 with {:ok, pem} <- Keys.generate_rsa_pem() do
1599 |> cast(%{keys: pem}, [:keys])
1600 |> validate_required([:keys])
1601 |> update_and_set_cache()
1605 def get_ap_ids_by_nicknames(nicknames) do
1607 where: u.nickname in ^nicknames,
1613 defdelegate search(query, opts \\ []), to: User.Search
1615 defp put_password_hash(
1616 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1618 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1621 defp put_password_hash(changeset), do: changeset
1623 def is_internal_user?(%User{nickname: nil}), do: true
1624 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1625 def is_internal_user?(_), do: false
1627 def change_email(user, email) do
1629 |> cast(%{email: email}, [:email])
1630 |> validate_required([:email])
1631 |> unique_constraint(:email)
1632 |> validate_format(:email, @email_regex)
1633 |> update_and_set_cache()