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
12 alias Pleroma.Activity
14 alias Pleroma.Notification
16 alias Pleroma.Registration
20 alias Pleroma.Web.ActivityPub.ActivityPub
21 alias Pleroma.Web.ActivityPub.Utils
22 alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
23 alias Pleroma.Web.OAuth
24 alias Pleroma.Web.OStatus
25 alias Pleroma.Web.RelMe
26 alias Pleroma.Web.Websub
30 @type t :: %__MODULE__{}
32 @primary_key {:id, Pleroma.FlakeId, autogenerate: true}
34 # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
35 @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])?)*$/
37 @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
38 @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
42 field(:email, :string)
44 field(:nickname, :string)
45 field(:password_hash, :string)
46 field(:password, :string, virtual: true)
47 field(:password_confirmation, :string, virtual: true)
48 field(:following, {:array, :string}, default: [])
49 field(:ap_id, :string)
51 field(:local, :boolean, default: true)
52 field(:follower_address, :string)
53 field(:search_rank, :float, virtual: true)
54 field(:search_type, :integer, virtual: true)
55 field(:tags, {:array, :string}, default: [])
56 field(:last_refreshed_at, :naive_datetime_usec)
57 field(:last_digest_emailed_at, :naive_datetime)
58 has_many(:notifications, Notification)
59 has_many(:registrations, Registration)
60 embeds_one(:info, User.Info)
65 def auth_active?(%User{info: %User.Info{confirmation_pending: true}}),
66 do: !Pleroma.Config.get([:instance, :account_activation_required])
68 def auth_active?(%User{}), do: true
70 def visible_for?(user, for_user \\ nil)
72 def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
74 def visible_for?(%User{} = user, for_user) do
75 auth_active?(user) || superuser?(for_user)
78 def visible_for?(_, _), do: false
80 def superuser?(%User{local: true, info: %User.Info{is_admin: true}}), do: true
81 def superuser?(%User{local: true, info: %User.Info{is_moderator: true}}), do: true
82 def superuser?(_), do: false
84 def avatar_url(user, options \\ []) do
86 %{"url" => [%{"href" => href} | _]} -> href
87 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
91 def banner_url(user, options \\ []) do
92 case user.info.banner do
93 %{"url" => [%{"href" => href} | _]} -> href
94 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
98 def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url
99 def profile_url(%User{ap_id: ap_id}), do: ap_id
100 def profile_url(_), do: nil
102 def ap_id(%User{nickname: nickname}) do
103 "#{Web.base_url()}/users/#{nickname}"
106 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
107 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
109 def user_info(%User{} = user) do
111 following_count: following_count(user),
112 note_count: user.info.note_count,
113 follower_count: user.info.follower_count,
114 locked: user.info.locked,
115 confirmation_pending: user.info.confirmation_pending,
116 default_scope: user.info.default_scope
120 def restrict_deactivated(query) do
122 where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info)
126 def following_count(%User{following: []}), do: 0
128 def following_count(%User{} = user) do
130 |> get_friends_query()
131 |> Repo.aggregate(:count, :id)
134 def remote_user_creation(params) do
137 |> Map.put(:info, params[:info] || %{})
139 info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])
143 |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
144 |> validate_required([:name, :ap_id])
145 |> unique_constraint(:nickname)
146 |> validate_format(:nickname, @email_regex)
147 |> validate_length(:bio, max: 5000)
148 |> validate_length(:name, max: 100)
149 |> put_change(:local, false)
150 |> put_embed(:info, info_cng)
153 case info_cng.changes[:source_data] do
154 %{"followers" => followers} ->
156 |> put_change(:follower_address, followers)
159 followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
162 |> put_change(:follower_address, followers)
169 def update_changeset(struct, params \\ %{}) do
171 |> cast(params, [:bio, :name, :avatar, :following])
172 |> unique_constraint(:nickname)
173 |> validate_format(:nickname, local_nickname_regex())
174 |> validate_length(:bio, max: 5000)
175 |> validate_length(:name, min: 1, max: 100)
178 def upgrade_changeset(struct, params \\ %{}) do
181 |> Map.put(:last_refreshed_at, NaiveDateTime.utc_now())
185 |> User.Info.user_upgrade(params[:info])
188 |> cast(params, [:bio, :name, :follower_address, :avatar, :last_refreshed_at])
189 |> unique_constraint(:nickname)
190 |> validate_format(:nickname, local_nickname_regex())
191 |> validate_length(:bio, max: 5000)
192 |> validate_length(:name, max: 100)
193 |> put_embed(:info, info_cng)
196 def password_update_changeset(struct, params) do
199 |> cast(params, [:password, :password_confirmation])
200 |> validate_required([:password, :password_confirmation])
201 |> validate_confirmation(:password)
203 OAuth.Token.delete_user_tokens(struct)
204 OAuth.Authorization.delete_user_authorizations(struct)
206 if changeset.valid? do
207 hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
210 |> put_change(:password_hash, hashed)
216 def reset_password(user, data) do
217 update_and_set_cache(password_update_changeset(user, data))
220 def register_changeset(struct, params \\ %{}, opts \\ []) do
222 if is_nil(opts[:need_confirmation]) do
223 Pleroma.Config.get([:instance, :account_activation_required])
225 opts[:need_confirmation]
229 User.Info.confirmation_changeset(%User.Info{}, need_confirmation: need_confirmation?)
233 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
234 |> validate_required([:name, :nickname, :password, :password_confirmation])
235 |> validate_confirmation(:password)
236 |> unique_constraint(:email)
237 |> unique_constraint(:nickname)
238 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
239 |> validate_format(:nickname, local_nickname_regex())
240 |> validate_format(:email, @email_regex)
241 |> validate_length(:bio, max: 1000)
242 |> validate_length(:name, min: 1, max: 100)
243 |> put_change(:info, info_change)
246 if opts[:external] do
249 validate_required(changeset, [:email])
252 if changeset.valid? do
253 hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
254 ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
255 followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
258 |> put_change(:password_hash, hashed)
259 |> put_change(:ap_id, ap_id)
260 |> unique_constraint(:ap_id)
261 |> put_change(:following, [followers])
262 |> put_change(:follower_address, followers)
268 defp autofollow_users(user) do
269 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
272 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
275 follow_all(user, autofollowed_users)
278 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
279 def register(%Ecto.Changeset{} = changeset) do
280 with {:ok, user} <- Repo.insert(changeset),
281 {:ok, user} <- autofollow_users(user),
282 {:ok, user} <- set_cache(user),
283 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
284 {:ok, _} <- try_send_confirmation_email(user) do
289 def try_send_confirmation_email(%User{} = user) do
290 if user.info.confirmation_pending &&
291 Pleroma.Config.get([:instance, :account_activation_required]) do
293 |> Pleroma.Emails.UserEmail.account_confirmation_email()
294 |> Pleroma.Emails.Mailer.deliver_async()
302 def needs_update?(%User{local: true}), do: false
304 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
306 def needs_update?(%User{local: false} = user) do
307 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
310 def needs_update?(_), do: true
312 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
316 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
317 follow(follower, followed)
320 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
321 if not User.ap_enabled?(followed) do
322 follow(follower, followed)
328 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
329 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
330 def follow_all(follower, followeds) do
333 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
334 |> Enum.map(fn %{follower_address: fa} -> fa end)
338 where: u.id == ^follower.id,
343 "array(select distinct unnest (array_cat(?, ?)))",
352 {1, [follower]} = Repo.update_all(q, [])
354 Enum.each(followeds, fn followed ->
355 update_follower_count(followed)
361 def follow(%User{} = follower, %User{info: info} = followed) do
362 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
363 ap_followers = followed.follower_address
367 {:error, "Could not follow user: You are deactivated."}
369 deny_follow_blocked and blocks?(followed, follower) ->
370 {:error, "Could not follow user: #{followed.nickname} blocked you."}
373 if !followed.local && follower.local && !ap_enabled?(followed) do
374 Websub.subscribe(follower, followed)
379 where: u.id == ^follower.id,
380 update: [push: [following: ^ap_followers]],
384 {1, [follower]} = Repo.update_all(q, [])
386 {:ok, _} = update_follower_count(followed)
392 def unfollow(%User{} = follower, %User{} = followed) do
393 ap_followers = followed.follower_address
395 if following?(follower, followed) and follower.ap_id != followed.ap_id do
398 where: u.id == ^follower.id,
399 update: [pull: [following: ^ap_followers]],
403 {1, [follower]} = Repo.update_all(q, [])
405 {:ok, followed} = update_follower_count(followed)
409 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
411 {:error, "Not subscribed!"}
415 @spec following?(User.t(), User.t()) :: boolean
416 def following?(%User{} = follower, %User{} = followed) do
417 Enum.member?(follower.following, followed.follower_address)
420 def locked?(%User{} = user) do
421 user.info.locked || false
425 Repo.get_by(User, id: id)
428 def get_by_ap_id(ap_id) do
429 Repo.get_by(User, ap_id: ap_id)
432 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
433 # of the ap_id and the domain and tries to get that user
434 def get_by_guessed_nickname(ap_id) do
435 domain = URI.parse(ap_id).host
436 name = List.last(String.split(ap_id, "/"))
437 nickname = "#{name}@#{domain}"
439 get_cached_by_nickname(nickname)
442 def set_cache({:ok, user}), do: set_cache(user)
443 def set_cache({:error, err}), do: {:error, err}
445 def set_cache(%User{} = user) do
446 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
447 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
448 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
452 def update_and_set_cache(changeset) do
453 with {:ok, user} <- Repo.update(changeset) do
460 def invalidate_cache(user) do
461 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
462 Cachex.del(:user_cache, "nickname:#{user.nickname}")
463 Cachex.del(:user_cache, "user_info:#{user.id}")
466 def get_cached_by_ap_id(ap_id) do
467 key = "ap_id:#{ap_id}"
468 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
471 def get_cached_by_id(id) do
475 Cachex.fetch!(:user_cache, key, fn _ ->
479 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
480 {:commit, user.ap_id}
486 get_cached_by_ap_id(ap_id)
489 def get_cached_by_nickname(nickname) do
490 key = "nickname:#{nickname}"
492 Cachex.fetch!(:user_cache, key, fn ->
493 user_result = get_or_fetch_by_nickname(nickname)
496 {:ok, user} -> {:commit, user}
497 {:error, _error} -> {:ignore, nil}
502 def get_cached_by_nickname_or_id(nickname_or_id) do
503 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
506 def get_by_nickname(nickname) do
507 Repo.get_by(User, nickname: nickname) ||
508 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
509 Repo.get_by(User, nickname: local_nickname(nickname))
513 def get_by_email(email), do: Repo.get_by(User, email: email)
515 def get_by_nickname_or_email(nickname_or_email) do
516 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
519 def get_cached_user_info(user) do
520 key = "user_info:#{user.id}"
521 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
524 def fetch_by_nickname(nickname) do
525 ap_try = ActivityPub.make_user_from_nickname(nickname)
528 {:ok, user} -> {:ok, user}
529 _ -> OStatus.make_user(nickname)
533 def get_or_fetch_by_nickname(nickname) do
534 with %User{} = user <- get_by_nickname(nickname) do
538 with [_nick, _domain] <- String.split(nickname, "@"),
539 {:ok, user} <- fetch_by_nickname(nickname) do
540 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
541 fetch_initial_posts(user)
546 _e -> {:error, "not found " <> nickname}
551 @doc "Fetch some posts when the user has just been federated with"
552 def fetch_initial_posts(user),
553 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:fetch_initial_posts, user])
555 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
556 def get_followers_query(%User{} = user, nil) do
557 User.Query.build(%{followers: user, deactivated: false})
560 def get_followers_query(user, page) do
561 from(u in get_followers_query(user, nil))
562 |> User.Query.paginate(page, 20)
565 @spec get_followers_query(User.t()) :: Ecto.Query.t()
566 def get_followers_query(user), do: get_followers_query(user, nil)
568 def get_followers(user, page \\ nil) do
569 q = get_followers_query(user, page)
574 def get_followers_ids(user, page \\ nil) do
575 q = get_followers_query(user, page)
577 Repo.all(from(u in q, select: u.id))
580 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
581 def get_friends_query(%User{} = user, nil) do
582 User.Query.build(%{friends: user, deactivated: false})
585 def get_friends_query(user, page) do
586 from(u in get_friends_query(user, nil))
587 |> User.Query.paginate(page, 20)
590 @spec get_friends_query(User.t()) :: Ecto.Query.t()
591 def get_friends_query(user), do: get_friends_query(user, nil)
593 def get_friends(user, page \\ nil) do
594 q = get_friends_query(user, page)
599 def get_friends_ids(user, page \\ nil) do
600 q = get_friends_query(user, page)
602 Repo.all(from(u in q, select: u.id))
605 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
606 def get_follow_requests(%User{} = user) do
608 Activity.follow_requests_for_actor(user)
609 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
610 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
611 |> group_by([a, u], u.id)
618 def increase_note_count(%User{} = user) do
620 |> where(id: ^user.id)
625 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
632 |> Repo.update_all([])
634 {1, [user]} -> set_cache(user)
639 def decrease_note_count(%User{} = user) do
641 |> where(id: ^user.id)
646 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
653 |> Repo.update_all([])
655 {1, [user]} -> set_cache(user)
660 def update_note_count(%User{} = user) do
664 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
668 note_count = Repo.one(note_count_query)
670 info_cng = User.Info.set_note_count(user.info, note_count)
674 |> put_embed(:info, info_cng)
675 |> update_and_set_cache()
678 def update_follower_count(%User{} = user) do
679 follower_count_query =
680 User.Query.build(%{followers: user, deactivated: false})
681 |> select([u], %{count: count(u.id)})
684 |> where(id: ^user.id)
685 |> join(:inner, [u], s in subquery(follower_count_query))
690 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
697 |> Repo.update_all([])
699 {1, [user]} -> set_cache(user)
704 def remove_duplicated_following(%User{following: following} = user) do
705 uniq_following = Enum.uniq(following)
707 if length(following) == length(uniq_following) do
711 |> update_changeset(%{following: uniq_following})
712 |> update_and_set_cache()
716 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
717 def get_users_from_set(ap_ids, local_only \\ true) do
718 criteria = %{ap_id: ap_ids, deactivated: false}
719 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
721 User.Query.build(criteria)
725 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
726 def get_recipients_from_activity(%Activity{recipients: to}) do
727 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
731 def mute(muter, %User{ap_id: ap_id}) do
734 |> User.Info.add_to_mutes(ap_id)
738 |> put_embed(:info, info_cng)
740 update_and_set_cache(cng)
743 def unmute(muter, %{ap_id: ap_id}) do
746 |> User.Info.remove_from_mutes(ap_id)
750 |> put_embed(:info, info_cng)
752 update_and_set_cache(cng)
755 def subscribe(subscriber, %{ap_id: ap_id}) do
756 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
758 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
759 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
762 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
766 |> User.Info.add_to_subscribers(subscriber.ap_id)
769 |> put_embed(:info, info_cng)
770 |> update_and_set_cache()
775 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
776 with %User{} = user <- get_cached_by_ap_id(ap_id) do
779 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
782 |> put_embed(:info, info_cng)
783 |> update_and_set_cache()
787 def block(blocker, %User{ap_id: ap_id} = blocked) do
788 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
790 if following?(blocker, blocked) do
791 {:ok, blocker, _} = unfollow(blocker, blocked)
798 if subscribed_to?(blocked, blocker) do
799 {:ok, blocker} = unsubscribe(blocked, blocker)
805 if following?(blocked, blocker) do
806 unfollow(blocked, blocker)
809 {:ok, blocker} = update_follower_count(blocker)
813 |> User.Info.add_to_block(ap_id)
817 |> put_embed(:info, info_cng)
819 update_and_set_cache(cng)
822 # helper to handle the block given only an actor's AP id
823 def block(blocker, %{ap_id: ap_id}) do
824 block(blocker, get_cached_by_ap_id(ap_id))
827 def unblock(blocker, %{ap_id: ap_id}) do
830 |> User.Info.remove_from_block(ap_id)
834 |> put_embed(:info, info_cng)
836 update_and_set_cache(cng)
839 def mutes?(nil, _), do: false
840 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
842 def blocks?(user, %{ap_id: ap_id}) do
843 blocks = user.info.blocks
844 domain_blocks = user.info.domain_blocks
845 %{host: host} = URI.parse(ap_id)
847 Enum.member?(blocks, ap_id) ||
848 Enum.any?(domain_blocks, fn domain ->
853 def subscribed_to?(user, %{ap_id: ap_id}) do
854 with %User{} = target <- get_cached_by_ap_id(ap_id) do
855 Enum.member?(target.info.subscribers, user.ap_id)
859 @spec muted_users(User.t()) :: [User.t()]
860 def muted_users(user) do
861 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
865 @spec blocked_users(User.t()) :: [User.t()]
866 def blocked_users(user) do
867 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
871 @spec subscribers(User.t()) :: [User.t()]
872 def subscribers(user) do
873 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
877 def block_domain(user, domain) do
880 |> User.Info.add_to_domain_block(domain)
884 |> put_embed(:info, info_cng)
886 update_and_set_cache(cng)
889 def unblock_domain(user, domain) do
892 |> User.Info.remove_from_domain_block(domain)
896 |> put_embed(:info, info_cng)
898 update_and_set_cache(cng)
901 def deactivate_async(user, status \\ true) do
902 PleromaJobQueue.enqueue(:background, __MODULE__, [:deactivate_async, user, status])
905 def deactivate(%User{} = user, status \\ true) do
906 info_cng = User.Info.set_activation_status(user.info, status)
908 with {:ok, friends} <- User.get_friends(user),
909 {:ok, followers} <- User.get_followers(user),
913 |> put_embed(:info, info_cng)
914 |> update_and_set_cache() do
915 Enum.each(followers, &invalidate_cache(&1))
916 Enum.each(friends, &update_follower_count(&1))
922 def update_notification_settings(%User{} = user, settings \\ %{}) do
923 info_changeset = User.Info.update_notification_settings(user.info, settings)
926 |> put_embed(:info, info_changeset)
927 |> update_and_set_cache()
930 @spec delete(User.t()) :: :ok
931 def delete(%User{} = user),
932 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:delete, user])
934 @spec perform(atom(), User.t()) :: {:ok, User.t()}
935 def perform(:delete, %User{} = user) do
936 {:ok, user} = User.deactivate(user)
938 # Remove all relationships
939 {:ok, followers} = User.get_followers(user)
941 Enum.each(followers, fn follower -> User.unfollow(follower, user) end)
943 {:ok, friends} = User.get_friends(user)
945 Enum.each(friends, fn followed -> User.unfollow(user, followed) end)
947 delete_user_activities(user)
950 @spec perform(atom(), User.t()) :: {:ok, User.t()}
951 def perform(:fetch_initial_posts, %User{} = user) do
952 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
955 # Insert all the posts in reverse order, so they're in the right order on the timeline
956 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
957 &Pleroma.Web.Federator.incoming_ap_doc/1
963 def perform(:deactivate_async, user, status), do: deactivate(user, status)
965 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
966 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
967 when is_list(blocked_identifiers) do
970 fn blocked_identifier ->
971 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
972 {:ok, blocker} <- block(blocker, blocked),
973 {:ok, _} <- ActivityPub.block(blocker, blocked) do
977 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
984 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
985 def perform(:follow_import, %User{} = follower, followed_identifiers)
986 when is_list(followed_identifiers) do
988 followed_identifiers,
989 fn followed_identifier ->
990 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
991 {:ok, follower} <- maybe_direct_follow(follower, followed),
992 {:ok, _} <- ActivityPub.follow(follower, followed) do
996 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1003 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers),
1005 PleromaJobQueue.enqueue(:background, __MODULE__, [
1011 def follow_import(%User{} = follower, followed_identifiers) when is_list(followed_identifiers),
1013 PleromaJobQueue.enqueue(:background, __MODULE__, [
1016 followed_identifiers
1019 def delete_user_activities(%User{ap_id: ap_id} = user) do
1022 |> Activity.query_by_actor()
1025 Repo.transaction(fn -> Enum.each(stream, &delete_activity(&1)) end, timeout: :infinity)
1030 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1031 Object.normalize(activity) |> ActivityPub.delete()
1034 defp delete_activity(_activity), do: "Doing nothing"
1036 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1037 Pleroma.HTML.Scrubber.TwitterText
1040 @default_scrubbers Pleroma.Config.get([:markup, :scrub_policy])
1042 def html_filter_policy(_), do: @default_scrubbers
1044 def fetch_by_ap_id(ap_id) do
1045 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1052 case OStatus.make_user(ap_id) do
1053 {:ok, user} -> {:ok, user}
1054 _ -> {:error, "Could not fetch by AP id"}
1059 def get_or_fetch_by_ap_id(ap_id) do
1060 user = get_cached_by_ap_id(ap_id)
1062 if !is_nil(user) and !User.needs_update?(user) do
1065 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1066 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1068 resp = fetch_by_ap_id(ap_id)
1070 if should_fetch_initial do
1071 with {:ok, %User{} = user} <- resp do
1072 fetch_initial_posts(user)
1080 def get_or_create_instance_user do
1081 relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"
1083 if user = get_cached_by_ap_id(relay_uri) do
1087 %User{info: %User.Info{}}
1088 |> cast(%{}, [:ap_id, :nickname, :local])
1089 |> put_change(:ap_id, relay_uri)
1090 |> put_change(:nickname, nil)
1091 |> put_change(:local, true)
1092 |> put_change(:follower_address, relay_uri <> "/followers")
1094 {:ok, user} = Repo.insert(changes)
1100 def public_key_from_info(%{
1101 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1105 |> :public_key.pem_decode()
1107 |> :public_key.pem_entry_decode()
1113 def public_key_from_info(%{magic_key: magic_key}) do
1114 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1117 def get_public_key_for_ap_id(ap_id) do
1118 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1119 {:ok, public_key} <- public_key_from_info(user.info) do
1126 defp blank?(""), do: nil
1127 defp blank?(n), do: n
1129 def insert_or_update_user(data) do
1131 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1132 |> remote_user_creation()
1133 |> Repo.insert(on_conflict: :replace_all, conflict_target: :nickname)
1137 def ap_enabled?(%User{local: true}), do: true
1138 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1139 def ap_enabled?(_), do: false
1141 @doc "Gets or fetch a user by uri or nickname."
1142 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1143 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1144 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1146 # wait a period of time and return newest version of the User structs
1147 # this is because we have synchronous follow APIs and need to simulate them
1148 # with an async handshake
1149 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1150 with %User{} = a <- User.get_cached_by_id(a.id),
1151 %User{} = b <- User.get_cached_by_id(b.id) do
1159 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1160 with :ok <- :timer.sleep(timeout),
1161 %User{} = a <- User.get_cached_by_id(a.id),
1162 %User{} = b <- User.get_cached_by_id(b.id) do
1170 def parse_bio(bio) when is_binary(bio) and bio != "" do
1172 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1176 def parse_bio(_), do: ""
1178 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1179 # TODO: get profile URLs other than user.ap_id
1180 profile_urls = [user.ap_id]
1183 |> CommonUtils.format_input("text/plain",
1184 mentions_format: :full,
1185 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1190 def parse_bio(_, _), do: ""
1192 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1193 Repo.transaction(fn ->
1194 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1198 def tag(nickname, tags) when is_binary(nickname),
1199 do: tag(get_by_nickname(nickname), tags)
1201 def tag(%User{} = user, tags),
1202 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1204 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1205 Repo.transaction(fn ->
1206 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1210 def untag(nickname, tags) when is_binary(nickname),
1211 do: untag(get_by_nickname(nickname), tags)
1213 def untag(%User{} = user, tags),
1214 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1216 defp update_tags(%User{} = user, new_tags) do
1217 {:ok, updated_user} =
1219 |> change(%{tags: new_tags})
1220 |> update_and_set_cache()
1225 defp normalize_tags(tags) do
1228 |> Enum.map(&String.downcase(&1))
1231 defp local_nickname_regex do
1232 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1233 @extended_local_nickname_regex
1235 @strict_local_nickname_regex
1239 def local_nickname(nickname_or_mention) do
1242 |> String.split("@")
1246 def full_nickname(nickname_or_mention),
1247 do: String.trim_leading(nickname_or_mention, "@")
1249 def error_user(ap_id) do
1254 nickname: "erroruser@example.com",
1255 inserted_at: NaiveDateTime.utc_now()
1259 @spec all_superusers() :: [User.t()]
1260 def all_superusers do
1261 User.Query.build(%{super_users: true, local: true, deactivated: false})
1265 def showing_reblogs?(%User{} = user, %User{} = target) do
1266 target.ap_id not in user.info.muted_reblogs
1270 The function returns a query to get users with no activity for given interval of days.
1271 Inactive users are those who didn't read any notification, or had any activity where
1272 the user is the activity's actor, during `inactivity_threshold` days.
1273 Deactivated users will not appear in this list.
1277 iex> Pleroma.User.list_inactive_users()
1280 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1281 def list_inactive_users_query(inactivity_threshold \\ 7) do
1282 negative_inactivity_threshold = -inactivity_threshold
1283 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1284 # Subqueries are not supported in `where` clauses, join gets too complicated.
1285 has_read_notifications =
1286 from(n in Pleroma.Notification,
1287 where: n.seen == true,
1289 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1292 |> Pleroma.Repo.all()
1294 from(u in Pleroma.User,
1295 left_join: a in Pleroma.Activity,
1296 on: u.ap_id == a.actor,
1297 where: not is_nil(u.nickname),
1298 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1299 where: u.id not in ^has_read_notifications,
1302 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1303 is_nil(max(a.inserted_at))
1308 Enable or disable email notifications for user
1312 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1313 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1315 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1316 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1318 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1319 {:ok, t()} | {:error, Ecto.Changeset.t()}
1320 def switch_email_notifications(user, type, status) do
1321 info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})
1324 |> put_embed(:info, info)
1325 |> update_and_set_cache()
1329 Set `last_digest_emailed_at` value for the user to current time
1331 @spec touch_last_digest_emailed_at(t()) :: t()
1332 def touch_last_digest_emailed_at(user) do
1333 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1335 {:ok, updated_user} =
1337 |> change(%{last_digest_emailed_at: now})
1338 |> update_and_set_cache()
1343 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1344 def toggle_confirmation(%User{} = user) do
1345 need_confirmation? = !user.info.confirmation_pending
1348 User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
1352 |> put_embed(:info, info_changeset)
1353 |> update_and_set_cache()
1356 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1360 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1361 # use instance-default
1362 config = Pleroma.Config.get([:assets, :mascots])
1363 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1364 mascot = Keyword.get(config, default_mascot)
1367 "id" => "default-mascot",
1368 "url" => mascot[:url],
1369 "preview_url" => mascot[:url],
1371 "mime_type" => mascot[:mime_type]
1376 def ensure_keys_present(user) do
1382 {:ok, pem} = Keys.generate_rsa_pem()
1386 |> User.Info.set_keys(pem)
1389 Ecto.Changeset.change(user)
1390 |> Ecto.Changeset.put_embed(:info, info_cng)
1392 update_and_set_cache(cng)
1396 def get_ap_ids_by_nicknames(nicknames) do
1398 where: u.nickname in ^nicknames,
1404 defdelegate search(query, opts \\ []), to: User.Search