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) do
113 following_count: following_count(user),
114 note_count: user.info.note_count,
115 follower_count: user.info.follower_count,
116 locked: user.info.locked,
117 confirmation_pending: user.info.confirmation_pending,
118 default_scope: user.info.default_scope
122 def restrict_deactivated(query) do
124 where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info)
128 def following_count(%User{following: []}), do: 0
130 def following_count(%User{} = user) do
132 |> get_friends_query()
133 |> Repo.aggregate(:count, :id)
136 def remote_user_creation(params) do
139 |> Map.put(:info, params[:info] || %{})
141 info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])
145 |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
146 |> validate_required([:name, :ap_id])
147 |> unique_constraint(:nickname)
148 |> validate_format(:nickname, @email_regex)
149 |> validate_length(:bio, max: 5000)
150 |> validate_length(:name, max: 100)
151 |> put_change(:local, false)
152 |> put_embed(:info, info_cng)
155 case info_cng.changes[:source_data] do
156 %{"followers" => followers} ->
158 |> put_change(:follower_address, followers)
161 followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
164 |> put_change(:follower_address, followers)
171 def update_changeset(struct, params \\ %{}) do
173 |> cast(params, [:bio, :name, :avatar, :following])
174 |> unique_constraint(:nickname)
175 |> validate_format(:nickname, local_nickname_regex())
176 |> validate_length(:bio, max: 5000)
177 |> validate_length(:name, min: 1, max: 100)
180 def upgrade_changeset(struct, params \\ %{}) do
183 |> Map.put(:last_refreshed_at, NaiveDateTime.utc_now())
187 |> User.Info.user_upgrade(params[:info])
190 |> cast(params, [:bio, :name, :follower_address, :avatar, :last_refreshed_at])
191 |> unique_constraint(:nickname)
192 |> validate_format(:nickname, local_nickname_regex())
193 |> validate_length(:bio, max: 5000)
194 |> validate_length(:name, max: 100)
195 |> put_embed(:info, info_cng)
198 def password_update_changeset(struct, params) do
200 |> cast(params, [:password, :password_confirmation])
201 |> validate_required([:password, :password_confirmation])
202 |> validate_confirmation(:password)
206 def reset_password(%User{id: user_id} = user, data) do
209 |> Multi.update(:user, password_update_changeset(user, data))
210 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
211 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
213 case Repo.transaction(multi) do
214 {:ok, %{user: user} = _} -> set_cache(user)
215 {:error, _, changeset, _} -> {:error, changeset}
219 def register_changeset(struct, params \\ %{}, opts \\ []) do
221 if is_nil(opts[:need_confirmation]) do
222 Pleroma.Config.get([:instance, :account_activation_required])
224 opts[:need_confirmation]
228 User.Info.confirmation_changeset(%User.Info{}, need_confirmation: need_confirmation?)
232 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
233 |> validate_required([:name, :nickname, :password, :password_confirmation])
234 |> validate_confirmation(:password)
235 |> unique_constraint(:email)
236 |> unique_constraint(:nickname)
237 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
238 |> validate_format(:nickname, local_nickname_regex())
239 |> validate_format(:email, @email_regex)
240 |> validate_length(:bio, max: 1000)
241 |> validate_length(:name, min: 1, max: 100)
242 |> put_change(:info, info_change)
245 if opts[:external] do
248 validate_required(changeset, [:email])
251 if changeset.valid? do
252 ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
253 followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
257 |> put_change(:ap_id, ap_id)
258 |> unique_constraint(:ap_id)
259 |> put_change(:following, [followers])
260 |> put_change(:follower_address, followers)
266 defp autofollow_users(user) do
267 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
270 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
273 follow_all(user, autofollowed_users)
276 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
277 def register(%Ecto.Changeset{} = changeset) do
278 with {:ok, user} <- Repo.insert(changeset),
279 {:ok, user} <- autofollow_users(user),
280 {:ok, user} <- set_cache(user),
281 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
282 {:ok, _} <- try_send_confirmation_email(user) do
287 def try_send_confirmation_email(%User{} = user) do
288 if user.info.confirmation_pending &&
289 Pleroma.Config.get([:instance, :account_activation_required]) do
291 |> Pleroma.Emails.UserEmail.account_confirmation_email()
292 |> Pleroma.Emails.Mailer.deliver_async()
300 def needs_update?(%User{local: true}), do: false
302 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
304 def needs_update?(%User{local: false} = user) do
305 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
308 def needs_update?(_), do: true
310 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
314 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
315 follow(follower, followed)
318 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
319 if not User.ap_enabled?(followed) do
320 follow(follower, followed)
326 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
327 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
328 def follow_all(follower, followeds) do
331 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
332 |> Enum.map(fn %{follower_address: fa} -> fa end)
336 where: u.id == ^follower.id,
341 "array(select distinct unnest (array_cat(?, ?)))",
350 {1, [follower]} = Repo.update_all(q, [])
352 Enum.each(followeds, fn followed ->
353 update_follower_count(followed)
359 def follow(%User{} = follower, %User{info: info} = followed) do
360 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
361 ap_followers = followed.follower_address
365 {:error, "Could not follow user: You are deactivated."}
367 deny_follow_blocked and blocks?(followed, follower) ->
368 {:error, "Could not follow user: #{followed.nickname} blocked you."}
371 if !followed.local && follower.local && !ap_enabled?(followed) do
372 Websub.subscribe(follower, followed)
377 where: u.id == ^follower.id,
378 update: [push: [following: ^ap_followers]],
382 {1, [follower]} = Repo.update_all(q, [])
384 {:ok, _} = update_follower_count(followed)
390 def unfollow(%User{} = follower, %User{} = followed) do
391 ap_followers = followed.follower_address
393 if following?(follower, followed) and follower.ap_id != followed.ap_id do
396 where: u.id == ^follower.id,
397 update: [pull: [following: ^ap_followers]],
401 {1, [follower]} = Repo.update_all(q, [])
403 {:ok, followed} = update_follower_count(followed)
407 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
409 {:error, "Not subscribed!"}
413 @spec following?(User.t(), User.t()) :: boolean
414 def following?(%User{} = follower, %User{} = followed) do
415 Enum.member?(follower.following, followed.follower_address)
418 def locked?(%User{} = user) do
419 user.info.locked || false
423 Repo.get_by(User, id: id)
426 def get_by_ap_id(ap_id) do
427 Repo.get_by(User, ap_id: ap_id)
430 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
431 # of the ap_id and the domain and tries to get that user
432 def get_by_guessed_nickname(ap_id) do
433 domain = URI.parse(ap_id).host
434 name = List.last(String.split(ap_id, "/"))
435 nickname = "#{name}@#{domain}"
437 get_cached_by_nickname(nickname)
440 def set_cache({:ok, user}), do: set_cache(user)
441 def set_cache({:error, err}), do: {:error, err}
443 def set_cache(%User{} = user) do
444 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
445 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
446 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
450 def update_and_set_cache(changeset) do
451 with {:ok, user} <- Repo.update(changeset) do
458 def invalidate_cache(user) do
459 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
460 Cachex.del(:user_cache, "nickname:#{user.nickname}")
461 Cachex.del(:user_cache, "user_info:#{user.id}")
464 def get_cached_by_ap_id(ap_id) do
465 key = "ap_id:#{ap_id}"
466 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
469 def get_cached_by_id(id) do
473 Cachex.fetch!(:user_cache, key, fn _ ->
477 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
478 {:commit, user.ap_id}
484 get_cached_by_ap_id(ap_id)
487 def get_cached_by_nickname(nickname) do
488 key = "nickname:#{nickname}"
490 Cachex.fetch!(:user_cache, key, fn ->
491 user_result = get_or_fetch_by_nickname(nickname)
494 {:ok, user} -> {:commit, user}
495 {:error, _error} -> {:ignore, nil}
500 def get_cached_by_nickname_or_id(nickname_or_id) do
501 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
504 def get_by_nickname(nickname) do
505 Repo.get_by(User, nickname: nickname) ||
506 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
507 Repo.get_by(User, nickname: local_nickname(nickname))
511 def get_by_email(email), do: Repo.get_by(User, email: email)
513 def get_by_nickname_or_email(nickname_or_email) do
514 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
517 def get_cached_user_info(user) do
518 key = "user_info:#{user.id}"
519 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
522 def fetch_by_nickname(nickname) do
523 ap_try = ActivityPub.make_user_from_nickname(nickname)
526 {:ok, user} -> {:ok, user}
527 _ -> OStatus.make_user(nickname)
531 def get_or_fetch_by_nickname(nickname) do
532 with %User{} = user <- get_by_nickname(nickname) do
536 with [_nick, _domain] <- String.split(nickname, "@"),
537 {:ok, user} <- fetch_by_nickname(nickname) do
538 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
539 fetch_initial_posts(user)
544 _e -> {:error, "not found " <> nickname}
549 @doc "Fetch some posts when the user has just been federated with"
550 def fetch_initial_posts(user),
551 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:fetch_initial_posts, user])
553 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
554 def get_followers_query(%User{} = user, nil) do
555 User.Query.build(%{followers: user, deactivated: false})
558 def get_followers_query(user, page) do
559 from(u in get_followers_query(user, nil))
560 |> User.Query.paginate(page, 20)
563 @spec get_followers_query(User.t()) :: Ecto.Query.t()
564 def get_followers_query(user), do: get_followers_query(user, nil)
566 def get_followers(user, page \\ nil) do
567 q = get_followers_query(user, page)
572 def get_followers_ids(user, page \\ nil) do
573 q = get_followers_query(user, page)
575 Repo.all(from(u in q, select: u.id))
578 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
579 def get_friends_query(%User{} = user, nil) do
580 User.Query.build(%{friends: user, deactivated: false})
583 def get_friends_query(user, page) do
584 from(u in get_friends_query(user, nil))
585 |> User.Query.paginate(page, 20)
588 @spec get_friends_query(User.t()) :: Ecto.Query.t()
589 def get_friends_query(user), do: get_friends_query(user, nil)
591 def get_friends(user, page \\ nil) do
592 q = get_friends_query(user, page)
597 def get_friends_ids(user, page \\ nil) do
598 q = get_friends_query(user, page)
600 Repo.all(from(u in q, select: u.id))
603 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
604 def get_follow_requests(%User{} = user) do
606 Activity.follow_requests_for_actor(user)
607 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
608 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
609 |> group_by([a, u], u.id)
616 def increase_note_count(%User{} = user) do
618 |> where(id: ^user.id)
623 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
630 |> Repo.update_all([])
632 {1, [user]} -> set_cache(user)
637 def decrease_note_count(%User{} = user) do
639 |> where(id: ^user.id)
644 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
651 |> Repo.update_all([])
653 {1, [user]} -> set_cache(user)
658 def update_note_count(%User{} = user) do
662 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
666 note_count = Repo.one(note_count_query)
668 info_cng = User.Info.set_note_count(user.info, note_count)
672 |> put_embed(:info, info_cng)
673 |> update_and_set_cache()
676 def update_follower_count(%User{} = user) do
677 follower_count_query =
678 User.Query.build(%{followers: user, deactivated: false})
679 |> select([u], %{count: count(u.id)})
682 |> where(id: ^user.id)
683 |> join(:inner, [u], s in subquery(follower_count_query))
688 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
695 |> Repo.update_all([])
697 {1, [user]} -> set_cache(user)
702 def remove_duplicated_following(%User{following: following} = user) do
703 uniq_following = Enum.uniq(following)
705 if length(following) == length(uniq_following) do
709 |> update_changeset(%{following: uniq_following})
710 |> update_and_set_cache()
714 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
715 def get_users_from_set(ap_ids, local_only \\ true) do
716 criteria = %{ap_id: ap_ids, deactivated: false}
717 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
719 User.Query.build(criteria)
723 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
724 def get_recipients_from_activity(%Activity{recipients: to}) do
725 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
729 def mute(muter, %User{ap_id: ap_id}) do
732 |> User.Info.add_to_mutes(ap_id)
736 |> put_embed(:info, info_cng)
738 update_and_set_cache(cng)
741 def unmute(muter, %{ap_id: ap_id}) do
744 |> User.Info.remove_from_mutes(ap_id)
748 |> put_embed(:info, info_cng)
750 update_and_set_cache(cng)
753 def subscribe(subscriber, %{ap_id: ap_id}) do
754 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
756 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
757 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
760 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
764 |> User.Info.add_to_subscribers(subscriber.ap_id)
767 |> put_embed(:info, info_cng)
768 |> update_and_set_cache()
773 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
774 with %User{} = user <- get_cached_by_ap_id(ap_id) do
777 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
780 |> put_embed(:info, info_cng)
781 |> update_and_set_cache()
785 def block(blocker, %User{ap_id: ap_id} = blocked) do
786 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
788 if following?(blocker, blocked) do
789 {:ok, blocker, _} = unfollow(blocker, blocked)
796 if subscribed_to?(blocked, blocker) do
797 {:ok, blocker} = unsubscribe(blocked, blocker)
803 if following?(blocked, blocker) do
804 unfollow(blocked, blocker)
807 {:ok, blocker} = update_follower_count(blocker)
811 |> User.Info.add_to_block(ap_id)
815 |> put_embed(:info, info_cng)
817 update_and_set_cache(cng)
820 # helper to handle the block given only an actor's AP id
821 def block(blocker, %{ap_id: ap_id}) do
822 block(blocker, get_cached_by_ap_id(ap_id))
825 def unblock(blocker, %{ap_id: ap_id}) do
828 |> User.Info.remove_from_block(ap_id)
832 |> put_embed(:info, info_cng)
834 update_and_set_cache(cng)
837 def mutes?(nil, _), do: false
838 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
840 def blocks?(user, %{ap_id: ap_id}) do
841 blocks = user.info.blocks
842 domain_blocks = user.info.domain_blocks
843 %{host: host} = URI.parse(ap_id)
845 Enum.member?(blocks, ap_id) ||
846 Enum.any?(domain_blocks, fn domain ->
851 def subscribed_to?(user, %{ap_id: ap_id}) do
852 with %User{} = target <- get_cached_by_ap_id(ap_id) do
853 Enum.member?(target.info.subscribers, user.ap_id)
857 @spec muted_users(User.t()) :: [User.t()]
858 def muted_users(user) do
859 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
863 @spec blocked_users(User.t()) :: [User.t()]
864 def blocked_users(user) do
865 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
869 @spec subscribers(User.t()) :: [User.t()]
870 def subscribers(user) do
871 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
875 def block_domain(user, domain) do
878 |> User.Info.add_to_domain_block(domain)
882 |> put_embed(:info, info_cng)
884 update_and_set_cache(cng)
887 def unblock_domain(user, domain) do
890 |> User.Info.remove_from_domain_block(domain)
894 |> put_embed(:info, info_cng)
896 update_and_set_cache(cng)
899 def deactivate_async(user, status \\ true) do
900 PleromaJobQueue.enqueue(:background, __MODULE__, [:deactivate_async, user, status])
903 def deactivate(%User{} = user, status \\ true) do
904 info_cng = User.Info.set_activation_status(user.info, status)
906 with {:ok, friends} <- User.get_friends(user),
907 {:ok, followers} <- User.get_followers(user),
911 |> put_embed(:info, info_cng)
912 |> update_and_set_cache() do
913 Enum.each(followers, &invalidate_cache(&1))
914 Enum.each(friends, &update_follower_count(&1))
920 def update_notification_settings(%User{} = user, settings \\ %{}) do
921 info_changeset = User.Info.update_notification_settings(user.info, settings)
924 |> put_embed(:info, info_changeset)
925 |> update_and_set_cache()
928 @spec delete(User.t()) :: :ok
929 def delete(%User{} = user),
930 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:delete, user])
932 @spec perform(atom(), User.t()) :: {:ok, User.t()}
933 def perform(:delete, %User{} = user) do
934 # Remove all relationships
935 {:ok, followers} = User.get_followers(user)
937 Enum.each(followers, fn follower ->
938 ActivityPub.unfollow(follower, user)
939 User.unfollow(follower, user)
942 {:ok, friends} = User.get_friends(user)
944 Enum.each(friends, fn followed ->
945 ActivityPub.unfollow(user, followed)
946 User.unfollow(user, followed)
949 delete_user_activities(user)
951 {:ok, _user} = Repo.delete(user)
954 @spec perform(atom(), User.t()) :: {:ok, User.t()}
955 def perform(:fetch_initial_posts, %User{} = user) do
956 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
959 # Insert all the posts in reverse order, so they're in the right order on the timeline
960 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
961 &Pleroma.Web.Federator.incoming_ap_doc/1
967 def perform(:deactivate_async, user, status), do: deactivate(user, status)
969 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
970 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
971 when is_list(blocked_identifiers) do
974 fn blocked_identifier ->
975 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
976 {:ok, blocker} <- block(blocker, blocked),
977 {:ok, _} <- ActivityPub.block(blocker, blocked) do
981 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
988 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
989 def perform(:follow_import, %User{} = follower, followed_identifiers)
990 when is_list(followed_identifiers) do
992 followed_identifiers,
993 fn followed_identifier ->
994 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
995 {:ok, follower} <- maybe_direct_follow(follower, followed),
996 {:ok, _} <- ActivityPub.follow(follower, followed) do
1000 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1007 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers),
1009 PleromaJobQueue.enqueue(:background, __MODULE__, [
1015 def follow_import(%User{} = follower, followed_identifiers) when is_list(followed_identifiers),
1017 PleromaJobQueue.enqueue(:background, __MODULE__, [
1020 followed_identifiers
1023 def delete_user_activities(%User{ap_id: ap_id} = user) do
1025 |> Activity.query_by_actor()
1026 |> RepoStreamer.chunk_stream(50)
1027 |> Stream.each(fn activities ->
1028 Enum.each(activities, &delete_activity(&1))
1035 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1037 |> Object.normalize()
1038 |> ActivityPub.delete()
1041 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1042 user = get_cached_by_ap_id(activity.actor)
1043 object = Object.normalize(activity)
1045 ActivityPub.unlike(user, object)
1048 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1049 user = get_cached_by_ap_id(activity.actor)
1050 object = Object.normalize(activity)
1052 ActivityPub.unannounce(user, object)
1055 defp delete_activity(_activity), do: "Doing nothing"
1057 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1058 Pleroma.HTML.Scrubber.TwitterText
1061 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1063 def fetch_by_ap_id(ap_id) do
1064 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1071 case OStatus.make_user(ap_id) do
1072 {:ok, user} -> {:ok, user}
1073 _ -> {:error, "Could not fetch by AP id"}
1078 def get_or_fetch_by_ap_id(ap_id) do
1079 user = get_cached_by_ap_id(ap_id)
1081 if !is_nil(user) and !User.needs_update?(user) do
1084 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1085 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1087 resp = fetch_by_ap_id(ap_id)
1089 if should_fetch_initial do
1090 with {:ok, %User{} = user} <- resp do
1091 fetch_initial_posts(user)
1099 def get_or_create_instance_user do
1100 relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"
1102 if user = get_cached_by_ap_id(relay_uri) do
1106 %User{info: %User.Info{}}
1107 |> cast(%{}, [:ap_id, :nickname, :local])
1108 |> put_change(:ap_id, relay_uri)
1109 |> put_change(:nickname, nil)
1110 |> put_change(:local, true)
1111 |> put_change(:follower_address, relay_uri <> "/followers")
1113 {:ok, user} = Repo.insert(changes)
1119 def public_key_from_info(%{
1120 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1124 |> :public_key.pem_decode()
1126 |> :public_key.pem_entry_decode()
1132 def public_key_from_info(%{magic_key: magic_key}) do
1133 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1136 def get_public_key_for_ap_id(ap_id) do
1137 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1138 {:ok, public_key} <- public_key_from_info(user.info) do
1145 defp blank?(""), do: nil
1146 defp blank?(n), do: n
1148 def insert_or_update_user(data) do
1150 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1151 |> remote_user_creation()
1152 |> Repo.insert(on_conflict: :replace_all, conflict_target: :nickname)
1156 def ap_enabled?(%User{local: true}), do: true
1157 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1158 def ap_enabled?(_), do: false
1160 @doc "Gets or fetch a user by uri or nickname."
1161 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1162 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1163 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1165 # wait a period of time and return newest version of the User structs
1166 # this is because we have synchronous follow APIs and need to simulate them
1167 # with an async handshake
1168 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1169 with %User{} = a <- User.get_cached_by_id(a.id),
1170 %User{} = b <- User.get_cached_by_id(b.id) do
1178 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1179 with :ok <- :timer.sleep(timeout),
1180 %User{} = a <- User.get_cached_by_id(a.id),
1181 %User{} = b <- User.get_cached_by_id(b.id) do
1189 def parse_bio(bio) when is_binary(bio) and bio != "" do
1191 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1195 def parse_bio(_), do: ""
1197 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1198 # TODO: get profile URLs other than user.ap_id
1199 profile_urls = [user.ap_id]
1202 |> CommonUtils.format_input("text/plain",
1203 mentions_format: :full,
1204 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1209 def parse_bio(_, _), do: ""
1211 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1212 Repo.transaction(fn ->
1213 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1217 def tag(nickname, tags) when is_binary(nickname),
1218 do: tag(get_by_nickname(nickname), tags)
1220 def tag(%User{} = user, tags),
1221 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1223 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1224 Repo.transaction(fn ->
1225 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1229 def untag(nickname, tags) when is_binary(nickname),
1230 do: untag(get_by_nickname(nickname), tags)
1232 def untag(%User{} = user, tags),
1233 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1235 defp update_tags(%User{} = user, new_tags) do
1236 {:ok, updated_user} =
1238 |> change(%{tags: new_tags})
1239 |> update_and_set_cache()
1244 defp normalize_tags(tags) do
1247 |> Enum.map(&String.downcase(&1))
1250 defp local_nickname_regex do
1251 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1252 @extended_local_nickname_regex
1254 @strict_local_nickname_regex
1258 def local_nickname(nickname_or_mention) do
1261 |> String.split("@")
1265 def full_nickname(nickname_or_mention),
1266 do: String.trim_leading(nickname_or_mention, "@")
1268 def error_user(ap_id) do
1273 nickname: "erroruser@example.com",
1274 inserted_at: NaiveDateTime.utc_now()
1278 @spec all_superusers() :: [User.t()]
1279 def all_superusers do
1280 User.Query.build(%{super_users: true, local: true, deactivated: false})
1284 def showing_reblogs?(%User{} = user, %User{} = target) do
1285 target.ap_id not in user.info.muted_reblogs
1289 The function returns a query to get users with no activity for given interval of days.
1290 Inactive users are those who didn't read any notification, or had any activity where
1291 the user is the activity's actor, during `inactivity_threshold` days.
1292 Deactivated users will not appear in this list.
1296 iex> Pleroma.User.list_inactive_users()
1299 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1300 def list_inactive_users_query(inactivity_threshold \\ 7) do
1301 negative_inactivity_threshold = -inactivity_threshold
1302 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1303 # Subqueries are not supported in `where` clauses, join gets too complicated.
1304 has_read_notifications =
1305 from(n in Pleroma.Notification,
1306 where: n.seen == true,
1308 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1311 |> Pleroma.Repo.all()
1313 from(u in Pleroma.User,
1314 left_join: a in Pleroma.Activity,
1315 on: u.ap_id == a.actor,
1316 where: not is_nil(u.nickname),
1317 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1318 where: u.id not in ^has_read_notifications,
1321 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1322 is_nil(max(a.inserted_at))
1327 Enable or disable email notifications for user
1331 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1332 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1334 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1335 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1337 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1338 {:ok, t()} | {:error, Ecto.Changeset.t()}
1339 def switch_email_notifications(user, type, status) do
1340 info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})
1343 |> put_embed(:info, info)
1344 |> update_and_set_cache()
1348 Set `last_digest_emailed_at` value for the user to current time
1350 @spec touch_last_digest_emailed_at(t()) :: t()
1351 def touch_last_digest_emailed_at(user) do
1352 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1354 {:ok, updated_user} =
1356 |> change(%{last_digest_emailed_at: now})
1357 |> update_and_set_cache()
1362 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1363 def toggle_confirmation(%User{} = user) do
1364 need_confirmation? = !user.info.confirmation_pending
1367 User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
1371 |> put_embed(:info, info_changeset)
1372 |> update_and_set_cache()
1375 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1379 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1380 # use instance-default
1381 config = Pleroma.Config.get([:assets, :mascots])
1382 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1383 mascot = Keyword.get(config, default_mascot)
1386 "id" => "default-mascot",
1387 "url" => mascot[:url],
1388 "preview_url" => mascot[:url],
1390 "mime_type" => mascot[:mime_type]
1395 def ensure_keys_present(user) do
1401 {:ok, pem} = Keys.generate_rsa_pem()
1405 |> User.Info.set_keys(pem)
1408 Ecto.Changeset.change(user)
1409 |> Ecto.Changeset.put_embed(:info, info_cng)
1411 update_and_set_cache(cng)
1415 def get_ap_ids_by_nicknames(nicknames) do
1417 where: u.nickname in ^nicknames,
1423 defdelegate search(query, opts \\ []), to: User.Search
1425 defp put_password_hash(
1426 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1428 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1431 defp put_password_hash(changeset), do: changeset