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(:following_address, :string)
56 field(:search_rank, :float, virtual: true)
57 field(:search_type, :integer, virtual: true)
58 field(:tags, {:array, :string}, default: [])
59 field(:last_refreshed_at, :naive_datetime_usec)
60 field(:last_digest_emailed_at, :naive_datetime)
61 has_many(:notifications, Notification)
62 has_many(:registrations, Registration)
63 embeds_one(:info, User.Info)
68 def auth_active?(%User{info: %User.Info{confirmation_pending: true}}),
69 do: !Pleroma.Config.get([:instance, :account_activation_required])
71 def auth_active?(%User{}), do: true
73 def visible_for?(user, for_user \\ nil)
75 def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
77 def visible_for?(%User{} = user, for_user) do
78 auth_active?(user) || superuser?(for_user)
81 def visible_for?(_, _), do: false
83 def superuser?(%User{local: true, info: %User.Info{is_admin: true}}), do: true
84 def superuser?(%User{local: true, info: %User.Info{is_moderator: true}}), do: true
85 def superuser?(_), do: false
87 def avatar_url(user, options \\ []) do
89 %{"url" => [%{"href" => href} | _]} -> href
90 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
94 def banner_url(user, options \\ []) do
95 case user.info.banner do
96 %{"url" => [%{"href" => href} | _]} -> href
97 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
101 def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url
102 def profile_url(%User{ap_id: ap_id}), do: ap_id
103 def profile_url(_), do: nil
105 def ap_id(%User{nickname: nickname}) do
106 "#{Web.base_url()}/users/#{nickname}"
109 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
110 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
112 @spec ap_following(User.t()) :: Sring.t()
113 def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
114 def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
116 def user_info(%User{} = user, args \\ %{}) do
118 if args[:following_count], do: args[:following_count], else: following_count(user)
121 if args[:follower_count], do: args[:follower_count], else: user.info.follower_count
124 note_count: user.info.note_count,
125 locked: user.info.locked,
126 confirmation_pending: user.info.confirmation_pending,
127 default_scope: user.info.default_scope
129 |> Map.put(:following_count, following_count)
130 |> Map.put(:follower_count, follower_count)
133 def set_info_cache(user, args) do
134 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user, args))
137 @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
138 def restrict_deactivated(query) do
140 where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info)
144 def following_count(%User{following: []}), do: 0
146 def following_count(%User{} = user) do
148 |> get_friends_query()
149 |> Repo.aggregate(:count, :id)
152 def remote_user_creation(params) do
155 |> Map.put(:info, params[:info] || %{})
157 info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])
161 |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
162 |> validate_required([:name, :ap_id])
163 |> unique_constraint(:nickname)
164 |> validate_format(:nickname, @email_regex)
165 |> validate_length(:bio, max: 5000)
166 |> validate_length(:name, max: 100)
167 |> put_change(:local, false)
168 |> put_embed(:info, info_cng)
171 case info_cng.changes[:source_data] do
172 %{"followers" => followers, "following" => following} ->
174 |> put_change(:follower_address, followers)
175 |> put_change(:following_address, following)
178 followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
181 |> put_change(:follower_address, followers)
188 def update_changeset(struct, params \\ %{}) do
190 |> cast(params, [:bio, :name, :avatar, :following])
191 |> unique_constraint(:nickname)
192 |> validate_format(:nickname, local_nickname_regex())
193 |> validate_length(:bio, max: 5000)
194 |> validate_length(:name, min: 1, max: 100)
197 def upgrade_changeset(struct, params \\ %{}) do
200 |> Map.put(:last_refreshed_at, NaiveDateTime.utc_now())
204 |> User.Info.user_upgrade(params[:info])
215 |> unique_constraint(:nickname)
216 |> validate_format(:nickname, local_nickname_regex())
217 |> validate_length(:bio, max: 5000)
218 |> validate_length(:name, max: 100)
219 |> put_embed(:info, info_cng)
222 def password_update_changeset(struct, params) do
224 |> cast(params, [:password, :password_confirmation])
225 |> validate_required([:password, :password_confirmation])
226 |> validate_confirmation(:password)
230 def reset_password(%User{id: user_id} = user, data) do
233 |> Multi.update(:user, password_update_changeset(user, data))
234 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
235 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
237 case Repo.transaction(multi) do
238 {:ok, %{user: user} = _} -> set_cache(user)
239 {:error, _, changeset, _} -> {:error, changeset}
243 def register_changeset(struct, params \\ %{}, opts \\ []) do
245 if is_nil(opts[:need_confirmation]) do
246 Pleroma.Config.get([:instance, :account_activation_required])
248 opts[:need_confirmation]
252 User.Info.confirmation_changeset(%User.Info{}, need_confirmation: need_confirmation?)
256 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
257 |> validate_required([:name, :nickname, :password, :password_confirmation])
258 |> validate_confirmation(:password)
259 |> unique_constraint(:email)
260 |> unique_constraint(:nickname)
261 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
262 |> validate_format(:nickname, local_nickname_regex())
263 |> validate_format(:email, @email_regex)
264 |> validate_length(:bio, max: 1000)
265 |> validate_length(:name, min: 1, max: 100)
266 |> put_change(:info, info_change)
269 if opts[:external] do
272 validate_required(changeset, [:email])
275 if changeset.valid? do
276 ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
277 followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
281 |> put_change(:ap_id, ap_id)
282 |> unique_constraint(:ap_id)
283 |> put_change(:following, [followers])
284 |> put_change(:follower_address, followers)
290 defp autofollow_users(user) do
291 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
294 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
297 follow_all(user, autofollowed_users)
300 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
301 def register(%Ecto.Changeset{} = changeset) do
302 with {:ok, user} <- Repo.insert(changeset),
303 {:ok, user} <- autofollow_users(user),
304 {:ok, user} <- set_cache(user),
305 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
306 {:ok, _} <- try_send_confirmation_email(user) do
311 def try_send_confirmation_email(%User{} = user) do
312 if user.info.confirmation_pending &&
313 Pleroma.Config.get([:instance, :account_activation_required]) do
315 |> Pleroma.Emails.UserEmail.account_confirmation_email()
316 |> Pleroma.Emails.Mailer.deliver_async()
324 def needs_update?(%User{local: true}), do: false
326 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
328 def needs_update?(%User{local: false} = user) do
329 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
332 def needs_update?(_), do: true
334 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
338 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
339 follow(follower, followed)
342 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
343 if not User.ap_enabled?(followed) do
344 follow(follower, followed)
350 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
351 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
352 def follow_all(follower, followeds) do
355 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
356 |> Enum.map(fn %{follower_address: fa} -> fa end)
360 where: u.id == ^follower.id,
365 "array(select distinct unnest (array_cat(?, ?)))",
374 {1, [follower]} = Repo.update_all(q, [])
376 Enum.each(followeds, fn followed ->
377 update_follower_count(followed)
383 def follow(%User{} = follower, %User{info: info} = followed) do
384 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
385 ap_followers = followed.follower_address
389 {:error, "Could not follow user: You are deactivated."}
391 deny_follow_blocked and blocks?(followed, follower) ->
392 {:error, "Could not follow user: #{followed.nickname} blocked you."}
395 if !followed.local && follower.local && !ap_enabled?(followed) do
396 Websub.subscribe(follower, followed)
401 where: u.id == ^follower.id,
402 update: [push: [following: ^ap_followers]],
406 {1, [follower]} = Repo.update_all(q, [])
408 {:ok, _} = update_follower_count(followed)
414 def unfollow(%User{} = follower, %User{} = followed) do
415 ap_followers = followed.follower_address
417 if following?(follower, followed) and follower.ap_id != followed.ap_id do
420 where: u.id == ^follower.id,
421 update: [pull: [following: ^ap_followers]],
425 {1, [follower]} = Repo.update_all(q, [])
427 {:ok, followed} = update_follower_count(followed)
431 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
433 {:error, "Not subscribed!"}
437 @spec following?(User.t(), User.t()) :: boolean
438 def following?(%User{} = follower, %User{} = followed) do
439 Enum.member?(follower.following, followed.follower_address)
442 def locked?(%User{} = user) do
443 user.info.locked || false
447 Repo.get_by(User, id: id)
450 def get_by_ap_id(ap_id) do
451 Repo.get_by(User, ap_id: ap_id)
454 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
455 # of the ap_id and the domain and tries to get that user
456 def get_by_guessed_nickname(ap_id) do
457 domain = URI.parse(ap_id).host
458 name = List.last(String.split(ap_id, "/"))
459 nickname = "#{name}@#{domain}"
461 get_cached_by_nickname(nickname)
464 def set_cache({:ok, user}), do: set_cache(user)
465 def set_cache({:error, err}), do: {:error, err}
467 def set_cache(%User{} = user) do
468 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
469 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
470 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
474 def update_and_set_cache(changeset) do
475 with {:ok, user} <- Repo.update(changeset) do
482 def invalidate_cache(user) do
483 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
484 Cachex.del(:user_cache, "nickname:#{user.nickname}")
485 Cachex.del(:user_cache, "user_info:#{user.id}")
488 def get_cached_by_ap_id(ap_id) do
489 key = "ap_id:#{ap_id}"
490 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
493 def get_cached_by_id(id) do
497 Cachex.fetch!(:user_cache, key, fn _ ->
501 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
502 {:commit, user.ap_id}
508 get_cached_by_ap_id(ap_id)
511 def get_cached_by_nickname(nickname) do
512 key = "nickname:#{nickname}"
514 Cachex.fetch!(:user_cache, key, fn ->
515 user_result = get_or_fetch_by_nickname(nickname)
518 {:ok, user} -> {:commit, user}
519 {:error, _error} -> {:ignore, nil}
524 def get_cached_by_nickname_or_id(nickname_or_id) do
525 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
528 def get_by_nickname(nickname) do
529 Repo.get_by(User, nickname: nickname) ||
530 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
531 Repo.get_by(User, nickname: local_nickname(nickname))
535 def get_by_email(email), do: Repo.get_by(User, email: email)
537 def get_by_nickname_or_email(nickname_or_email) do
538 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
541 def get_cached_user_info(user) do
542 key = "user_info:#{user.id}"
543 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
546 def fetch_by_nickname(nickname) do
547 ap_try = ActivityPub.make_user_from_nickname(nickname)
550 {:ok, user} -> {:ok, user}
551 _ -> OStatus.make_user(nickname)
555 def get_or_fetch_by_nickname(nickname) do
556 with %User{} = user <- get_by_nickname(nickname) do
560 with [_nick, _domain] <- String.split(nickname, "@"),
561 {:ok, user} <- fetch_by_nickname(nickname) do
562 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
563 fetch_initial_posts(user)
568 _e -> {:error, "not found " <> nickname}
573 @doc "Fetch some posts when the user has just been federated with"
574 def fetch_initial_posts(user),
575 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:fetch_initial_posts, user])
577 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
578 def get_followers_query(%User{} = user, nil) do
579 User.Query.build(%{followers: user, deactivated: false})
582 def get_followers_query(user, page) do
583 from(u in get_followers_query(user, nil))
584 |> User.Query.paginate(page, 20)
587 @spec get_followers_query(User.t()) :: Ecto.Query.t()
588 def get_followers_query(user), do: get_followers_query(user, nil)
590 def get_followers(user, page \\ nil) do
591 q = get_followers_query(user, page)
596 def get_followers_ids(user, page \\ nil) do
597 q = get_followers_query(user, page)
599 Repo.all(from(u in q, select: u.id))
602 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
603 def get_friends_query(%User{} = user, nil) do
604 User.Query.build(%{friends: user, deactivated: false})
607 def get_friends_query(user, page) do
608 from(u in get_friends_query(user, nil))
609 |> User.Query.paginate(page, 20)
612 @spec get_friends_query(User.t()) :: Ecto.Query.t()
613 def get_friends_query(user), do: get_friends_query(user, nil)
615 def get_friends(user, page \\ nil) do
616 q = get_friends_query(user, page)
621 def get_friends_ids(user, page \\ nil) do
622 q = get_friends_query(user, page)
624 Repo.all(from(u in q, select: u.id))
627 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
628 def get_follow_requests(%User{} = user) do
630 Activity.follow_requests_for_actor(user)
631 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
632 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
633 |> group_by([a, u], u.id)
640 def increase_note_count(%User{} = user) do
642 |> where(id: ^user.id)
647 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
654 |> Repo.update_all([])
656 {1, [user]} -> set_cache(user)
661 def decrease_note_count(%User{} = user) do
663 |> where(id: ^user.id)
668 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
675 |> Repo.update_all([])
677 {1, [user]} -> set_cache(user)
682 def update_note_count(%User{} = user) do
686 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
690 note_count = Repo.one(note_count_query)
692 info_cng = User.Info.set_note_count(user.info, note_count)
696 |> put_embed(:info, info_cng)
697 |> update_and_set_cache()
700 def update_follower_count(%User{} = user) do
701 follower_count_query =
702 User.Query.build(%{followers: user, deactivated: false})
703 |> select([u], %{count: count(u.id)})
706 |> where(id: ^user.id)
707 |> join(:inner, [u], s in subquery(follower_count_query))
712 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
719 |> Repo.update_all([])
721 {1, [user]} -> set_cache(user)
726 def remove_duplicated_following(%User{following: following} = user) do
727 uniq_following = Enum.uniq(following)
729 if length(following) == length(uniq_following) do
733 |> update_changeset(%{following: uniq_following})
734 |> update_and_set_cache()
738 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
739 def get_users_from_set(ap_ids, local_only \\ true) do
740 criteria = %{ap_id: ap_ids, deactivated: false}
741 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
743 User.Query.build(criteria)
747 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
748 def get_recipients_from_activity(%Activity{recipients: to}) do
749 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
753 def mute(muter, %User{ap_id: ap_id}) do
756 |> User.Info.add_to_mutes(ap_id)
760 |> put_embed(:info, info_cng)
762 update_and_set_cache(cng)
765 def unmute(muter, %{ap_id: ap_id}) do
768 |> User.Info.remove_from_mutes(ap_id)
772 |> put_embed(:info, info_cng)
774 update_and_set_cache(cng)
777 def subscribe(subscriber, %{ap_id: ap_id}) do
778 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
780 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
781 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
784 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
788 |> User.Info.add_to_subscribers(subscriber.ap_id)
791 |> put_embed(:info, info_cng)
792 |> update_and_set_cache()
797 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
798 with %User{} = user <- get_cached_by_ap_id(ap_id) do
801 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
804 |> put_embed(:info, info_cng)
805 |> update_and_set_cache()
809 def block(blocker, %User{ap_id: ap_id} = blocked) do
810 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
812 if following?(blocker, blocked) do
813 {:ok, blocker, _} = unfollow(blocker, blocked)
820 if subscribed_to?(blocked, blocker) do
821 {:ok, blocker} = unsubscribe(blocked, blocker)
827 if following?(blocked, blocker) do
828 unfollow(blocked, blocker)
831 {:ok, blocker} = update_follower_count(blocker)
835 |> User.Info.add_to_block(ap_id)
839 |> put_embed(:info, info_cng)
841 update_and_set_cache(cng)
844 # helper to handle the block given only an actor's AP id
845 def block(blocker, %{ap_id: ap_id}) do
846 block(blocker, get_cached_by_ap_id(ap_id))
849 def unblock(blocker, %{ap_id: ap_id}) do
852 |> User.Info.remove_from_block(ap_id)
856 |> put_embed(:info, info_cng)
858 update_and_set_cache(cng)
861 def mutes?(nil, _), do: false
862 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
864 def blocks?(%User{info: info} = _user, %{ap_id: ap_id}) do
866 domain_blocks = info.domain_blocks
867 %{host: host} = URI.parse(ap_id)
869 Enum.member?(blocks, ap_id) || Enum.any?(domain_blocks, &(&1 == host))
872 def subscribed_to?(user, %{ap_id: ap_id}) do
873 with %User{} = target <- get_cached_by_ap_id(ap_id) do
874 Enum.member?(target.info.subscribers, user.ap_id)
878 @spec muted_users(User.t()) :: [User.t()]
879 def muted_users(user) do
880 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
884 @spec blocked_users(User.t()) :: [User.t()]
885 def blocked_users(user) do
886 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
890 @spec subscribers(User.t()) :: [User.t()]
891 def subscribers(user) do
892 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
896 def block_domain(user, domain) do
899 |> User.Info.add_to_domain_block(domain)
903 |> put_embed(:info, info_cng)
905 update_and_set_cache(cng)
908 def unblock_domain(user, domain) do
911 |> User.Info.remove_from_domain_block(domain)
915 |> put_embed(:info, info_cng)
917 update_and_set_cache(cng)
920 def deactivate_async(user, status \\ true) do
921 PleromaJobQueue.enqueue(:background, __MODULE__, [:deactivate_async, user, status])
924 def deactivate(%User{} = user, status \\ true) do
925 info_cng = User.Info.set_activation_status(user.info, status)
927 with {:ok, friends} <- User.get_friends(user),
928 {:ok, followers} <- User.get_followers(user),
932 |> put_embed(:info, info_cng)
933 |> update_and_set_cache() do
934 Enum.each(followers, &invalidate_cache(&1))
935 Enum.each(friends, &update_follower_count(&1))
941 def update_notification_settings(%User{} = user, settings \\ %{}) do
942 info_changeset = User.Info.update_notification_settings(user.info, settings)
945 |> put_embed(:info, info_changeset)
946 |> update_and_set_cache()
949 @spec delete(User.t()) :: :ok
950 def delete(%User{} = user),
951 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:delete, user])
953 @spec perform(atom(), User.t()) :: {:ok, User.t()}
954 def perform(:delete, %User{} = user) do
955 {:ok, _user} = ActivityPub.delete(user)
957 # Remove all relationships
958 {:ok, followers} = User.get_followers(user)
960 Enum.each(followers, fn follower ->
961 ActivityPub.unfollow(follower, user)
962 User.unfollow(follower, user)
965 {:ok, friends} = User.get_friends(user)
967 Enum.each(friends, fn followed ->
968 ActivityPub.unfollow(user, followed)
969 User.unfollow(user, followed)
972 delete_user_activities(user)
973 invalidate_cache(user)
977 @spec perform(atom(), User.t()) :: {:ok, User.t()}
978 def perform(:fetch_initial_posts, %User{} = user) do
979 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
982 # Insert all the posts in reverse order, so they're in the right order on the timeline
983 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
984 &Pleroma.Web.Federator.incoming_ap_doc/1
990 def perform(:deactivate_async, user, status), do: deactivate(user, status)
992 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
993 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
994 when is_list(blocked_identifiers) do
997 fn blocked_identifier ->
998 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
999 {:ok, blocker} <- block(blocker, blocked),
1000 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1004 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1011 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1012 def perform(:follow_import, %User{} = follower, followed_identifiers)
1013 when is_list(followed_identifiers) do
1015 followed_identifiers,
1016 fn followed_identifier ->
1017 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1018 {:ok, follower} <- maybe_direct_follow(follower, followed),
1019 {:ok, _} <- ActivityPub.follow(follower, followed) do
1023 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1030 @spec external_users_query() :: Ecto.Query.t()
1031 def external_users_query do
1039 @spec external_users(keyword()) :: [User.t()]
1040 def external_users(opts \\ []) do
1042 external_users_query()
1043 |> select([u], struct(u, [:id, :ap_id, :info]))
1047 do: where(query, [u], u.id > ^opts[:max_id]),
1052 do: limit(query, ^opts[:limit]),
1058 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers),
1060 PleromaJobQueue.enqueue(:background, __MODULE__, [
1066 def follow_import(%User{} = follower, followed_identifiers) when is_list(followed_identifiers),
1068 PleromaJobQueue.enqueue(:background, __MODULE__, [
1071 followed_identifiers
1074 def delete_user_activities(%User{ap_id: ap_id} = user) do
1076 |> Activity.query_by_actor()
1077 |> RepoStreamer.chunk_stream(50)
1078 |> Stream.each(fn activities ->
1079 Enum.each(activities, &delete_activity(&1))
1086 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1088 |> Object.normalize()
1089 |> ActivityPub.delete()
1092 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1093 user = get_cached_by_ap_id(activity.actor)
1094 object = Object.normalize(activity)
1096 ActivityPub.unlike(user, object)
1099 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1100 user = get_cached_by_ap_id(activity.actor)
1101 object = Object.normalize(activity)
1103 ActivityPub.unannounce(user, object)
1106 defp delete_activity(_activity), do: "Doing nothing"
1108 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1109 Pleroma.HTML.Scrubber.TwitterText
1112 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1114 def fetch_by_ap_id(ap_id) do
1115 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1122 case OStatus.make_user(ap_id) do
1123 {:ok, user} -> {:ok, user}
1124 _ -> {:error, "Could not fetch by AP id"}
1129 def get_or_fetch_by_ap_id(ap_id) do
1130 user = get_cached_by_ap_id(ap_id)
1132 if !is_nil(user) and !User.needs_update?(user) do
1135 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1136 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1138 resp = fetch_by_ap_id(ap_id)
1140 if should_fetch_initial do
1141 with {:ok, %User{} = user} <- resp do
1142 fetch_initial_posts(user)
1150 def get_or_create_instance_user do
1151 relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"
1153 if user = get_cached_by_ap_id(relay_uri) do
1157 %User{info: %User.Info{}}
1158 |> cast(%{}, [:ap_id, :nickname, :local])
1159 |> put_change(:ap_id, relay_uri)
1160 |> put_change(:nickname, nil)
1161 |> put_change(:local, true)
1162 |> put_change(:follower_address, relay_uri <> "/followers")
1164 {:ok, user} = Repo.insert(changes)
1170 def public_key_from_info(%{
1171 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1175 |> :public_key.pem_decode()
1177 |> :public_key.pem_entry_decode()
1183 def public_key_from_info(%{magic_key: magic_key}) do
1184 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1187 def get_public_key_for_ap_id(ap_id) do
1188 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1189 {:ok, public_key} <- public_key_from_info(user.info) do
1196 defp blank?(""), do: nil
1197 defp blank?(n), do: n
1199 def insert_or_update_user(data) do
1201 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1202 |> remote_user_creation()
1203 |> Repo.insert(on_conflict: :replace_all, conflict_target: :nickname)
1207 def ap_enabled?(%User{local: true}), do: true
1208 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1209 def ap_enabled?(_), do: false
1211 @doc "Gets or fetch a user by uri or nickname."
1212 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1213 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1214 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1216 # wait a period of time and return newest version of the User structs
1217 # this is because we have synchronous follow APIs and need to simulate them
1218 # with an async handshake
1219 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1220 with %User{} = a <- User.get_cached_by_id(a.id),
1221 %User{} = b <- User.get_cached_by_id(b.id) do
1229 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1230 with :ok <- :timer.sleep(timeout),
1231 %User{} = a <- User.get_cached_by_id(a.id),
1232 %User{} = b <- User.get_cached_by_id(b.id) do
1240 def parse_bio(bio) when is_binary(bio) and bio != "" do
1242 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1246 def parse_bio(_), do: ""
1248 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1249 # TODO: get profile URLs other than user.ap_id
1250 profile_urls = [user.ap_id]
1253 |> CommonUtils.format_input("text/plain",
1254 mentions_format: :full,
1255 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1260 def parse_bio(_, _), do: ""
1262 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1263 Repo.transaction(fn ->
1264 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1268 def tag(nickname, tags) when is_binary(nickname),
1269 do: tag(get_by_nickname(nickname), tags)
1271 def tag(%User{} = user, tags),
1272 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1274 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1275 Repo.transaction(fn ->
1276 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1280 def untag(nickname, tags) when is_binary(nickname),
1281 do: untag(get_by_nickname(nickname), tags)
1283 def untag(%User{} = user, tags),
1284 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1286 defp update_tags(%User{} = user, new_tags) do
1287 {:ok, updated_user} =
1289 |> change(%{tags: new_tags})
1290 |> update_and_set_cache()
1295 defp normalize_tags(tags) do
1298 |> Enum.map(&String.downcase(&1))
1301 defp local_nickname_regex do
1302 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1303 @extended_local_nickname_regex
1305 @strict_local_nickname_regex
1309 def local_nickname(nickname_or_mention) do
1312 |> String.split("@")
1316 def full_nickname(nickname_or_mention),
1317 do: String.trim_leading(nickname_or_mention, "@")
1319 def error_user(ap_id) do
1324 nickname: "erroruser@example.com",
1325 inserted_at: NaiveDateTime.utc_now()
1329 @spec all_superusers() :: [User.t()]
1330 def all_superusers do
1331 User.Query.build(%{super_users: true, local: true, deactivated: false})
1335 def showing_reblogs?(%User{} = user, %User{} = target) do
1336 target.ap_id not in user.info.muted_reblogs
1340 The function returns a query to get users with no activity for given interval of days.
1341 Inactive users are those who didn't read any notification, or had any activity where
1342 the user is the activity's actor, during `inactivity_threshold` days.
1343 Deactivated users will not appear in this list.
1347 iex> Pleroma.User.list_inactive_users()
1350 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1351 def list_inactive_users_query(inactivity_threshold \\ 7) do
1352 negative_inactivity_threshold = -inactivity_threshold
1353 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1354 # Subqueries are not supported in `where` clauses, join gets too complicated.
1355 has_read_notifications =
1356 from(n in Pleroma.Notification,
1357 where: n.seen == true,
1359 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1362 |> Pleroma.Repo.all()
1364 from(u in Pleroma.User,
1365 left_join: a in Pleroma.Activity,
1366 on: u.ap_id == a.actor,
1367 where: not is_nil(u.nickname),
1368 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1369 where: u.id not in ^has_read_notifications,
1372 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1373 is_nil(max(a.inserted_at))
1378 Enable or disable email notifications for user
1382 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1383 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1385 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1386 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1388 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1389 {:ok, t()} | {:error, Ecto.Changeset.t()}
1390 def switch_email_notifications(user, type, status) do
1391 info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})
1394 |> put_embed(:info, info)
1395 |> update_and_set_cache()
1399 Set `last_digest_emailed_at` value for the user to current time
1401 @spec touch_last_digest_emailed_at(t()) :: t()
1402 def touch_last_digest_emailed_at(user) do
1403 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1405 {:ok, updated_user} =
1407 |> change(%{last_digest_emailed_at: now})
1408 |> update_and_set_cache()
1413 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1414 def toggle_confirmation(%User{} = user) do
1415 need_confirmation? = !user.info.confirmation_pending
1418 User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
1422 |> put_embed(:info, info_changeset)
1423 |> update_and_set_cache()
1426 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1430 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1431 # use instance-default
1432 config = Pleroma.Config.get([:assets, :mascots])
1433 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1434 mascot = Keyword.get(config, default_mascot)
1437 "id" => "default-mascot",
1438 "url" => mascot[:url],
1439 "preview_url" => mascot[:url],
1441 "mime_type" => mascot[:mime_type]
1446 def ensure_keys_present(user) do
1452 {:ok, pem} = Keys.generate_rsa_pem()
1456 |> User.Info.set_keys(pem)
1459 Ecto.Changeset.change(user)
1460 |> Ecto.Changeset.put_embed(:info, info_cng)
1462 update_and_set_cache(cng)
1466 def get_ap_ids_by_nicknames(nicknames) do
1468 where: u.nickname in ^nicknames,
1474 defdelegate search(query, opts \\ []), to: User.Search
1476 defp put_password_hash(
1477 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1479 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1482 defp put_password_hash(changeset), do: changeset