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
30 alias Pleroma.Workers.BackgroundWorker
34 @type t :: %__MODULE__{}
36 @primary_key {:id, Pleroma.FlakeId, autogenerate: true}
38 # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
39 @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])?)*$/
41 @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
42 @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
46 field(:email, :string)
48 field(:nickname, :string)
49 field(:password_hash, :string)
50 field(:password, :string, virtual: true)
51 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) do
574 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
577 def get_by_nickname(nickname) do
578 Repo.get_by(User, nickname: nickname) ||
579 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
580 Repo.get_by(User, nickname: local_nickname(nickname))
584 def get_by_email(email), do: Repo.get_by(User, email: email)
586 def get_by_nickname_or_email(nickname_or_email) do
587 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
590 def get_cached_user_info(user) do
591 key = "user_info:#{user.id}"
592 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
595 def fetch_by_nickname(nickname) do
596 ap_try = ActivityPub.make_user_from_nickname(nickname)
599 {:ok, user} -> {:ok, user}
600 _ -> OStatus.make_user(nickname)
604 def get_or_fetch_by_nickname(nickname) do
605 with %User{} = user <- get_by_nickname(nickname) do
609 with [_nick, _domain] <- String.split(nickname, "@"),
610 {:ok, user} <- fetch_by_nickname(nickname) do
611 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
612 fetch_initial_posts(user)
617 _e -> {:error, "not found " <> nickname}
622 @doc "Fetch some posts when the user has just been federated with"
623 def fetch_initial_posts(user) do
624 BackgroundWorker.enqueue("fetch_initial_posts", %{"user_id" => user.id})
627 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
628 def get_followers_query(%User{} = user, nil) do
629 User.Query.build(%{followers: user, deactivated: false})
632 def get_followers_query(user, page) do
633 from(u in get_followers_query(user, nil))
634 |> User.Query.paginate(page, 20)
637 @spec get_followers_query(User.t()) :: Ecto.Query.t()
638 def get_followers_query(user), do: get_followers_query(user, nil)
640 @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
641 def get_followers(user, page \\ nil) do
642 q = get_followers_query(user, page)
647 @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
648 def get_external_followers(user, page \\ nil) do
651 |> get_followers_query(page)
652 |> User.Query.build(%{external: true})
657 def get_followers_ids(user, page \\ nil) do
658 q = get_followers_query(user, page)
660 Repo.all(from(u in q, select: u.id))
663 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
664 def get_friends_query(%User{} = user, nil) do
665 User.Query.build(%{friends: user, deactivated: false})
668 def get_friends_query(user, page) do
669 from(u in get_friends_query(user, nil))
670 |> User.Query.paginate(page, 20)
673 @spec get_friends_query(User.t()) :: Ecto.Query.t()
674 def get_friends_query(user), do: get_friends_query(user, nil)
676 def get_friends(user, page \\ nil) do
677 q = get_friends_query(user, page)
682 def get_friends_ids(user, page \\ nil) do
683 q = get_friends_query(user, page)
685 Repo.all(from(u in q, select: u.id))
688 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
689 def get_follow_requests(%User{} = user) do
691 Activity.follow_requests_for_actor(user)
692 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
693 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
694 |> group_by([a, u], u.id)
701 def increase_note_count(%User{} = user) do
703 |> where(id: ^user.id)
708 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
715 |> Repo.update_all([])
717 {1, [user]} -> set_cache(user)
722 def decrease_note_count(%User{} = user) do
724 |> where(id: ^user.id)
729 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
736 |> Repo.update_all([])
738 {1, [user]} -> set_cache(user)
743 def update_note_count(%User{} = user) do
747 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
751 note_count = Repo.one(note_count_query)
753 info_cng = User.Info.set_note_count(user.info, note_count)
757 |> put_embed(:info, info_cng)
758 |> update_and_set_cache()
761 @spec maybe_fetch_follow_information(User.t()) :: User.t()
762 def maybe_fetch_follow_information(user) do
763 with {:ok, user} <- fetch_follow_information(user) do
767 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
773 def fetch_follow_information(user) do
774 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
775 info_cng = User.Info.follow_information_update(user.info, info)
780 |> put_embed(:info, info_cng)
782 update_and_set_cache(changeset)
789 def update_follower_count(%User{} = user) do
790 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
791 follower_count_query =
792 User.Query.build(%{followers: user, deactivated: false})
793 |> select([u], %{count: count(u.id)})
796 |> where(id: ^user.id)
797 |> join(:inner, [u], s in subquery(follower_count_query))
802 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
809 |> Repo.update_all([])
811 {1, [user]} -> set_cache(user)
815 {:ok, maybe_fetch_follow_information(user)}
819 @spec maybe_update_following_count(User.t()) :: User.t()
820 def maybe_update_following_count(%User{local: false} = user) do
821 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
822 maybe_fetch_follow_information(user)
828 def maybe_update_following_count(user), do: user
830 def remove_duplicated_following(%User{following: following} = user) do
831 uniq_following = Enum.uniq(following)
833 if length(following) == length(uniq_following) do
837 |> update_changeset(%{following: uniq_following})
838 |> update_and_set_cache()
842 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
843 def get_users_from_set(ap_ids, local_only \\ true) do
844 criteria = %{ap_id: ap_ids, deactivated: false}
845 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
847 User.Query.build(criteria)
851 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
852 def get_recipients_from_activity(%Activity{recipients: to}) do
853 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
857 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
858 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
862 User.Info.add_to_mutes(info, ap_id)
863 |> User.Info.add_to_muted_notifications(info, ap_id, notifications?)
867 |> put_embed(:info, info_cng)
869 update_and_set_cache(cng)
872 def unmute(muter, %{ap_id: ap_id}) do
876 User.Info.remove_from_mutes(info, ap_id)
877 |> User.Info.remove_from_muted_notifications(info, ap_id)
881 |> put_embed(:info, info_cng)
883 update_and_set_cache(cng)
886 def subscribe(subscriber, %{ap_id: ap_id}) do
887 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
889 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
890 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
893 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
897 |> User.Info.add_to_subscribers(subscriber.ap_id)
900 |> put_embed(:info, info_cng)
901 |> update_and_set_cache()
906 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
907 with %User{} = user <- get_cached_by_ap_id(ap_id) do
910 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
913 |> put_embed(:info, info_cng)
914 |> update_and_set_cache()
918 def block(blocker, %User{ap_id: ap_id} = blocked) do
919 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
921 if following?(blocker, blocked) do
922 {:ok, blocker, _} = unfollow(blocker, blocked)
928 # clear any requested follows as well
930 case CommonAPI.reject_follow_request(blocked, blocker) do
931 {:ok, %User{} = updated_blocked} -> updated_blocked
936 if subscribed_to?(blocked, blocker) do
937 {:ok, blocker} = unsubscribe(blocked, blocker)
943 if following?(blocked, blocker) do
944 unfollow(blocked, blocker)
947 {:ok, blocker} = update_follower_count(blocker)
951 |> User.Info.add_to_block(ap_id)
955 |> put_embed(:info, info_cng)
957 update_and_set_cache(cng)
960 # helper to handle the block given only an actor's AP id
961 def block(blocker, %{ap_id: ap_id}) do
962 block(blocker, get_cached_by_ap_id(ap_id))
965 def unblock(blocker, %{ap_id: ap_id}) do
968 |> User.Info.remove_from_block(ap_id)
972 |> put_embed(:info, info_cng)
974 update_and_set_cache(cng)
977 def mutes?(nil, _), do: false
978 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
980 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
981 def muted_notifications?(nil, _), do: false
983 def muted_notifications?(user, %{ap_id: ap_id}),
984 do: Enum.member?(user.info.muted_notifications, ap_id)
986 def blocks?(%User{} = user, %User{} = target) do
987 blocks_ap_id?(user, target) || blocks_domain?(user, target)
990 def blocks?(nil, _), do: false
992 def blocks_ap_id?(%User{} = user, %User{} = target) do
993 Enum.member?(user.info.blocks, target.ap_id)
996 def blocks_ap_id?(_, _), do: false
998 def blocks_domain?(%User{} = user, %User{} = target) do
999 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
1000 %{host: host} = URI.parse(target.ap_id)
1001 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1004 def blocks_domain?(_, _), do: false
1006 def subscribed_to?(user, %{ap_id: ap_id}) do
1007 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1008 Enum.member?(target.info.subscribers, user.ap_id)
1012 @spec muted_users(User.t()) :: [User.t()]
1013 def muted_users(user) do
1014 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
1018 @spec blocked_users(User.t()) :: [User.t()]
1019 def blocked_users(user) do
1020 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
1024 @spec subscribers(User.t()) :: [User.t()]
1025 def subscribers(user) do
1026 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
1030 def block_domain(user, domain) do
1033 |> User.Info.add_to_domain_block(domain)
1037 |> put_embed(:info, info_cng)
1039 update_and_set_cache(cng)
1042 def unblock_domain(user, domain) do
1045 |> User.Info.remove_from_domain_block(domain)
1049 |> put_embed(:info, info_cng)
1051 update_and_set_cache(cng)
1054 def deactivate_async(user, status \\ true) do
1055 BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
1058 def deactivate(%User{} = user, status \\ true) do
1059 info_cng = User.Info.set_activation_status(user.info, status)
1061 with {:ok, friends} <- User.get_friends(user),
1062 {:ok, followers} <- User.get_followers(user),
1066 |> put_embed(:info, info_cng)
1067 |> update_and_set_cache() do
1068 Enum.each(followers, &invalidate_cache(&1))
1069 Enum.each(friends, &update_follower_count(&1))
1075 def update_notification_settings(%User{} = user, settings \\ %{}) do
1076 info_changeset = User.Info.update_notification_settings(user.info, settings)
1079 |> put_embed(:info, info_changeset)
1080 |> update_and_set_cache()
1083 def delete(%User{} = user) do
1084 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1087 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1088 def perform(:delete, %User{} = user) do
1089 {:ok, _user} = ActivityPub.delete(user)
1091 # Remove all relationships
1092 {:ok, followers} = User.get_followers(user)
1094 Enum.each(followers, fn follower ->
1095 ActivityPub.unfollow(follower, user)
1096 User.unfollow(follower, user)
1099 {:ok, friends} = User.get_friends(user)
1101 Enum.each(friends, fn followed ->
1102 ActivityPub.unfollow(user, followed)
1103 User.unfollow(user, followed)
1106 delete_user_activities(user)
1107 invalidate_cache(user)
1111 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1112 def perform(:fetch_initial_posts, %User{} = user) do
1113 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1116 # Insert all the posts in reverse order, so they're in the right order on the timeline
1117 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
1118 &Pleroma.Web.Federator.incoming_ap_doc/1
1124 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1126 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1127 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1128 when is_list(blocked_identifiers) do
1130 blocked_identifiers,
1131 fn blocked_identifier ->
1132 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1133 {:ok, blocker} <- block(blocker, blocked),
1134 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1138 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1145 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1146 def perform(:follow_import, %User{} = follower, followed_identifiers)
1147 when is_list(followed_identifiers) do
1149 followed_identifiers,
1150 fn followed_identifier ->
1151 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1152 {:ok, follower} <- maybe_direct_follow(follower, followed),
1153 {:ok, _} <- ActivityPub.follow(follower, followed) do
1157 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1164 @spec external_users_query() :: Ecto.Query.t()
1165 def external_users_query do
1173 @spec external_users(keyword()) :: [User.t()]
1174 def external_users(opts \\ []) do
1176 external_users_query()
1177 |> select([u], struct(u, [:id, :ap_id, :info]))
1181 do: where(query, [u], u.id > ^opts[:max_id]),
1186 do: limit(query, ^opts[:limit]),
1192 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1193 BackgroundWorker.enqueue("blocks_import", %{
1194 "blocker_id" => blocker.id,
1195 "blocked_identifiers" => blocked_identifiers
1199 def follow_import(%User{} = follower, followed_identifiers)
1200 when is_list(followed_identifiers) do
1201 BackgroundWorker.enqueue("follow_import", %{
1202 "follower_id" => follower.id,
1203 "followed_identifiers" => followed_identifiers
1207 def delete_user_activities(%User{ap_id: ap_id} = user) do
1209 |> Activity.query_by_actor()
1210 |> RepoStreamer.chunk_stream(50)
1211 |> Stream.each(fn activities ->
1212 Enum.each(activities, &delete_activity(&1))
1219 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1221 |> Object.normalize()
1222 |> ActivityPub.delete()
1225 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1226 user = get_cached_by_ap_id(activity.actor)
1227 object = Object.normalize(activity)
1229 ActivityPub.unlike(user, object)
1232 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1233 user = get_cached_by_ap_id(activity.actor)
1234 object = Object.normalize(activity)
1236 ActivityPub.unannounce(user, object)
1239 defp delete_activity(_activity), do: "Doing nothing"
1241 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1242 Pleroma.HTML.Scrubber.TwitterText
1245 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1247 def fetch_by_ap_id(ap_id) do
1248 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1255 case OStatus.make_user(ap_id) do
1256 {:ok, user} -> {:ok, user}
1257 _ -> {:error, "Could not fetch by AP id"}
1262 def get_or_fetch_by_ap_id(ap_id) do
1263 user = get_cached_by_ap_id(ap_id)
1265 if !is_nil(user) and !User.needs_update?(user) do
1268 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1269 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1271 resp = fetch_by_ap_id(ap_id)
1273 if should_fetch_initial do
1274 with {:ok, %User{} = user} <- resp do
1275 fetch_initial_posts(user)
1283 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1284 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1285 if user = get_cached_by_ap_id(uri) do
1289 %User{info: %User.Info{}}
1290 |> cast(%{}, [:ap_id, :nickname, :local])
1291 |> put_change(:ap_id, uri)
1292 |> put_change(:nickname, nickname)
1293 |> put_change(:local, true)
1294 |> put_change(:follower_address, uri <> "/followers")
1296 {:ok, user} = Repo.insert(changes)
1302 def public_key_from_info(%{
1303 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1307 |> :public_key.pem_decode()
1309 |> :public_key.pem_entry_decode()
1315 def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
1316 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1319 def public_key_from_info(_), do: {:error, "not found key"}
1321 def get_public_key_for_ap_id(ap_id) do
1322 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1323 {:ok, public_key} <- public_key_from_info(user.info) do
1330 defp blank?(""), do: nil
1331 defp blank?(n), do: n
1333 def insert_or_update_user(data) do
1335 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1336 |> remote_user_creation()
1337 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1341 def ap_enabled?(%User{local: true}), do: true
1342 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1343 def ap_enabled?(_), do: false
1345 @doc "Gets or fetch a user by uri or nickname."
1346 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1347 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1348 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1350 # wait a period of time and return newest version of the User structs
1351 # this is because we have synchronous follow APIs and need to simulate them
1352 # with an async handshake
1353 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1354 with %User{} = a <- User.get_cached_by_id(a.id),
1355 %User{} = b <- User.get_cached_by_id(b.id) do
1363 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1364 with :ok <- :timer.sleep(timeout),
1365 %User{} = a <- User.get_cached_by_id(a.id),
1366 %User{} = b <- User.get_cached_by_id(b.id) do
1374 def parse_bio(bio) when is_binary(bio) and bio != "" do
1376 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1380 def parse_bio(_), do: ""
1382 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1383 # TODO: get profile URLs other than user.ap_id
1384 profile_urls = [user.ap_id]
1387 |> CommonUtils.format_input("text/plain",
1388 mentions_format: :full,
1389 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1394 def parse_bio(_, _), do: ""
1396 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1397 Repo.transaction(fn ->
1398 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1402 def tag(nickname, tags) when is_binary(nickname),
1403 do: tag(get_by_nickname(nickname), tags)
1405 def tag(%User{} = user, tags),
1406 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1408 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1409 Repo.transaction(fn ->
1410 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1414 def untag(nickname, tags) when is_binary(nickname),
1415 do: untag(get_by_nickname(nickname), tags)
1417 def untag(%User{} = user, tags),
1418 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1420 defp update_tags(%User{} = user, new_tags) do
1421 {:ok, updated_user} =
1423 |> change(%{tags: new_tags})
1424 |> update_and_set_cache()
1429 defp normalize_tags(tags) do
1432 |> Enum.map(&String.downcase(&1))
1435 defp local_nickname_regex do
1436 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1437 @extended_local_nickname_regex
1439 @strict_local_nickname_regex
1443 def local_nickname(nickname_or_mention) do
1446 |> String.split("@")
1450 def full_nickname(nickname_or_mention),
1451 do: String.trim_leading(nickname_or_mention, "@")
1453 def error_user(ap_id) do
1458 nickname: "erroruser@example.com",
1459 inserted_at: NaiveDateTime.utc_now()
1463 @spec all_superusers() :: [User.t()]
1464 def all_superusers do
1465 User.Query.build(%{super_users: true, local: true, deactivated: false})
1469 def showing_reblogs?(%User{} = user, %User{} = target) do
1470 target.ap_id not in user.info.muted_reblogs
1474 The function returns a query to get users with no activity for given interval of days.
1475 Inactive users are those who didn't read any notification, or had any activity where
1476 the user is the activity's actor, during `inactivity_threshold` days.
1477 Deactivated users will not appear in this list.
1481 iex> Pleroma.User.list_inactive_users()
1484 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1485 def list_inactive_users_query(inactivity_threshold \\ 7) do
1486 negative_inactivity_threshold = -inactivity_threshold
1487 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1488 # Subqueries are not supported in `where` clauses, join gets too complicated.
1489 has_read_notifications =
1490 from(n in Pleroma.Notification,
1491 where: n.seen == true,
1493 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1496 |> Pleroma.Repo.all()
1498 from(u in Pleroma.User,
1499 left_join: a in Pleroma.Activity,
1500 on: u.ap_id == a.actor,
1501 where: not is_nil(u.nickname),
1502 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1503 where: u.id not in ^has_read_notifications,
1506 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1507 is_nil(max(a.inserted_at))
1512 Enable or disable email notifications for user
1516 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1517 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1519 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1520 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1522 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1523 {:ok, t()} | {:error, Ecto.Changeset.t()}
1524 def switch_email_notifications(user, type, status) do
1525 info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})
1528 |> put_embed(:info, info)
1529 |> update_and_set_cache()
1533 Set `last_digest_emailed_at` value for the user to current time
1535 @spec touch_last_digest_emailed_at(t()) :: t()
1536 def touch_last_digest_emailed_at(user) do
1537 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1539 {:ok, updated_user} =
1541 |> change(%{last_digest_emailed_at: now})
1542 |> update_and_set_cache()
1547 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1548 def toggle_confirmation(%User{} = user) do
1549 need_confirmation? = !user.info.confirmation_pending
1552 User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
1556 |> put_embed(:info, info_changeset)
1557 |> update_and_set_cache()
1560 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1564 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1565 # use instance-default
1566 config = Pleroma.Config.get([:assets, :mascots])
1567 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1568 mascot = Keyword.get(config, default_mascot)
1571 "id" => "default-mascot",
1572 "url" => mascot[:url],
1573 "preview_url" => mascot[:url],
1575 "mime_type" => mascot[:mime_type]
1580 def ensure_keys_present(%User{info: info} = user) do
1584 {:ok, pem} = Keys.generate_rsa_pem()
1587 |> Ecto.Changeset.change()
1588 |> Ecto.Changeset.put_embed(:info, User.Info.set_keys(info, pem))
1589 |> update_and_set_cache()
1593 def get_ap_ids_by_nicknames(nicknames) do
1595 where: u.nickname in ^nicknames,
1601 defdelegate search(query, opts \\ []), to: User.Search
1603 defp put_password_hash(
1604 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1606 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1609 defp put_password_hash(changeset), do: changeset
1611 def is_internal_user?(%User{nickname: nil}), do: true
1612 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1613 def is_internal_user?(_), do: false