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
29 alias Pleroma.Workers.BackgroundWorker
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_-]+$/
43 defdelegate worker_args(queue), to: Pleroma.Workers.Helper
47 field(:email, :string)
49 field(:nickname, :string)
50 field(:password_hash, :string)
51 field(:password, :string, virtual: true)
52 field(:password_confirmation, :string, virtual: true)
53 field(:following, {:array, :string}, default: [])
54 field(:ap_id, :string)
56 field(:local, :boolean, default: true)
57 field(:follower_address, :string)
58 field(:following_address, :string)
59 field(:search_rank, :float, virtual: true)
60 field(:search_type, :integer, virtual: true)
61 field(:tags, {:array, :string}, default: [])
62 field(:last_refreshed_at, :naive_datetime_usec)
63 field(:last_digest_emailed_at, :naive_datetime)
64 has_many(:notifications, Notification)
65 has_many(:registrations, Registration)
66 embeds_one(:info, User.Info)
71 def auth_active?(%User{info: %User.Info{confirmation_pending: true}}),
72 do: !Pleroma.Config.get([:instance, :account_activation_required])
74 def auth_active?(%User{}), do: true
76 def visible_for?(user, for_user \\ nil)
78 def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
80 def visible_for?(%User{} = user, for_user) do
81 auth_active?(user) || superuser?(for_user)
84 def visible_for?(_, _), do: false
86 def superuser?(%User{local: true, info: %User.Info{is_admin: true}}), do: true
87 def superuser?(%User{local: true, info: %User.Info{is_moderator: true}}), do: true
88 def superuser?(_), do: false
90 def avatar_url(user, options \\ []) do
92 %{"url" => [%{"href" => href} | _]} -> href
93 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
97 def banner_url(user, options \\ []) do
98 case user.info.banner do
99 %{"url" => [%{"href" => href} | _]} -> href
100 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
104 def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url
105 def profile_url(%User{ap_id: ap_id}), do: ap_id
106 def profile_url(_), do: nil
108 def ap_id(%User{nickname: nickname}) do
109 "#{Web.base_url()}/users/#{nickname}"
112 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
113 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
115 @spec ap_following(User.t()) :: Sring.t()
116 def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
117 def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
119 def user_info(%User{} = user, args \\ %{}) do
121 if args[:following_count],
122 do: args[:following_count],
123 else: user.info.following_count || following_count(user)
126 if args[:follower_count], do: args[:follower_count], else: user.info.follower_count
129 note_count: user.info.note_count,
130 locked: user.info.locked,
131 confirmation_pending: user.info.confirmation_pending,
132 default_scope: user.info.default_scope
134 |> Map.put(:following_count, following_count)
135 |> Map.put(:follower_count, follower_count)
138 def set_info_cache(user, args) do
139 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user, args))
142 @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
143 def restrict_deactivated(query) do
145 where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info)
149 def following_count(%User{following: []}), do: 0
151 def following_count(%User{} = user) do
153 |> get_friends_query()
154 |> Repo.aggregate(:count, :id)
157 def remote_user_creation(params) do
158 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
159 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
161 params = Map.put(params, :info, params[:info] || %{})
162 info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])
166 |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
167 |> validate_required([:name, :ap_id])
168 |> unique_constraint(:nickname)
169 |> validate_format(:nickname, @email_regex)
170 |> validate_length(:bio, max: bio_limit)
171 |> validate_length(:name, max: name_limit)
172 |> put_change(:local, false)
173 |> put_embed(:info, info_cng)
176 case info_cng.changes[:source_data] do
177 %{"followers" => followers, "following" => following} ->
179 |> put_change(:follower_address, followers)
180 |> put_change(:following_address, following)
183 followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
186 |> put_change(:follower_address, followers)
193 def update_changeset(struct, params \\ %{}) do
194 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
195 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
198 |> cast(params, [:bio, :name, :avatar, :following])
199 |> unique_constraint(:nickname)
200 |> validate_format(:nickname, local_nickname_regex())
201 |> validate_length(:bio, max: bio_limit)
202 |> validate_length(:name, min: 1, max: name_limit)
205 def upgrade_changeset(struct, params \\ %{}) do
206 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
207 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
209 params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
210 info_cng = User.Info.user_upgrade(struct.info, params[:info])
221 |> unique_constraint(:nickname)
222 |> validate_format(:nickname, local_nickname_regex())
223 |> validate_length(:bio, max: bio_limit)
224 |> validate_length(:name, max: name_limit)
225 |> put_embed(:info, info_cng)
228 def password_update_changeset(struct, params) do
230 |> cast(params, [:password, :password_confirmation])
231 |> validate_required([:password, :password_confirmation])
232 |> validate_confirmation(:password)
236 @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
237 def reset_password(%User{id: user_id} = user, data) do
240 |> Multi.update(:user, password_update_changeset(user, data))
241 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
242 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
244 case Repo.transaction(multi) do
245 {:ok, %{user: user} = _} -> set_cache(user)
246 {:error, _, changeset, _} -> {:error, changeset}
250 def register_changeset(struct, params \\ %{}, opts \\ []) do
251 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
252 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
255 if is_nil(opts[:need_confirmation]) do
256 Pleroma.Config.get([:instance, :account_activation_required])
258 opts[:need_confirmation]
262 User.Info.confirmation_changeset(%User.Info{}, need_confirmation: need_confirmation?)
266 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
267 |> validate_required([:name, :nickname, :password, :password_confirmation])
268 |> validate_confirmation(:password)
269 |> unique_constraint(:email)
270 |> unique_constraint(:nickname)
271 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
272 |> validate_format(:nickname, local_nickname_regex())
273 |> validate_format(:email, @email_regex)
274 |> validate_length(:bio, max: bio_limit)
275 |> validate_length(:name, min: 1, max: name_limit)
276 |> put_change(:info, info_change)
279 if opts[:external] do
282 validate_required(changeset, [:email])
285 if changeset.valid? do
286 ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
287 followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
291 |> put_change(:ap_id, ap_id)
292 |> unique_constraint(:ap_id)
293 |> put_change(:following, [followers])
294 |> put_change(:follower_address, followers)
300 defp autofollow_users(user) do
301 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
304 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
307 follow_all(user, autofollowed_users)
310 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
311 def register(%Ecto.Changeset{} = changeset) do
312 with {:ok, user} <- Repo.insert(changeset),
313 {:ok, user} <- autofollow_users(user),
314 {:ok, user} <- set_cache(user),
315 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
316 {:ok, _} <- try_send_confirmation_email(user) do
321 def try_send_confirmation_email(%User{} = user) do
322 if user.info.confirmation_pending &&
323 Pleroma.Config.get([:instance, :account_activation_required]) do
325 |> Pleroma.Emails.UserEmail.account_confirmation_email()
326 |> Pleroma.Emails.Mailer.deliver_async()
334 def needs_update?(%User{local: true}), do: false
336 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
338 def needs_update?(%User{local: false} = user) do
339 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
342 def needs_update?(_), do: true
344 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
345 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
349 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
350 follow(follower, followed)
353 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
354 if not User.ap_enabled?(followed) do
355 follow(follower, followed)
361 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
362 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
363 def follow_all(follower, followeds) do
366 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
367 |> Enum.map(fn %{follower_address: fa} -> fa end)
371 where: u.id == ^follower.id,
376 "array(select distinct unnest (array_cat(?, ?)))",
385 {1, [follower]} = Repo.update_all(q, [])
387 Enum.each(followeds, fn followed ->
388 update_follower_count(followed)
394 def follow(%User{} = follower, %User{info: info} = followed) do
395 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
396 ap_followers = followed.follower_address
400 {:error, "Could not follow user: You are deactivated."}
402 deny_follow_blocked and blocks?(followed, follower) ->
403 {:error, "Could not follow user: #{followed.nickname} blocked you."}
406 if !followed.local && follower.local && !ap_enabled?(followed) do
407 Websub.subscribe(follower, followed)
412 where: u.id == ^follower.id,
413 update: [push: [following: ^ap_followers]],
417 {1, [follower]} = Repo.update_all(q, [])
419 follower = maybe_update_following_count(follower)
421 {:ok, _} = update_follower_count(followed)
427 def unfollow(%User{} = follower, %User{} = followed) do
428 ap_followers = followed.follower_address
430 if following?(follower, followed) and follower.ap_id != followed.ap_id do
433 where: u.id == ^follower.id,
434 update: [pull: [following: ^ap_followers]],
438 {1, [follower]} = Repo.update_all(q, [])
440 follower = maybe_update_following_count(follower)
442 {:ok, followed} = update_follower_count(followed)
446 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
448 {:error, "Not subscribed!"}
452 @spec following?(User.t(), User.t()) :: boolean
453 def following?(%User{} = follower, %User{} = followed) do
454 Enum.member?(follower.following, followed.follower_address)
457 def locked?(%User{} = user) do
458 user.info.locked || false
462 Repo.get_by(User, id: id)
465 def get_by_ap_id(ap_id) do
466 Repo.get_by(User, ap_id: ap_id)
469 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
470 # of the ap_id and the domain and tries to get that user
471 def get_by_guessed_nickname(ap_id) do
472 domain = URI.parse(ap_id).host
473 name = List.last(String.split(ap_id, "/"))
474 nickname = "#{name}@#{domain}"
476 get_cached_by_nickname(nickname)
479 def set_cache({:ok, user}), do: set_cache(user)
480 def set_cache({:error, err}), do: {:error, err}
482 def set_cache(%User{} = user) do
483 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
484 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
485 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
489 def update_and_set_cache(changeset) do
490 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
497 def invalidate_cache(user) do
498 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
499 Cachex.del(:user_cache, "nickname:#{user.nickname}")
500 Cachex.del(:user_cache, "user_info:#{user.id}")
503 def get_cached_by_ap_id(ap_id) do
504 key = "ap_id:#{ap_id}"
505 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
508 def get_cached_by_id(id) do
512 Cachex.fetch!(:user_cache, key, fn _ ->
516 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
517 {:commit, user.ap_id}
523 get_cached_by_ap_id(ap_id)
526 def get_cached_by_nickname(nickname) do
527 key = "nickname:#{nickname}"
529 Cachex.fetch!(:user_cache, key, fn ->
530 user_result = get_or_fetch_by_nickname(nickname)
533 {:ok, user} -> {:commit, user}
534 {:error, _error} -> {:ignore, nil}
539 def get_cached_by_nickname_or_id(nickname_or_id) do
540 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
543 def get_by_nickname(nickname) do
544 Repo.get_by(User, nickname: nickname) ||
545 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
546 Repo.get_by(User, nickname: local_nickname(nickname))
550 def get_by_email(email), do: Repo.get_by(User, email: email)
552 def get_by_nickname_or_email(nickname_or_email) do
553 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
556 def get_cached_user_info(user) do
557 key = "user_info:#{user.id}"
558 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
561 def fetch_by_nickname(nickname) do
562 ap_try = ActivityPub.make_user_from_nickname(nickname)
565 {:ok, user} -> {:ok, user}
566 _ -> OStatus.make_user(nickname)
570 def get_or_fetch_by_nickname(nickname) do
571 with %User{} = user <- get_by_nickname(nickname) do
575 with [_nick, _domain] <- String.split(nickname, "@"),
576 {:ok, user} <- fetch_by_nickname(nickname) do
577 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
578 fetch_initial_posts(user)
583 _e -> {:error, "not found " <> nickname}
588 @doc "Fetch some posts when the user has just been federated with"
589 def fetch_initial_posts(user) do
590 %{"op" => "fetch_initial_posts", "user_id" => user.id}
591 |> BackgroundWorker.new(worker_args(:background))
595 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
596 def get_followers_query(%User{} = user, nil) do
597 User.Query.build(%{followers: user, deactivated: false})
600 def get_followers_query(user, page) do
601 from(u in get_followers_query(user, nil))
602 |> User.Query.paginate(page, 20)
605 @spec get_followers_query(User.t()) :: Ecto.Query.t()
606 def get_followers_query(user), do: get_followers_query(user, nil)
608 @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
609 def get_followers(user, page \\ nil) do
610 q = get_followers_query(user, page)
615 @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
616 def get_external_followers(user, page \\ nil) do
619 |> get_followers_query(page)
620 |> User.Query.build(%{external: true})
625 def get_followers_ids(user, page \\ nil) do
626 q = get_followers_query(user, page)
628 Repo.all(from(u in q, select: u.id))
631 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
632 def get_friends_query(%User{} = user, nil) do
633 User.Query.build(%{friends: user, deactivated: false})
636 def get_friends_query(user, page) do
637 from(u in get_friends_query(user, nil))
638 |> User.Query.paginate(page, 20)
641 @spec get_friends_query(User.t()) :: Ecto.Query.t()
642 def get_friends_query(user), do: get_friends_query(user, nil)
644 def get_friends(user, page \\ nil) do
645 q = get_friends_query(user, page)
650 def get_friends_ids(user, page \\ nil) do
651 q = get_friends_query(user, page)
653 Repo.all(from(u in q, select: u.id))
656 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
657 def get_follow_requests(%User{} = user) do
659 Activity.follow_requests_for_actor(user)
660 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
661 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
662 |> group_by([a, u], u.id)
669 def increase_note_count(%User{} = user) do
671 |> where(id: ^user.id)
676 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
683 |> Repo.update_all([])
685 {1, [user]} -> set_cache(user)
690 def decrease_note_count(%User{} = user) do
692 |> where(id: ^user.id)
697 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
704 |> Repo.update_all([])
706 {1, [user]} -> set_cache(user)
711 def update_note_count(%User{} = user) do
715 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
719 note_count = Repo.one(note_count_query)
721 info_cng = User.Info.set_note_count(user.info, note_count)
725 |> put_embed(:info, info_cng)
726 |> update_and_set_cache()
729 def maybe_fetch_follow_information(user) do
730 with {:ok, user} <- fetch_follow_information(user) do
734 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
740 def fetch_follow_information(user) do
741 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
742 info_cng = User.Info.follow_information_update(user.info, info)
747 |> put_embed(:info, info_cng)
749 update_and_set_cache(changeset)
756 def update_follower_count(%User{} = user) do
757 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
758 follower_count_query =
759 User.Query.build(%{followers: user, deactivated: false})
760 |> select([u], %{count: count(u.id)})
763 |> where(id: ^user.id)
764 |> join(:inner, [u], s in subquery(follower_count_query))
769 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
776 |> Repo.update_all([])
778 {1, [user]} -> set_cache(user)
782 {:ok, maybe_fetch_follow_information(user)}
786 def maybe_update_following_count(%User{local: false} = user) do
787 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
788 {:ok, maybe_fetch_follow_information(user)}
794 def maybe_update_following_count(user), do: user
796 def remove_duplicated_following(%User{following: following} = user) do
797 uniq_following = Enum.uniq(following)
799 if length(following) == length(uniq_following) do
803 |> update_changeset(%{following: uniq_following})
804 |> update_and_set_cache()
808 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
809 def get_users_from_set(ap_ids, local_only \\ true) do
810 criteria = %{ap_id: ap_ids, deactivated: false}
811 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
813 User.Query.build(criteria)
817 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
818 def get_recipients_from_activity(%Activity{recipients: to}) do
819 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
823 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
824 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
828 User.Info.add_to_mutes(info, ap_id)
829 |> User.Info.add_to_muted_notifications(info, ap_id, notifications?)
833 |> put_embed(:info, info_cng)
835 update_and_set_cache(cng)
838 def unmute(muter, %{ap_id: ap_id}) do
842 User.Info.remove_from_mutes(info, ap_id)
843 |> User.Info.remove_from_muted_notifications(info, ap_id)
847 |> put_embed(:info, info_cng)
849 update_and_set_cache(cng)
852 def subscribe(subscriber, %{ap_id: ap_id}) do
853 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
855 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
856 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
859 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
863 |> User.Info.add_to_subscribers(subscriber.ap_id)
866 |> put_embed(:info, info_cng)
867 |> update_and_set_cache()
872 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
873 with %User{} = user <- get_cached_by_ap_id(ap_id) do
876 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
879 |> put_embed(:info, info_cng)
880 |> update_and_set_cache()
884 def block(blocker, %User{ap_id: ap_id} = blocked) do
885 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
887 if following?(blocker, blocked) do
888 {:ok, blocker, _} = unfollow(blocker, blocked)
895 if subscribed_to?(blocked, blocker) do
896 {:ok, blocker} = unsubscribe(blocked, blocker)
902 if following?(blocked, blocker) do
903 unfollow(blocked, blocker)
906 {:ok, blocker} = update_follower_count(blocker)
910 |> User.Info.add_to_block(ap_id)
914 |> put_embed(:info, info_cng)
916 update_and_set_cache(cng)
919 # helper to handle the block given only an actor's AP id
920 def block(blocker, %{ap_id: ap_id}) do
921 block(blocker, get_cached_by_ap_id(ap_id))
924 def unblock(blocker, %{ap_id: ap_id}) do
927 |> User.Info.remove_from_block(ap_id)
931 |> put_embed(:info, info_cng)
933 update_and_set_cache(cng)
936 def mutes?(nil, _), do: false
937 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
939 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
940 def muted_notifications?(nil, _), do: false
942 def muted_notifications?(user, %{ap_id: ap_id}),
943 do: Enum.member?(user.info.muted_notifications, ap_id)
945 def blocks?(%User{} = user, %User{} = target) do
946 blocks_ap_id?(user, target) || blocks_domain?(user, target)
949 def blocks?(nil, _), do: false
951 def blocks_ap_id?(%User{} = user, %User{} = target) do
952 Enum.member?(user.info.blocks, target.ap_id)
955 def blocks_ap_id?(_, _), do: false
957 def blocks_domain?(%User{} = user, %User{} = target) do
958 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
959 %{host: host} = URI.parse(target.ap_id)
960 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
963 def blocks_domain?(_, _), do: false
965 def subscribed_to?(user, %{ap_id: ap_id}) do
966 with %User{} = target <- get_cached_by_ap_id(ap_id) do
967 Enum.member?(target.info.subscribers, user.ap_id)
971 @spec muted_users(User.t()) :: [User.t()]
972 def muted_users(user) do
973 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
977 @spec blocked_users(User.t()) :: [User.t()]
978 def blocked_users(user) do
979 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
983 @spec subscribers(User.t()) :: [User.t()]
984 def subscribers(user) do
985 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
989 def block_domain(user, domain) do
992 |> User.Info.add_to_domain_block(domain)
996 |> put_embed(:info, info_cng)
998 update_and_set_cache(cng)
1001 def unblock_domain(user, domain) do
1004 |> User.Info.remove_from_domain_block(domain)
1008 |> put_embed(:info, info_cng)
1010 update_and_set_cache(cng)
1013 def deactivate_async(user, status \\ true) do
1014 %{"op" => "deactivate_user", "user_id" => user.id, "status" => status}
1015 |> BackgroundWorker.new(worker_args(:background))
1019 def deactivate(%User{} = user, status \\ true) do
1020 info_cng = User.Info.set_activation_status(user.info, status)
1022 with {:ok, friends} <- User.get_friends(user),
1023 {:ok, followers} <- User.get_followers(user),
1027 |> put_embed(:info, info_cng)
1028 |> update_and_set_cache() do
1029 Enum.each(followers, &invalidate_cache(&1))
1030 Enum.each(friends, &update_follower_count(&1))
1036 def update_notification_settings(%User{} = user, settings \\ %{}) do
1037 info_changeset = User.Info.update_notification_settings(user.info, settings)
1040 |> put_embed(:info, info_changeset)
1041 |> update_and_set_cache()
1044 def delete(%User{} = user) do
1045 %{"op" => "delete_user", "user_id" => user.id}
1046 |> BackgroundWorker.new(worker_args(:background))
1050 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1051 def perform(:delete, %User{} = user) do
1052 {:ok, _user} = ActivityPub.delete(user)
1054 # Remove all relationships
1055 {:ok, followers} = User.get_followers(user)
1057 Enum.each(followers, fn follower ->
1058 ActivityPub.unfollow(follower, user)
1059 User.unfollow(follower, user)
1062 {:ok, friends} = User.get_friends(user)
1064 Enum.each(friends, fn followed ->
1065 ActivityPub.unfollow(user, followed)
1066 User.unfollow(user, followed)
1069 delete_user_activities(user)
1070 invalidate_cache(user)
1074 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1075 def perform(:fetch_initial_posts, %User{} = user) do
1076 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1079 # Insert all the posts in reverse order, so they're in the right order on the timeline
1080 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
1081 &Pleroma.Web.Federator.incoming_ap_doc/1
1087 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1089 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1090 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1091 when is_list(blocked_identifiers) do
1093 blocked_identifiers,
1094 fn blocked_identifier ->
1095 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1096 {:ok, blocker} <- block(blocker, blocked),
1097 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1101 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1108 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1109 def perform(:follow_import, %User{} = follower, followed_identifiers)
1110 when is_list(followed_identifiers) do
1112 followed_identifiers,
1113 fn followed_identifier ->
1114 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1115 {:ok, follower} <- maybe_direct_follow(follower, followed),
1116 {:ok, _} <- ActivityPub.follow(follower, followed) do
1120 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1127 @spec external_users_query() :: Ecto.Query.t()
1128 def external_users_query do
1136 @spec external_users(keyword()) :: [User.t()]
1137 def external_users(opts \\ []) do
1139 external_users_query()
1140 |> select([u], struct(u, [:id, :ap_id, :info]))
1144 do: where(query, [u], u.id > ^opts[:max_id]),
1149 do: limit(query, ^opts[:limit]),
1155 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1157 "op" => "blocks_import",
1158 "blocker_id" => blocker.id,
1159 "blocked_identifiers" => blocked_identifiers
1161 |> BackgroundWorker.new(worker_args(:background))
1165 def follow_import(%User{} = follower, followed_identifiers)
1166 when is_list(followed_identifiers) do
1168 "op" => "follow_import",
1169 "follower_id" => follower.id,
1170 "followed_identifiers" => followed_identifiers
1172 |> BackgroundWorker.new(worker_args(:background))
1176 def delete_user_activities(%User{ap_id: ap_id} = user) do
1178 |> Activity.query_by_actor()
1179 |> RepoStreamer.chunk_stream(50)
1180 |> Stream.each(fn activities ->
1181 Enum.each(activities, &delete_activity(&1))
1188 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1190 |> Object.normalize()
1191 |> ActivityPub.delete()
1194 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1195 user = get_cached_by_ap_id(activity.actor)
1196 object = Object.normalize(activity)
1198 ActivityPub.unlike(user, object)
1201 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1202 user = get_cached_by_ap_id(activity.actor)
1203 object = Object.normalize(activity)
1205 ActivityPub.unannounce(user, object)
1208 defp delete_activity(_activity), do: "Doing nothing"
1210 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1211 Pleroma.HTML.Scrubber.TwitterText
1214 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1216 def fetch_by_ap_id(ap_id) do
1217 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1224 case OStatus.make_user(ap_id) do
1225 {:ok, user} -> {:ok, user}
1226 _ -> {:error, "Could not fetch by AP id"}
1231 def get_or_fetch_by_ap_id(ap_id) do
1232 user = get_cached_by_ap_id(ap_id)
1234 if !is_nil(user) and !User.needs_update?(user) do
1237 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1238 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1240 resp = fetch_by_ap_id(ap_id)
1242 if should_fetch_initial do
1243 with {:ok, %User{} = user} <- resp do
1244 fetch_initial_posts(user)
1252 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1253 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1254 if user = get_cached_by_ap_id(uri) do
1258 %User{info: %User.Info{}}
1259 |> cast(%{}, [:ap_id, :nickname, :local])
1260 |> put_change(:ap_id, uri)
1261 |> put_change(:nickname, nickname)
1262 |> put_change(:local, true)
1263 |> put_change(:follower_address, uri <> "/followers")
1265 {:ok, user} = Repo.insert(changes)
1271 def public_key_from_info(%{
1272 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1276 |> :public_key.pem_decode()
1278 |> :public_key.pem_entry_decode()
1284 def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
1285 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1288 def public_key_from_info(_), do: {:error, "not found key"}
1290 def get_public_key_for_ap_id(ap_id) do
1291 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1292 {:ok, public_key} <- public_key_from_info(user.info) do
1299 defp blank?(""), do: nil
1300 defp blank?(n), do: n
1302 def insert_or_update_user(data) do
1304 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1305 |> remote_user_creation()
1306 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1310 def ap_enabled?(%User{local: true}), do: true
1311 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1312 def ap_enabled?(_), do: false
1314 @doc "Gets or fetch a user by uri or nickname."
1315 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1316 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1317 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1319 # wait a period of time and return newest version of the User structs
1320 # this is because we have synchronous follow APIs and need to simulate them
1321 # with an async handshake
1322 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1323 with %User{} = a <- User.get_cached_by_id(a.id),
1324 %User{} = b <- User.get_cached_by_id(b.id) do
1332 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1333 with :ok <- :timer.sleep(timeout),
1334 %User{} = a <- User.get_cached_by_id(a.id),
1335 %User{} = b <- User.get_cached_by_id(b.id) do
1343 def parse_bio(bio) when is_binary(bio) and bio != "" do
1345 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1349 def parse_bio(_), do: ""
1351 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1352 # TODO: get profile URLs other than user.ap_id
1353 profile_urls = [user.ap_id]
1356 |> CommonUtils.format_input("text/plain",
1357 mentions_format: :full,
1358 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1363 def parse_bio(_, _), do: ""
1365 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1366 Repo.transaction(fn ->
1367 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1371 def tag(nickname, tags) when is_binary(nickname),
1372 do: tag(get_by_nickname(nickname), tags)
1374 def tag(%User{} = user, tags),
1375 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1377 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1378 Repo.transaction(fn ->
1379 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1383 def untag(nickname, tags) when is_binary(nickname),
1384 do: untag(get_by_nickname(nickname), tags)
1386 def untag(%User{} = user, tags),
1387 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1389 defp update_tags(%User{} = user, new_tags) do
1390 {:ok, updated_user} =
1392 |> change(%{tags: new_tags})
1393 |> update_and_set_cache()
1398 defp normalize_tags(tags) do
1401 |> Enum.map(&String.downcase(&1))
1404 defp local_nickname_regex do
1405 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1406 @extended_local_nickname_regex
1408 @strict_local_nickname_regex
1412 def local_nickname(nickname_or_mention) do
1415 |> String.split("@")
1419 def full_nickname(nickname_or_mention),
1420 do: String.trim_leading(nickname_or_mention, "@")
1422 def error_user(ap_id) do
1427 nickname: "erroruser@example.com",
1428 inserted_at: NaiveDateTime.utc_now()
1432 @spec all_superusers() :: [User.t()]
1433 def all_superusers do
1434 User.Query.build(%{super_users: true, local: true, deactivated: false})
1438 def showing_reblogs?(%User{} = user, %User{} = target) do
1439 target.ap_id not in user.info.muted_reblogs
1443 The function returns a query to get users with no activity for given interval of days.
1444 Inactive users are those who didn't read any notification, or had any activity where
1445 the user is the activity's actor, during `inactivity_threshold` days.
1446 Deactivated users will not appear in this list.
1450 iex> Pleroma.User.list_inactive_users()
1453 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1454 def list_inactive_users_query(inactivity_threshold \\ 7) do
1455 negative_inactivity_threshold = -inactivity_threshold
1456 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1457 # Subqueries are not supported in `where` clauses, join gets too complicated.
1458 has_read_notifications =
1459 from(n in Pleroma.Notification,
1460 where: n.seen == true,
1462 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1465 |> Pleroma.Repo.all()
1467 from(u in Pleroma.User,
1468 left_join: a in Pleroma.Activity,
1469 on: u.ap_id == a.actor,
1470 where: not is_nil(u.nickname),
1471 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1472 where: u.id not in ^has_read_notifications,
1475 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1476 is_nil(max(a.inserted_at))
1481 Enable or disable email notifications for user
1485 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1486 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1488 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1489 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1491 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1492 {:ok, t()} | {:error, Ecto.Changeset.t()}
1493 def switch_email_notifications(user, type, status) do
1494 info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})
1497 |> put_embed(:info, info)
1498 |> update_and_set_cache()
1502 Set `last_digest_emailed_at` value for the user to current time
1504 @spec touch_last_digest_emailed_at(t()) :: t()
1505 def touch_last_digest_emailed_at(user) do
1506 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1508 {:ok, updated_user} =
1510 |> change(%{last_digest_emailed_at: now})
1511 |> update_and_set_cache()
1516 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1517 def toggle_confirmation(%User{} = user) do
1518 need_confirmation? = !user.info.confirmation_pending
1521 User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
1525 |> put_embed(:info, info_changeset)
1526 |> update_and_set_cache()
1529 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1533 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1534 # use instance-default
1535 config = Pleroma.Config.get([:assets, :mascots])
1536 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1537 mascot = Keyword.get(config, default_mascot)
1540 "id" => "default-mascot",
1541 "url" => mascot[:url],
1542 "preview_url" => mascot[:url],
1544 "mime_type" => mascot[:mime_type]
1549 def ensure_keys_present(%User{info: info} = user) do
1553 {:ok, pem} = Keys.generate_rsa_pem()
1556 |> Ecto.Changeset.change()
1557 |> Ecto.Changeset.put_embed(:info, User.Info.set_keys(info, pem))
1558 |> update_and_set_cache()
1562 def get_ap_ids_by_nicknames(nicknames) do
1564 where: u.nickname in ^nicknames,
1570 defdelegate search(query, opts \\ []), to: User.Search
1572 defp put_password_hash(
1573 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1575 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1578 defp put_password_hash(changeset), do: changeset
1580 def is_internal_user?(%User{nickname: nil}), do: true
1581 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1582 def is_internal_user?(_), do: false