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} <- autofollow_users(user),
334 {:ok, user} <- set_cache(user),
335 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
336 {:ok, _} <- try_send_confirmation_email(user) do
341 def try_send_confirmation_email(%User{} = user) do
342 if user.info.confirmation_pending &&
343 Pleroma.Config.get([:instance, :account_activation_required]) do
345 |> Pleroma.Emails.UserEmail.account_confirmation_email()
346 |> Pleroma.Emails.Mailer.deliver_async()
354 def needs_update?(%User{local: true}), do: false
356 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
358 def needs_update?(%User{local: false} = user) do
359 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
362 def needs_update?(_), do: true
364 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
365 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
369 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
370 follow(follower, followed)
373 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
374 if not User.ap_enabled?(followed) do
375 follow(follower, followed)
381 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
382 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
383 def follow_all(follower, followeds) do
386 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
387 |> Enum.map(fn %{follower_address: fa} -> fa end)
391 where: u.id == ^follower.id,
396 "array(select distinct unnest (array_cat(?, ?)))",
405 {1, [follower]} = Repo.update_all(q, [])
407 Enum.each(followeds, fn followed ->
408 update_follower_count(followed)
414 def follow(%User{} = follower, %User{info: info} = followed) do
415 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
416 ap_followers = followed.follower_address
420 {:error, "Could not follow user: You are deactivated."}
422 deny_follow_blocked and blocks?(followed, follower) ->
423 {:error, "Could not follow user: #{followed.nickname} blocked you."}
426 if !followed.local && follower.local && !ap_enabled?(followed) do
427 Websub.subscribe(follower, followed)
432 where: u.id == ^follower.id,
433 update: [push: [following: ^ap_followers]],
437 {1, [follower]} = Repo.update_all(q, [])
439 follower = maybe_update_following_count(follower)
441 {:ok, _} = update_follower_count(followed)
447 def unfollow(%User{} = follower, %User{} = followed) do
448 ap_followers = followed.follower_address
450 if following?(follower, followed) and follower.ap_id != followed.ap_id do
453 where: u.id == ^follower.id,
454 update: [pull: [following: ^ap_followers]],
458 {1, [follower]} = Repo.update_all(q, [])
460 follower = maybe_update_following_count(follower)
462 {:ok, followed} = update_follower_count(followed)
466 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
468 {:error, "Not subscribed!"}
472 @spec following?(User.t(), User.t()) :: boolean
473 def following?(%User{} = follower, %User{} = followed) do
474 Enum.member?(follower.following, followed.follower_address)
477 def locked?(%User{} = user) do
478 user.info.locked || false
482 Repo.get_by(User, id: id)
485 def get_by_ap_id(ap_id) do
486 Repo.get_by(User, ap_id: ap_id)
489 def get_all_by_ap_id(ap_ids) do
490 from(u in __MODULE__,
491 where: u.ap_id in ^ap_ids
496 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
497 # of the ap_id and the domain and tries to get that user
498 def get_by_guessed_nickname(ap_id) do
499 domain = URI.parse(ap_id).host
500 name = List.last(String.split(ap_id, "/"))
501 nickname = "#{name}@#{domain}"
503 get_cached_by_nickname(nickname)
506 def set_cache({:ok, user}), do: set_cache(user)
507 def set_cache({:error, err}), do: {:error, err}
509 def set_cache(%User{} = user) do
510 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
511 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
512 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
516 def update_and_set_cache(changeset) do
517 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
524 def invalidate_cache(user) do
525 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
526 Cachex.del(:user_cache, "nickname:#{user.nickname}")
527 Cachex.del(:user_cache, "user_info:#{user.id}")
530 def get_cached_by_ap_id(ap_id) do
531 key = "ap_id:#{ap_id}"
532 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
535 def get_cached_by_id(id) do
539 Cachex.fetch!(:user_cache, key, fn _ ->
543 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
544 {:commit, user.ap_id}
550 get_cached_by_ap_id(ap_id)
553 def get_cached_by_nickname(nickname) do
554 key = "nickname:#{nickname}"
556 Cachex.fetch!(:user_cache, key, fn ->
557 user_result = get_or_fetch_by_nickname(nickname)
560 {:ok, user} -> {:commit, user}
561 {:error, _error} -> {:ignore, nil}
566 def get_cached_by_nickname_or_id(nickname_or_id) do
567 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
570 def get_by_nickname(nickname) do
571 Repo.get_by(User, nickname: nickname) ||
572 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
573 Repo.get_by(User, nickname: local_nickname(nickname))
577 def get_by_email(email), do: Repo.get_by(User, email: email)
579 def get_by_nickname_or_email(nickname_or_email) do
580 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
583 def get_cached_user_info(user) do
584 key = "user_info:#{user.id}"
585 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
588 def fetch_by_nickname(nickname) do
589 ap_try = ActivityPub.make_user_from_nickname(nickname)
592 {:ok, user} -> {:ok, user}
593 _ -> OStatus.make_user(nickname)
597 def get_or_fetch_by_nickname(nickname) do
598 with %User{} = user <- get_by_nickname(nickname) do
602 with [_nick, _domain] <- String.split(nickname, "@"),
603 {:ok, user} <- fetch_by_nickname(nickname) do
604 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
605 fetch_initial_posts(user)
610 _e -> {:error, "not found " <> nickname}
615 @doc "Fetch some posts when the user has just been federated with"
616 def fetch_initial_posts(user),
617 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:fetch_initial_posts, user])
619 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
620 def get_followers_query(%User{} = user, nil) do
621 User.Query.build(%{followers: user, deactivated: false})
624 def get_followers_query(user, page) do
625 from(u in get_followers_query(user, nil))
626 |> User.Query.paginate(page, 20)
629 @spec get_followers_query(User.t()) :: Ecto.Query.t()
630 def get_followers_query(user), do: get_followers_query(user, nil)
632 @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
633 def get_followers(user, page \\ nil) do
634 q = get_followers_query(user, page)
639 @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
640 def get_external_followers(user, page \\ nil) do
643 |> get_followers_query(page)
644 |> User.Query.build(%{external: true})
649 def get_followers_ids(user, page \\ nil) do
650 q = get_followers_query(user, page)
652 Repo.all(from(u in q, select: u.id))
655 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
656 def get_friends_query(%User{} = user, nil) do
657 User.Query.build(%{friends: user, deactivated: false})
660 def get_friends_query(user, page) do
661 from(u in get_friends_query(user, nil))
662 |> User.Query.paginate(page, 20)
665 @spec get_friends_query(User.t()) :: Ecto.Query.t()
666 def get_friends_query(user), do: get_friends_query(user, nil)
668 def get_friends(user, page \\ nil) do
669 q = get_friends_query(user, page)
674 def get_friends_ids(user, page \\ nil) do
675 q = get_friends_query(user, page)
677 Repo.all(from(u in q, select: u.id))
680 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
681 def get_follow_requests(%User{} = user) do
683 Activity.follow_requests_for_actor(user)
684 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
685 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
686 |> group_by([a, u], u.id)
693 def increase_note_count(%User{} = user) do
695 |> where(id: ^user.id)
700 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
707 |> Repo.update_all([])
709 {1, [user]} -> set_cache(user)
714 def decrease_note_count(%User{} = user) do
716 |> where(id: ^user.id)
721 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
728 |> Repo.update_all([])
730 {1, [user]} -> set_cache(user)
735 def update_note_count(%User{} = user) do
739 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
743 note_count = Repo.one(note_count_query)
745 info_cng = User.Info.set_note_count(user.info, note_count)
749 |> put_embed(:info, info_cng)
750 |> update_and_set_cache()
753 @spec maybe_fetch_follow_information(User.t()) :: User.t()
754 def maybe_fetch_follow_information(user) do
755 with {:ok, user} <- fetch_follow_information(user) do
759 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
765 def fetch_follow_information(user) do
766 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
767 info_cng = User.Info.follow_information_update(user.info, info)
772 |> put_embed(:info, info_cng)
774 update_and_set_cache(changeset)
781 def update_follower_count(%User{} = user) do
782 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
783 follower_count_query =
784 User.Query.build(%{followers: user, deactivated: false})
785 |> select([u], %{count: count(u.id)})
788 |> where(id: ^user.id)
789 |> join(:inner, [u], s in subquery(follower_count_query))
794 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
801 |> Repo.update_all([])
803 {1, [user]} -> set_cache(user)
807 {:ok, maybe_fetch_follow_information(user)}
811 @spec maybe_update_following_count(User.t()) :: User.t()
812 def maybe_update_following_count(%User{local: false} = user) do
813 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
814 maybe_fetch_follow_information(user)
820 def maybe_update_following_count(user), do: user
822 def remove_duplicated_following(%User{following: following} = user) do
823 uniq_following = Enum.uniq(following)
825 if length(following) == length(uniq_following) do
829 |> update_changeset(%{following: uniq_following})
830 |> update_and_set_cache()
834 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
835 def get_users_from_set(ap_ids, local_only \\ true) do
836 criteria = %{ap_id: ap_ids, deactivated: false}
837 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
839 User.Query.build(criteria)
843 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
844 def get_recipients_from_activity(%Activity{recipients: to}) do
845 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
849 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
850 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
854 User.Info.add_to_mutes(info, ap_id)
855 |> User.Info.add_to_muted_notifications(info, ap_id, notifications?)
859 |> put_embed(:info, info_cng)
861 update_and_set_cache(cng)
864 def unmute(muter, %{ap_id: ap_id}) do
868 User.Info.remove_from_mutes(info, ap_id)
869 |> User.Info.remove_from_muted_notifications(info, ap_id)
873 |> put_embed(:info, info_cng)
875 update_and_set_cache(cng)
878 def subscribe(subscriber, %{ap_id: ap_id}) do
879 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
881 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
882 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
885 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
889 |> User.Info.add_to_subscribers(subscriber.ap_id)
892 |> put_embed(:info, info_cng)
893 |> update_and_set_cache()
898 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
899 with %User{} = user <- get_cached_by_ap_id(ap_id) do
902 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
905 |> put_embed(:info, info_cng)
906 |> update_and_set_cache()
910 def block(blocker, %User{ap_id: ap_id} = blocked) do
911 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
913 if following?(blocker, blocked) do
914 {:ok, blocker, _} = unfollow(blocker, blocked)
920 # clear any requested follows as well
922 case CommonAPI.reject_follow_request(blocked, blocker) do
923 {:ok, %User{} = updated_blocked} -> updated_blocked
928 if subscribed_to?(blocked, blocker) do
929 {:ok, blocker} = unsubscribe(blocked, blocker)
935 if following?(blocked, blocker) do
936 unfollow(blocked, blocker)
939 {:ok, blocker} = update_follower_count(blocker)
943 |> User.Info.add_to_block(ap_id)
947 |> put_embed(:info, info_cng)
949 update_and_set_cache(cng)
952 # helper to handle the block given only an actor's AP id
953 def block(blocker, %{ap_id: ap_id}) do
954 block(blocker, get_cached_by_ap_id(ap_id))
957 def unblock(blocker, %{ap_id: ap_id}) do
960 |> User.Info.remove_from_block(ap_id)
964 |> put_embed(:info, info_cng)
966 update_and_set_cache(cng)
969 def mutes?(nil, _), do: false
970 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
972 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
973 def muted_notifications?(nil, _), do: false
975 def muted_notifications?(user, %{ap_id: ap_id}),
976 do: Enum.member?(user.info.muted_notifications, ap_id)
978 def blocks?(%User{} = user, %User{} = target) do
979 blocks_ap_id?(user, target) || blocks_domain?(user, target)
982 def blocks?(nil, _), do: false
984 def blocks_ap_id?(%User{} = user, %User{} = target) do
985 Enum.member?(user.info.blocks, target.ap_id)
988 def blocks_ap_id?(_, _), do: false
990 def blocks_domain?(%User{} = user, %User{} = target) do
991 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
992 %{host: host} = URI.parse(target.ap_id)
993 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
996 def blocks_domain?(_, _), do: false
998 def subscribed_to?(user, %{ap_id: ap_id}) do
999 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1000 Enum.member?(target.info.subscribers, user.ap_id)
1004 @spec muted_users(User.t()) :: [User.t()]
1005 def muted_users(user) do
1006 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
1010 @spec blocked_users(User.t()) :: [User.t()]
1011 def blocked_users(user) do
1012 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
1016 @spec subscribers(User.t()) :: [User.t()]
1017 def subscribers(user) do
1018 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
1022 def block_domain(user, domain) do
1025 |> User.Info.add_to_domain_block(domain)
1029 |> put_embed(:info, info_cng)
1031 update_and_set_cache(cng)
1034 def unblock_domain(user, domain) do
1037 |> User.Info.remove_from_domain_block(domain)
1041 |> put_embed(:info, info_cng)
1043 update_and_set_cache(cng)
1046 def deactivate_async(user, status \\ true) do
1047 PleromaJobQueue.enqueue(:background, __MODULE__, [:deactivate_async, user, status])
1050 def deactivate(%User{} = user, status \\ true) do
1051 info_cng = User.Info.set_activation_status(user.info, status)
1053 with {:ok, friends} <- User.get_friends(user),
1054 {:ok, followers} <- User.get_followers(user),
1058 |> put_embed(:info, info_cng)
1059 |> update_and_set_cache() do
1060 Enum.each(followers, &invalidate_cache(&1))
1061 Enum.each(friends, &update_follower_count(&1))
1067 def update_notification_settings(%User{} = user, settings \\ %{}) do
1068 info_changeset = User.Info.update_notification_settings(user.info, settings)
1071 |> put_embed(:info, info_changeset)
1072 |> update_and_set_cache()
1075 @spec delete(User.t()) :: :ok
1076 def delete(%User{} = user),
1077 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:delete, user])
1079 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1080 def perform(:delete, %User{} = user) do
1081 {:ok, _user} = ActivityPub.delete(user)
1083 # Remove all relationships
1084 {:ok, followers} = User.get_followers(user)
1086 Enum.each(followers, fn follower ->
1087 ActivityPub.unfollow(follower, user)
1088 User.unfollow(follower, user)
1091 {:ok, friends} = User.get_friends(user)
1093 Enum.each(friends, fn followed ->
1094 ActivityPub.unfollow(user, followed)
1095 User.unfollow(user, followed)
1098 delete_user_activities(user)
1099 invalidate_cache(user)
1103 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1104 def perform(:fetch_initial_posts, %User{} = user) do
1105 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1108 # Insert all the posts in reverse order, so they're in the right order on the timeline
1109 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
1110 &Pleroma.Web.Federator.incoming_ap_doc/1
1116 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1118 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1119 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1120 when is_list(blocked_identifiers) do
1122 blocked_identifiers,
1123 fn blocked_identifier ->
1124 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1125 {:ok, blocker} <- block(blocker, blocked),
1126 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1130 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1137 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1138 def perform(:follow_import, %User{} = follower, followed_identifiers)
1139 when is_list(followed_identifiers) do
1141 followed_identifiers,
1142 fn followed_identifier ->
1143 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1144 {:ok, follower} <- maybe_direct_follow(follower, followed),
1145 {:ok, _} <- ActivityPub.follow(follower, followed) do
1149 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1156 @spec external_users_query() :: Ecto.Query.t()
1157 def external_users_query do
1165 @spec external_users(keyword()) :: [User.t()]
1166 def external_users(opts \\ []) do
1168 external_users_query()
1169 |> select([u], struct(u, [:id, :ap_id, :info]))
1173 do: where(query, [u], u.id > ^opts[:max_id]),
1178 do: limit(query, ^opts[:limit]),
1184 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers),
1186 PleromaJobQueue.enqueue(:background, __MODULE__, [
1192 def follow_import(%User{} = follower, followed_identifiers) when is_list(followed_identifiers),
1194 PleromaJobQueue.enqueue(:background, __MODULE__, [
1197 followed_identifiers
1200 def delete_user_activities(%User{ap_id: ap_id} = user) do
1202 |> Activity.query_by_actor()
1203 |> RepoStreamer.chunk_stream(50)
1204 |> Stream.each(fn activities ->
1205 Enum.each(activities, &delete_activity(&1))
1212 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1214 |> Object.normalize()
1215 |> ActivityPub.delete()
1218 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1219 user = get_cached_by_ap_id(activity.actor)
1220 object = Object.normalize(activity)
1222 ActivityPub.unlike(user, object)
1225 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1226 user = get_cached_by_ap_id(activity.actor)
1227 object = Object.normalize(activity)
1229 ActivityPub.unannounce(user, object)
1232 defp delete_activity(_activity), do: "Doing nothing"
1234 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1235 Pleroma.HTML.Scrubber.TwitterText
1238 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1240 def fetch_by_ap_id(ap_id) do
1241 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1248 case OStatus.make_user(ap_id) do
1249 {:ok, user} -> {:ok, user}
1250 _ -> {:error, "Could not fetch by AP id"}
1255 def get_or_fetch_by_ap_id(ap_id) do
1256 user = get_cached_by_ap_id(ap_id)
1258 if !is_nil(user) and !User.needs_update?(user) do
1261 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1262 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1264 resp = fetch_by_ap_id(ap_id)
1266 if should_fetch_initial do
1267 with {:ok, %User{} = user} <- resp do
1268 fetch_initial_posts(user)
1276 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1277 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1278 if user = get_cached_by_ap_id(uri) do
1282 %User{info: %User.Info{}}
1283 |> cast(%{}, [:ap_id, :nickname, :local])
1284 |> put_change(:ap_id, uri)
1285 |> put_change(:nickname, nickname)
1286 |> put_change(:local, true)
1287 |> put_change(:follower_address, uri <> "/followers")
1289 {:ok, user} = Repo.insert(changes)
1295 def public_key_from_info(%{
1296 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1300 |> :public_key.pem_decode()
1302 |> :public_key.pem_entry_decode()
1308 def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
1309 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1312 def public_key_from_info(_), do: {:error, "not found key"}
1314 def get_public_key_for_ap_id(ap_id) do
1315 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1316 {:ok, public_key} <- public_key_from_info(user.info) do
1323 defp blank?(""), do: nil
1324 defp blank?(n), do: n
1326 def insert_or_update_user(data) do
1328 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1329 |> remote_user_creation()
1330 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1334 def ap_enabled?(%User{local: true}), do: true
1335 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1336 def ap_enabled?(_), do: false
1338 @doc "Gets or fetch a user by uri or nickname."
1339 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1340 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1341 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1343 # wait a period of time and return newest version of the User structs
1344 # this is because we have synchronous follow APIs and need to simulate them
1345 # with an async handshake
1346 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1347 with %User{} = a <- User.get_cached_by_id(a.id),
1348 %User{} = b <- User.get_cached_by_id(b.id) do
1356 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1357 with :ok <- :timer.sleep(timeout),
1358 %User{} = a <- User.get_cached_by_id(a.id),
1359 %User{} = b <- User.get_cached_by_id(b.id) do
1367 def parse_bio(bio) when is_binary(bio) and bio != "" do
1369 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1373 def parse_bio(_), do: ""
1375 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1376 # TODO: get profile URLs other than user.ap_id
1377 profile_urls = [user.ap_id]
1380 |> CommonUtils.format_input("text/plain",
1381 mentions_format: :full,
1382 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1387 def parse_bio(_, _), do: ""
1389 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1390 Repo.transaction(fn ->
1391 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1395 def tag(nickname, tags) when is_binary(nickname),
1396 do: tag(get_by_nickname(nickname), tags)
1398 def tag(%User{} = user, tags),
1399 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1401 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1402 Repo.transaction(fn ->
1403 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1407 def untag(nickname, tags) when is_binary(nickname),
1408 do: untag(get_by_nickname(nickname), tags)
1410 def untag(%User{} = user, tags),
1411 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1413 defp update_tags(%User{} = user, new_tags) do
1414 {:ok, updated_user} =
1416 |> change(%{tags: new_tags})
1417 |> update_and_set_cache()
1422 defp normalize_tags(tags) do
1425 |> Enum.map(&String.downcase(&1))
1428 defp local_nickname_regex do
1429 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1430 @extended_local_nickname_regex
1432 @strict_local_nickname_regex
1436 def local_nickname(nickname_or_mention) do
1439 |> String.split("@")
1443 def full_nickname(nickname_or_mention),
1444 do: String.trim_leading(nickname_or_mention, "@")
1446 def error_user(ap_id) do
1451 nickname: "erroruser@example.com",
1452 inserted_at: NaiveDateTime.utc_now()
1456 @spec all_superusers() :: [User.t()]
1457 def all_superusers do
1458 User.Query.build(%{super_users: true, local: true, deactivated: false})
1462 def showing_reblogs?(%User{} = user, %User{} = target) do
1463 target.ap_id not in user.info.muted_reblogs
1467 The function returns a query to get users with no activity for given interval of days.
1468 Inactive users are those who didn't read any notification, or had any activity where
1469 the user is the activity's actor, during `inactivity_threshold` days.
1470 Deactivated users will not appear in this list.
1474 iex> Pleroma.User.list_inactive_users()
1477 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1478 def list_inactive_users_query(inactivity_threshold \\ 7) do
1479 negative_inactivity_threshold = -inactivity_threshold
1480 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1481 # Subqueries are not supported in `where` clauses, join gets too complicated.
1482 has_read_notifications =
1483 from(n in Pleroma.Notification,
1484 where: n.seen == true,
1486 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1489 |> Pleroma.Repo.all()
1491 from(u in Pleroma.User,
1492 left_join: a in Pleroma.Activity,
1493 on: u.ap_id == a.actor,
1494 where: not is_nil(u.nickname),
1495 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1496 where: u.id not in ^has_read_notifications,
1499 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1500 is_nil(max(a.inserted_at))
1505 Enable or disable email notifications for user
1509 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1510 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1512 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1513 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1515 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1516 {:ok, t()} | {:error, Ecto.Changeset.t()}
1517 def switch_email_notifications(user, type, status) do
1518 info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})
1521 |> put_embed(:info, info)
1522 |> update_and_set_cache()
1526 Set `last_digest_emailed_at` value for the user to current time
1528 @spec touch_last_digest_emailed_at(t()) :: t()
1529 def touch_last_digest_emailed_at(user) do
1530 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1532 {:ok, updated_user} =
1534 |> change(%{last_digest_emailed_at: now})
1535 |> update_and_set_cache()
1540 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1541 def toggle_confirmation(%User{} = user) do
1542 need_confirmation? = !user.info.confirmation_pending
1545 User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
1549 |> put_embed(:info, info_changeset)
1550 |> update_and_set_cache()
1553 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1557 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1558 # use instance-default
1559 config = Pleroma.Config.get([:assets, :mascots])
1560 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1561 mascot = Keyword.get(config, default_mascot)
1564 "id" => "default-mascot",
1565 "url" => mascot[:url],
1566 "preview_url" => mascot[:url],
1568 "mime_type" => mascot[:mime_type]
1573 def ensure_keys_present(%User{info: info} = user) do
1577 {:ok, pem} = Keys.generate_rsa_pem()
1580 |> Ecto.Changeset.change()
1581 |> Ecto.Changeset.put_embed(:info, User.Info.set_keys(info, pem))
1582 |> update_and_set_cache()
1586 def get_ap_ids_by_nicknames(nicknames) do
1588 where: u.nickname in ^nicknames,
1594 defdelegate search(query, opts \\ []), to: User.Search
1596 defp put_password_hash(
1597 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1599 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1602 defp put_password_hash(changeset), do: changeset
1604 def is_internal_user?(%User{nickname: nil}), do: true
1605 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1606 def is_internal_user?(_), do: false