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
32 @type t :: %__MODULE__{}
34 @primary_key {:id, Pleroma.FlakeId, autogenerate: true}
36 # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
37 @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])?)*$/
39 @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
40 @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
44 field(:email, :string)
46 field(:nickname, :string)
47 field(:password_hash, :string)
48 field(:password, :string, virtual: true)
49 field(:password_confirmation, :string, virtual: true)
50 field(:following, {:array, :string}, default: [])
51 field(:ap_id, :string)
53 field(:local, :boolean, default: true)
54 field(:follower_address, :string)
55 field(:search_rank, :float, virtual: true)
56 field(:search_type, :integer, virtual: true)
57 field(:tags, {:array, :string}, default: [])
58 field(:last_refreshed_at, :naive_datetime_usec)
59 field(:last_digest_emailed_at, :naive_datetime)
60 has_many(:notifications, Notification)
61 has_many(:registrations, Registration)
62 embeds_one(:info, User.Info)
67 def auth_active?(%User{info: %User.Info{confirmation_pending: true}}),
68 do: !Pleroma.Config.get([:instance, :account_activation_required])
70 def auth_active?(%User{}), do: true
72 def visible_for?(user, for_user \\ nil)
74 def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
76 def visible_for?(%User{} = user, for_user) do
77 auth_active?(user) || superuser?(for_user)
80 def visible_for?(_, _), do: false
82 def superuser?(%User{local: true, info: %User.Info{is_admin: true}}), do: true
83 def superuser?(%User{local: true, info: %User.Info{is_moderator: true}}), do: true
84 def superuser?(_), do: false
86 def avatar_url(user, options \\ []) do
88 %{"url" => [%{"href" => href} | _]} -> href
89 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
93 def banner_url(user, options \\ []) do
94 case user.info.banner do
95 %{"url" => [%{"href" => href} | _]} -> href
96 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
100 def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url
101 def profile_url(%User{ap_id: ap_id}), do: ap_id
102 def profile_url(_), do: nil
104 def ap_id(%User{nickname: nickname}) do
105 "#{Web.base_url()}/users/#{nickname}"
108 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
109 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
111 def user_info(%User{} = user, args \\ %{}) do
113 if args[:following_count], do: args[:following_count], else: following_count(user)
116 if args[:follower_count], do: args[:follower_count], else: user.info.follower_count
119 note_count: user.info.note_count,
120 locked: user.info.locked,
121 confirmation_pending: user.info.confirmation_pending,
122 default_scope: user.info.default_scope
124 |> Map.put(:following_count, following_count)
125 |> Map.put(:follower_count, follower_count)
128 def set_info_cache(user, args) do
129 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user, args))
132 def restrict_deactivated(query) do
134 where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info)
138 def following_count(%User{following: []}), do: 0
140 def following_count(%User{} = user) do
142 |> get_friends_query()
143 |> Repo.aggregate(:count, :id)
146 def remote_user_creation(params) do
149 |> Map.put(:info, params[:info] || %{})
151 info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])
155 |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
156 |> validate_required([:name, :ap_id])
157 |> unique_constraint(:nickname)
158 |> validate_format(:nickname, @email_regex)
159 |> validate_length(:bio, max: 5000)
160 |> validate_length(:name, max: 100)
161 |> put_change(:local, false)
162 |> put_embed(:info, info_cng)
165 case info_cng.changes[:source_data] do
166 %{"followers" => followers} ->
168 |> put_change(:follower_address, followers)
171 followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
174 |> put_change(:follower_address, followers)
181 def update_changeset(struct, params \\ %{}) do
183 |> cast(params, [:bio, :name, :avatar, :following])
184 |> unique_constraint(:nickname)
185 |> validate_format(:nickname, local_nickname_regex())
186 |> validate_length(:bio, max: 5000)
187 |> validate_length(:name, min: 1, max: 100)
190 def upgrade_changeset(struct, params \\ %{}) do
193 |> Map.put(:last_refreshed_at, NaiveDateTime.utc_now())
197 |> User.Info.user_upgrade(params[:info])
200 |> cast(params, [:bio, :name, :follower_address, :avatar, :last_refreshed_at])
201 |> unique_constraint(:nickname)
202 |> validate_format(:nickname, local_nickname_regex())
203 |> validate_length(:bio, max: 5000)
204 |> validate_length(:name, max: 100)
205 |> put_embed(:info, info_cng)
208 def password_update_changeset(struct, params) do
210 |> cast(params, [:password, :password_confirmation])
211 |> validate_required([:password, :password_confirmation])
212 |> validate_confirmation(:password)
216 def reset_password(%User{id: user_id} = user, data) do
219 |> Multi.update(:user, password_update_changeset(user, data))
220 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
221 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
223 case Repo.transaction(multi) do
224 {:ok, %{user: user} = _} -> set_cache(user)
225 {:error, _, changeset, _} -> {:error, changeset}
229 def register_changeset(struct, params \\ %{}, opts \\ []) do
231 if is_nil(opts[:need_confirmation]) do
232 Pleroma.Config.get([:instance, :account_activation_required])
234 opts[:need_confirmation]
238 User.Info.confirmation_changeset(%User.Info{}, need_confirmation: need_confirmation?)
242 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
243 |> validate_required([:name, :nickname, :password, :password_confirmation])
244 |> validate_confirmation(:password)
245 |> unique_constraint(:email)
246 |> unique_constraint(:nickname)
247 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
248 |> validate_format(:nickname, local_nickname_regex())
249 |> validate_format(:email, @email_regex)
250 |> validate_length(:bio, max: 1000)
251 |> validate_length(:name, min: 1, max: 100)
252 |> put_change(:info, info_change)
255 if opts[:external] do
258 validate_required(changeset, [:email])
261 if changeset.valid? do
262 ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
263 followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
267 |> put_change(:ap_id, ap_id)
268 |> unique_constraint(:ap_id)
269 |> put_change(:following, [followers])
270 |> put_change(:follower_address, followers)
276 defp autofollow_users(user) do
277 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
280 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
283 follow_all(user, autofollowed_users)
286 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
287 def register(%Ecto.Changeset{} = changeset) do
288 with {:ok, user} <- Repo.insert(changeset),
289 {:ok, user} <- autofollow_users(user),
290 {:ok, user} <- set_cache(user),
291 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
292 {:ok, _} <- try_send_confirmation_email(user) do
297 def try_send_confirmation_email(%User{} = user) do
298 if user.info.confirmation_pending &&
299 Pleroma.Config.get([:instance, :account_activation_required]) do
301 |> Pleroma.Emails.UserEmail.account_confirmation_email()
302 |> Pleroma.Emails.Mailer.deliver_async()
310 def needs_update?(%User{local: true}), do: false
312 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
314 def needs_update?(%User{local: false} = user) do
315 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
318 def needs_update?(_), do: true
320 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
324 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
325 follow(follower, followed)
328 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
329 if not User.ap_enabled?(followed) do
330 follow(follower, followed)
336 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
337 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
338 def follow_all(follower, followeds) do
341 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
342 |> Enum.map(fn %{follower_address: fa} -> fa end)
346 where: u.id == ^follower.id,
351 "array(select distinct unnest (array_cat(?, ?)))",
360 {1, [follower]} = Repo.update_all(q, [])
362 Enum.each(followeds, fn followed ->
363 update_follower_count(followed)
369 def follow(%User{} = follower, %User{info: info} = followed) do
370 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
371 ap_followers = followed.follower_address
375 {:error, "Could not follow user: You are deactivated."}
377 deny_follow_blocked and blocks?(followed, follower) ->
378 {:error, "Could not follow user: #{followed.nickname} blocked you."}
381 if !followed.local && follower.local && !ap_enabled?(followed) do
382 Websub.subscribe(follower, followed)
387 where: u.id == ^follower.id,
388 update: [push: [following: ^ap_followers]],
392 {1, [follower]} = Repo.update_all(q, [])
394 {:ok, _} = update_follower_count(followed)
400 def unfollow(%User{} = follower, %User{} = followed) do
401 ap_followers = followed.follower_address
403 if following?(follower, followed) and follower.ap_id != followed.ap_id do
406 where: u.id == ^follower.id,
407 update: [pull: [following: ^ap_followers]],
411 {1, [follower]} = Repo.update_all(q, [])
413 {:ok, followed} = update_follower_count(followed)
417 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
419 {:error, "Not subscribed!"}
423 @spec following?(User.t(), User.t()) :: boolean
424 def following?(%User{} = follower, %User{} = followed) do
425 Enum.member?(follower.following, followed.follower_address)
428 def locked?(%User{} = user) do
429 user.info.locked || false
433 Repo.get_by(User, id: id)
436 def get_by_ap_id(ap_id) do
437 Repo.get_by(User, ap_id: ap_id)
440 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
441 # of the ap_id and the domain and tries to get that user
442 def get_by_guessed_nickname(ap_id) do
443 domain = URI.parse(ap_id).host
444 name = List.last(String.split(ap_id, "/"))
445 nickname = "#{name}@#{domain}"
447 get_cached_by_nickname(nickname)
450 def set_cache({:ok, user}), do: set_cache(user)
451 def set_cache({:error, err}), do: {:error, err}
453 def set_cache(%User{} = user) do
454 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
455 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
456 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
460 def update_and_set_cache(changeset) do
461 with {:ok, user} <- Repo.update(changeset) do
468 def invalidate_cache(user) do
469 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
470 Cachex.del(:user_cache, "nickname:#{user.nickname}")
471 Cachex.del(:user_cache, "user_info:#{user.id}")
474 def get_cached_by_ap_id(ap_id) do
475 key = "ap_id:#{ap_id}"
476 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
479 def get_cached_by_id(id) do
483 Cachex.fetch!(:user_cache, key, fn _ ->
487 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
488 {:commit, user.ap_id}
494 get_cached_by_ap_id(ap_id)
497 def get_cached_by_nickname(nickname) do
498 key = "nickname:#{nickname}"
500 Cachex.fetch!(:user_cache, key, fn ->
501 user_result = get_or_fetch_by_nickname(nickname)
504 {:ok, user} -> {:commit, user}
505 {:error, _error} -> {:ignore, nil}
510 def get_cached_by_nickname_or_id(nickname_or_id) do
511 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
514 def get_by_nickname(nickname) do
515 Repo.get_by(User, nickname: nickname) ||
516 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
517 Repo.get_by(User, nickname: local_nickname(nickname))
521 def get_by_email(email), do: Repo.get_by(User, email: email)
523 def get_by_nickname_or_email(nickname_or_email) do
524 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
527 def get_cached_user_info(user) do
528 key = "user_info:#{user.id}"
529 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
532 def fetch_by_nickname(nickname) do
533 ap_try = ActivityPub.make_user_from_nickname(nickname)
536 {:ok, user} -> {:ok, user}
537 _ -> OStatus.make_user(nickname)
541 def get_or_fetch_by_nickname(nickname) do
542 with %User{} = user <- get_by_nickname(nickname) do
546 with [_nick, _domain] <- String.split(nickname, "@"),
547 {:ok, user} <- fetch_by_nickname(nickname) do
548 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
549 fetch_initial_posts(user)
554 _e -> {:error, "not found " <> nickname}
559 @doc "Fetch some posts when the user has just been federated with"
560 def fetch_initial_posts(user),
561 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:fetch_initial_posts, user])
563 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
564 def get_followers_query(%User{} = user, nil) do
565 User.Query.build(%{followers: user, deactivated: false})
568 def get_followers_query(user, page) do
569 from(u in get_followers_query(user, nil))
570 |> User.Query.paginate(page, 20)
573 @spec get_followers_query(User.t()) :: Ecto.Query.t()
574 def get_followers_query(user), do: get_followers_query(user, nil)
576 def get_followers(user, page \\ nil) do
577 q = get_followers_query(user, page)
582 def get_followers_ids(user, page \\ nil) do
583 q = get_followers_query(user, page)
585 Repo.all(from(u in q, select: u.id))
588 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
589 def get_friends_query(%User{} = user, nil) do
590 User.Query.build(%{friends: user, deactivated: false})
593 def get_friends_query(user, page) do
594 from(u in get_friends_query(user, nil))
595 |> User.Query.paginate(page, 20)
598 @spec get_friends_query(User.t()) :: Ecto.Query.t()
599 def get_friends_query(user), do: get_friends_query(user, nil)
601 def get_friends(user, page \\ nil) do
602 q = get_friends_query(user, page)
607 def get_friends_ids(user, page \\ nil) do
608 q = get_friends_query(user, page)
610 Repo.all(from(u in q, select: u.id))
613 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
614 def get_follow_requests(%User{} = user) do
616 Activity.follow_requests_for_actor(user)
617 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
618 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
619 |> group_by([a, u], u.id)
626 def increase_note_count(%User{} = user) do
628 |> where(id: ^user.id)
633 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
640 |> Repo.update_all([])
642 {1, [user]} -> set_cache(user)
647 def decrease_note_count(%User{} = user) do
649 |> where(id: ^user.id)
654 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
661 |> Repo.update_all([])
663 {1, [user]} -> set_cache(user)
668 def update_note_count(%User{} = user) do
672 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
676 note_count = Repo.one(note_count_query)
678 info_cng = User.Info.set_note_count(user.info, note_count)
682 |> put_embed(:info, info_cng)
683 |> update_and_set_cache()
686 def update_follower_count(%User{} = user) do
687 follower_count_query =
688 User.Query.build(%{followers: user, deactivated: false})
689 |> select([u], %{count: count(u.id)})
692 |> where(id: ^user.id)
693 |> join(:inner, [u], s in subquery(follower_count_query))
698 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
705 |> Repo.update_all([])
707 {1, [user]} -> set_cache(user)
712 def remove_duplicated_following(%User{following: following} = user) do
713 uniq_following = Enum.uniq(following)
715 if length(following) == length(uniq_following) do
719 |> update_changeset(%{following: uniq_following})
720 |> update_and_set_cache()
724 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
725 def get_users_from_set(ap_ids, local_only \\ true) do
726 criteria = %{ap_id: ap_ids, deactivated: false}
727 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
729 User.Query.build(criteria)
733 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
734 def get_recipients_from_activity(%Activity{recipients: to}) do
735 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
739 def mute(muter, %User{ap_id: ap_id}) do
742 |> User.Info.add_to_mutes(ap_id)
746 |> put_embed(:info, info_cng)
748 update_and_set_cache(cng)
751 def unmute(muter, %{ap_id: ap_id}) do
754 |> User.Info.remove_from_mutes(ap_id)
758 |> put_embed(:info, info_cng)
760 update_and_set_cache(cng)
763 def subscribe(subscriber, %{ap_id: ap_id}) do
764 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
766 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
767 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
770 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
774 |> User.Info.add_to_subscribers(subscriber.ap_id)
777 |> put_embed(:info, info_cng)
778 |> update_and_set_cache()
783 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
784 with %User{} = user <- get_cached_by_ap_id(ap_id) do
787 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
790 |> put_embed(:info, info_cng)
791 |> update_and_set_cache()
795 def block(blocker, %User{ap_id: ap_id} = blocked) do
796 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
798 if following?(blocker, blocked) do
799 {:ok, blocker, _} = unfollow(blocker, blocked)
806 if subscribed_to?(blocked, blocker) do
807 {:ok, blocker} = unsubscribe(blocked, blocker)
813 if following?(blocked, blocker) do
814 unfollow(blocked, blocker)
817 {:ok, blocker} = update_follower_count(blocker)
821 |> User.Info.add_to_block(ap_id)
825 |> put_embed(:info, info_cng)
827 update_and_set_cache(cng)
830 # helper to handle the block given only an actor's AP id
831 def block(blocker, %{ap_id: ap_id}) do
832 block(blocker, get_cached_by_ap_id(ap_id))
835 def unblock(blocker, %{ap_id: ap_id}) do
838 |> User.Info.remove_from_block(ap_id)
842 |> put_embed(:info, info_cng)
844 update_and_set_cache(cng)
847 def mutes?(nil, _), do: false
848 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
850 def blocks?(%User{info: info} = _user, %{ap_id: ap_id}) do
852 domain_blocks = info.domain_blocks
853 %{host: host} = URI.parse(ap_id)
855 Enum.member?(blocks, ap_id) || Enum.any?(domain_blocks, &(&1 == host))
858 def subscribed_to?(user, %{ap_id: ap_id}) do
859 with %User{} = target <- get_cached_by_ap_id(ap_id) do
860 Enum.member?(target.info.subscribers, user.ap_id)
864 @spec muted_users(User.t()) :: [User.t()]
865 def muted_users(user) do
866 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
870 @spec blocked_users(User.t()) :: [User.t()]
871 def blocked_users(user) do
872 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
876 @spec subscribers(User.t()) :: [User.t()]
877 def subscribers(user) do
878 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
882 def block_domain(user, domain) do
885 |> User.Info.add_to_domain_block(domain)
889 |> put_embed(:info, info_cng)
891 update_and_set_cache(cng)
894 def unblock_domain(user, domain) do
897 |> User.Info.remove_from_domain_block(domain)
901 |> put_embed(:info, info_cng)
903 update_and_set_cache(cng)
906 def deactivate_async(user, status \\ true) do
907 PleromaJobQueue.enqueue(:background, __MODULE__, [:deactivate_async, user, status])
910 def deactivate(%User{} = user, status \\ true) do
911 info_cng = User.Info.set_activation_status(user.info, status)
913 with {:ok, friends} <- User.get_friends(user),
914 {:ok, followers} <- User.get_followers(user),
918 |> put_embed(:info, info_cng)
919 |> update_and_set_cache() do
920 Enum.each(followers, &invalidate_cache(&1))
921 Enum.each(friends, &update_follower_count(&1))
927 def update_notification_settings(%User{} = user, settings \\ %{}) do
928 info_changeset = User.Info.update_notification_settings(user.info, settings)
931 |> put_embed(:info, info_changeset)
932 |> update_and_set_cache()
935 @spec delete(User.t()) :: :ok
936 def delete(%User{} = user),
937 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:delete, user])
939 @spec perform(atom(), User.t()) :: {:ok, User.t()}
940 def perform(:delete, %User{} = user) do
941 # Remove all relationships
942 {:ok, followers} = User.get_followers(user)
944 Enum.each(followers, fn follower ->
945 ActivityPub.unfollow(follower, user)
946 User.unfollow(follower, user)
949 {:ok, friends} = User.get_friends(user)
951 Enum.each(friends, fn followed ->
952 ActivityPub.unfollow(user, followed)
953 User.unfollow(user, followed)
956 delete_user_activities(user)
958 {:ok, _user} = Repo.delete(user)
961 @spec perform(atom(), User.t()) :: {:ok, User.t()}
962 def perform(:fetch_initial_posts, %User{} = user) do
963 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
966 # Insert all the posts in reverse order, so they're in the right order on the timeline
967 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
968 &Pleroma.Web.Federator.incoming_ap_doc/1
974 def perform(:deactivate_async, user, status), do: deactivate(user, status)
976 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
977 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
978 when is_list(blocked_identifiers) do
981 fn blocked_identifier ->
982 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
983 {:ok, blocker} <- block(blocker, blocked),
984 {:ok, _} <- ActivityPub.block(blocker, blocked) do
988 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
995 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
996 def perform(:follow_import, %User{} = follower, followed_identifiers)
997 when is_list(followed_identifiers) do
999 followed_identifiers,
1000 fn followed_identifier ->
1001 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1002 {:ok, follower} <- maybe_direct_follow(follower, followed),
1003 {:ok, _} <- ActivityPub.follow(follower, followed) do
1007 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1014 @spec sync_follow_counter() :: :ok
1015 def sync_follow_counter,
1016 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:sync_follow_counters])
1018 @spec perform(:sync_follow_counters) :: :ok
1019 def perform(:sync_follow_counters) do
1020 {:ok, _pid} = Agent.start_link(fn -> %{} end, name: :domain_errors)
1021 config = Pleroma.Config.get([:instance, :external_user_synchronization])
1023 :ok = sync_follow_counters(config)
1024 Agent.stop(:domain_errors)
1027 @spec sync_follow_counters(keyword()) :: :ok
1028 def sync_follow_counters(opts \\ []) do
1029 users = external_users(opts)
1031 if length(users) > 0 do
1032 errors = Agent.get(:domain_errors, fn state -> state end)
1033 {last, updated_errors} = User.Synchronization.call(users, errors, opts)
1034 Agent.update(:domain_errors, fn _state -> updated_errors end)
1035 sync_follow_counters(max_id: last.id, limit: opts[:limit])
1041 @spec external_users(keyword()) :: [User.t()]
1042 def external_users(opts \\ []) do
1048 select: [:id, :ap_id, :info]
1053 do: where(query, [u], u.id > ^opts[:max_id]),
1058 do: limit(query, ^opts[:limit]),
1064 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers),
1066 PleromaJobQueue.enqueue(:background, __MODULE__, [
1072 def follow_import(%User{} = follower, followed_identifiers) when is_list(followed_identifiers),
1074 PleromaJobQueue.enqueue(:background, __MODULE__, [
1077 followed_identifiers
1080 def delete_user_activities(%User{ap_id: ap_id} = user) do
1082 |> Activity.query_by_actor()
1083 |> RepoStreamer.chunk_stream(50)
1084 |> Stream.each(fn activities ->
1085 Enum.each(activities, &delete_activity(&1))
1092 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1094 |> Object.normalize()
1095 |> ActivityPub.delete()
1098 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1099 user = get_cached_by_ap_id(activity.actor)
1100 object = Object.normalize(activity)
1102 ActivityPub.unlike(user, object)
1105 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1106 user = get_cached_by_ap_id(activity.actor)
1107 object = Object.normalize(activity)
1109 ActivityPub.unannounce(user, object)
1112 defp delete_activity(_activity), do: "Doing nothing"
1114 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1115 Pleroma.HTML.Scrubber.TwitterText
1118 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1120 def fetch_by_ap_id(ap_id) do
1121 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1128 case OStatus.make_user(ap_id) do
1129 {:ok, user} -> {:ok, user}
1130 _ -> {:error, "Could not fetch by AP id"}
1135 def get_or_fetch_by_ap_id(ap_id) do
1136 user = get_cached_by_ap_id(ap_id)
1138 if !is_nil(user) and !User.needs_update?(user) do
1141 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1142 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1144 resp = fetch_by_ap_id(ap_id)
1146 if should_fetch_initial do
1147 with {:ok, %User{} = user} <- resp do
1148 fetch_initial_posts(user)
1156 def get_or_create_instance_user do
1157 relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"
1159 if user = get_cached_by_ap_id(relay_uri) do
1163 %User{info: %User.Info{}}
1164 |> cast(%{}, [:ap_id, :nickname, :local])
1165 |> put_change(:ap_id, relay_uri)
1166 |> put_change(:nickname, nil)
1167 |> put_change(:local, true)
1168 |> put_change(:follower_address, relay_uri <> "/followers")
1170 {:ok, user} = Repo.insert(changes)
1176 def public_key_from_info(%{
1177 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1181 |> :public_key.pem_decode()
1183 |> :public_key.pem_entry_decode()
1189 def public_key_from_info(%{magic_key: magic_key}) do
1190 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1193 def get_public_key_for_ap_id(ap_id) do
1194 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1195 {:ok, public_key} <- public_key_from_info(user.info) do
1202 defp blank?(""), do: nil
1203 defp blank?(n), do: n
1205 def insert_or_update_user(data) do
1207 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1208 |> remote_user_creation()
1209 |> Repo.insert(on_conflict: :replace_all, conflict_target: :nickname)
1213 def ap_enabled?(%User{local: true}), do: true
1214 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1215 def ap_enabled?(_), do: false
1217 @doc "Gets or fetch a user by uri or nickname."
1218 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1219 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1220 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1222 # wait a period of time and return newest version of the User structs
1223 # this is because we have synchronous follow APIs and need to simulate them
1224 # with an async handshake
1225 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1226 with %User{} = a <- User.get_cached_by_id(a.id),
1227 %User{} = b <- User.get_cached_by_id(b.id) do
1235 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1236 with :ok <- :timer.sleep(timeout),
1237 %User{} = a <- User.get_cached_by_id(a.id),
1238 %User{} = b <- User.get_cached_by_id(b.id) do
1246 def parse_bio(bio) when is_binary(bio) and bio != "" do
1248 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1252 def parse_bio(_), do: ""
1254 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1255 # TODO: get profile URLs other than user.ap_id
1256 profile_urls = [user.ap_id]
1259 |> CommonUtils.format_input("text/plain",
1260 mentions_format: :full,
1261 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1266 def parse_bio(_, _), do: ""
1268 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1269 Repo.transaction(fn ->
1270 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1274 def tag(nickname, tags) when is_binary(nickname),
1275 do: tag(get_by_nickname(nickname), tags)
1277 def tag(%User{} = user, tags),
1278 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1280 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1281 Repo.transaction(fn ->
1282 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1286 def untag(nickname, tags) when is_binary(nickname),
1287 do: untag(get_by_nickname(nickname), tags)
1289 def untag(%User{} = user, tags),
1290 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1292 defp update_tags(%User{} = user, new_tags) do
1293 {:ok, updated_user} =
1295 |> change(%{tags: new_tags})
1296 |> update_and_set_cache()
1301 defp normalize_tags(tags) do
1304 |> Enum.map(&String.downcase(&1))
1307 defp local_nickname_regex do
1308 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1309 @extended_local_nickname_regex
1311 @strict_local_nickname_regex
1315 def local_nickname(nickname_or_mention) do
1318 |> String.split("@")
1322 def full_nickname(nickname_or_mention),
1323 do: String.trim_leading(nickname_or_mention, "@")
1325 def error_user(ap_id) do
1330 nickname: "erroruser@example.com",
1331 inserted_at: NaiveDateTime.utc_now()
1335 @spec all_superusers() :: [User.t()]
1336 def all_superusers do
1337 User.Query.build(%{super_users: true, local: true, deactivated: false})
1341 def showing_reblogs?(%User{} = user, %User{} = target) do
1342 target.ap_id not in user.info.muted_reblogs
1346 The function returns a query to get users with no activity for given interval of days.
1347 Inactive users are those who didn't read any notification, or had any activity where
1348 the user is the activity's actor, during `inactivity_threshold` days.
1349 Deactivated users will not appear in this list.
1353 iex> Pleroma.User.list_inactive_users()
1356 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1357 def list_inactive_users_query(inactivity_threshold \\ 7) do
1358 negative_inactivity_threshold = -inactivity_threshold
1359 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1360 # Subqueries are not supported in `where` clauses, join gets too complicated.
1361 has_read_notifications =
1362 from(n in Pleroma.Notification,
1363 where: n.seen == true,
1365 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1368 |> Pleroma.Repo.all()
1370 from(u in Pleroma.User,
1371 left_join: a in Pleroma.Activity,
1372 on: u.ap_id == a.actor,
1373 where: not is_nil(u.nickname),
1374 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1375 where: u.id not in ^has_read_notifications,
1378 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1379 is_nil(max(a.inserted_at))
1384 Enable or disable email notifications for user
1388 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1389 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1391 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1392 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1394 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1395 {:ok, t()} | {:error, Ecto.Changeset.t()}
1396 def switch_email_notifications(user, type, status) do
1397 info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})
1400 |> put_embed(:info, info)
1401 |> update_and_set_cache()
1405 Set `last_digest_emailed_at` value for the user to current time
1407 @spec touch_last_digest_emailed_at(t()) :: t()
1408 def touch_last_digest_emailed_at(user) do
1409 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1411 {:ok, updated_user} =
1413 |> change(%{last_digest_emailed_at: now})
1414 |> update_and_set_cache()
1419 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1420 def toggle_confirmation(%User{} = user) do
1421 need_confirmation? = !user.info.confirmation_pending
1424 User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
1428 |> put_embed(:info, info_changeset)
1429 |> update_and_set_cache()
1432 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1436 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1437 # use instance-default
1438 config = Pleroma.Config.get([:assets, :mascots])
1439 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1440 mascot = Keyword.get(config, default_mascot)
1443 "id" => "default-mascot",
1444 "url" => mascot[:url],
1445 "preview_url" => mascot[:url],
1447 "mime_type" => mascot[:mime_type]
1452 def ensure_keys_present(user) do
1458 {:ok, pem} = Keys.generate_rsa_pem()
1462 |> User.Info.set_keys(pem)
1465 Ecto.Changeset.change(user)
1466 |> Ecto.Changeset.put_embed(:info, info_cng)
1468 update_and_set_cache(cng)
1472 def get_ap_ids_by_nicknames(nicknames) do
1474 where: u.nickname in ^nicknames,
1480 defdelegate search(query, opts \\ []), to: User.Search
1482 defp put_password_hash(
1483 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1485 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1488 defp put_password_hash(changeset), do: changeset