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 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
754 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
758 User.Info.add_to_mutes(info, ap_id)
759 |> User.Info.add_to_muted_notifications(info, ap_id, notifications?)
763 |> put_embed(:info, info_cng)
765 update_and_set_cache(cng)
768 def unmute(muter, %{ap_id: ap_id}) do
772 User.Info.remove_from_mutes(info, ap_id)
773 |> User.Info.remove_from_muted_notifications(info, ap_id)
777 |> put_embed(:info, info_cng)
779 update_and_set_cache(cng)
782 def subscribe(subscriber, %{ap_id: ap_id}) do
783 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
785 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
786 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
789 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
793 |> User.Info.add_to_subscribers(subscriber.ap_id)
796 |> put_embed(:info, info_cng)
797 |> update_and_set_cache()
802 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
803 with %User{} = user <- get_cached_by_ap_id(ap_id) do
806 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
809 |> put_embed(:info, info_cng)
810 |> update_and_set_cache()
814 def block(blocker, %User{ap_id: ap_id} = blocked) do
815 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
817 if following?(blocker, blocked) do
818 {:ok, blocker, _} = unfollow(blocker, blocked)
825 if subscribed_to?(blocked, blocker) do
826 {:ok, blocker} = unsubscribe(blocked, blocker)
832 if following?(blocked, blocker) do
833 unfollow(blocked, blocker)
836 {:ok, blocker} = update_follower_count(blocker)
840 |> User.Info.add_to_block(ap_id)
844 |> put_embed(:info, info_cng)
846 update_and_set_cache(cng)
849 # helper to handle the block given only an actor's AP id
850 def block(blocker, %{ap_id: ap_id}) do
851 block(blocker, get_cached_by_ap_id(ap_id))
854 def unblock(blocker, %{ap_id: ap_id}) do
857 |> User.Info.remove_from_block(ap_id)
861 |> put_embed(:info, info_cng)
863 update_and_set_cache(cng)
866 def mutes?(nil, _), do: false
867 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
869 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
870 def muted_notifications?(nil, _), do: false
872 def muted_notifications?(user, %{ap_id: ap_id}),
873 do: Enum.member?(user.info.muted_notifications, ap_id)
875 def blocks?(%User{info: info} = _user, %{ap_id: ap_id}) do
877 domain_blocks = info.domain_blocks
878 %{host: host} = URI.parse(ap_id)
880 Enum.member?(blocks, ap_id) || Enum.any?(domain_blocks, &(&1 == host))
883 def subscribed_to?(user, %{ap_id: ap_id}) do
884 with %User{} = target <- get_cached_by_ap_id(ap_id) do
885 Enum.member?(target.info.subscribers, user.ap_id)
889 @spec muted_users(User.t()) :: [User.t()]
890 def muted_users(user) do
891 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
895 @spec blocked_users(User.t()) :: [User.t()]
896 def blocked_users(user) do
897 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
901 @spec subscribers(User.t()) :: [User.t()]
902 def subscribers(user) do
903 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
907 def block_domain(user, domain) do
910 |> User.Info.add_to_domain_block(domain)
914 |> put_embed(:info, info_cng)
916 update_and_set_cache(cng)
919 def unblock_domain(user, domain) do
922 |> User.Info.remove_from_domain_block(domain)
926 |> put_embed(:info, info_cng)
928 update_and_set_cache(cng)
931 def deactivate_async(user, status \\ true) do
932 PleromaJobQueue.enqueue(:background, __MODULE__, [:deactivate_async, user, status])
935 def deactivate(%User{} = user, status \\ true) do
936 info_cng = User.Info.set_activation_status(user.info, status)
938 with {:ok, friends} <- User.get_friends(user),
939 {:ok, followers} <- User.get_followers(user),
943 |> put_embed(:info, info_cng)
944 |> update_and_set_cache() do
945 Enum.each(followers, &invalidate_cache(&1))
946 Enum.each(friends, &update_follower_count(&1))
952 def update_notification_settings(%User{} = user, settings \\ %{}) do
953 info_changeset = User.Info.update_notification_settings(user.info, settings)
956 |> put_embed(:info, info_changeset)
957 |> update_and_set_cache()
960 @spec delete(User.t()) :: :ok
961 def delete(%User{} = user),
962 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:delete, user])
964 @spec perform(atom(), User.t()) :: {:ok, User.t()}
965 def perform(:delete, %User{} = user) do
966 {:ok, _user} = ActivityPub.delete(user)
968 # Remove all relationships
969 {:ok, followers} = User.get_followers(user)
971 Enum.each(followers, fn follower ->
972 ActivityPub.unfollow(follower, user)
973 User.unfollow(follower, user)
976 {:ok, friends} = User.get_friends(user)
978 Enum.each(friends, fn followed ->
979 ActivityPub.unfollow(user, followed)
980 User.unfollow(user, followed)
983 delete_user_activities(user)
984 invalidate_cache(user)
988 @spec perform(atom(), User.t()) :: {:ok, User.t()}
989 def perform(:fetch_initial_posts, %User{} = user) do
990 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
993 # Insert all the posts in reverse order, so they're in the right order on the timeline
994 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
995 &Pleroma.Web.Federator.incoming_ap_doc/1
1001 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1003 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1004 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1005 when is_list(blocked_identifiers) do
1007 blocked_identifiers,
1008 fn blocked_identifier ->
1009 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1010 {:ok, blocker} <- block(blocker, blocked),
1011 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1015 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1022 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1023 def perform(:follow_import, %User{} = follower, followed_identifiers)
1024 when is_list(followed_identifiers) do
1026 followed_identifiers,
1027 fn followed_identifier ->
1028 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1029 {:ok, follower} <- maybe_direct_follow(follower, followed),
1030 {:ok, _} <- ActivityPub.follow(follower, followed) do
1034 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1041 @spec external_users_query() :: Ecto.Query.t()
1042 def external_users_query do
1050 @spec external_users(keyword()) :: [User.t()]
1051 def external_users(opts \\ []) do
1053 external_users_query()
1054 |> select([u], struct(u, [:id, :ap_id, :info]))
1058 do: where(query, [u], u.id > ^opts[:max_id]),
1063 do: limit(query, ^opts[:limit]),
1069 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers),
1071 PleromaJobQueue.enqueue(:background, __MODULE__, [
1077 def follow_import(%User{} = follower, followed_identifiers) when is_list(followed_identifiers),
1079 PleromaJobQueue.enqueue(:background, __MODULE__, [
1082 followed_identifiers
1085 def delete_user_activities(%User{ap_id: ap_id} = user) do
1087 |> Activity.query_by_actor()
1088 |> RepoStreamer.chunk_stream(50)
1089 |> Stream.each(fn activities ->
1090 Enum.each(activities, &delete_activity(&1))
1097 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1099 |> Object.normalize()
1100 |> ActivityPub.delete()
1103 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1104 user = get_cached_by_ap_id(activity.actor)
1105 object = Object.normalize(activity)
1107 ActivityPub.unlike(user, object)
1110 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1111 user = get_cached_by_ap_id(activity.actor)
1112 object = Object.normalize(activity)
1114 ActivityPub.unannounce(user, object)
1117 defp delete_activity(_activity), do: "Doing nothing"
1119 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1120 Pleroma.HTML.Scrubber.TwitterText
1123 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1125 def fetch_by_ap_id(ap_id) do
1126 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1133 case OStatus.make_user(ap_id) do
1134 {:ok, user} -> {:ok, user}
1135 _ -> {:error, "Could not fetch by AP id"}
1140 def get_or_fetch_by_ap_id(ap_id) do
1141 user = get_cached_by_ap_id(ap_id)
1143 if !is_nil(user) and !User.needs_update?(user) do
1146 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1147 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1149 resp = fetch_by_ap_id(ap_id)
1151 if should_fetch_initial do
1152 with {:ok, %User{} = user} <- resp do
1153 fetch_initial_posts(user)
1161 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1162 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1163 if user = get_cached_by_ap_id(uri) do
1167 %User{info: %User.Info{}}
1168 |> cast(%{}, [:ap_id, :nickname, :local])
1169 |> put_change(:ap_id, uri)
1170 |> put_change(:nickname, nickname)
1171 |> put_change(:local, true)
1172 |> put_change(:follower_address, uri <> "/followers")
1174 {:ok, user} = Repo.insert(changes)
1180 def public_key_from_info(%{
1181 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1185 |> :public_key.pem_decode()
1187 |> :public_key.pem_entry_decode()
1193 def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
1194 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1197 def public_key_from_info(_), do: {:error, "not found key"}
1199 def get_public_key_for_ap_id(ap_id) do
1200 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1201 {:ok, public_key} <- public_key_from_info(user.info) do
1208 defp blank?(""), do: nil
1209 defp blank?(n), do: n
1211 def insert_or_update_user(data) do
1213 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1214 |> remote_user_creation()
1215 |> Repo.insert(on_conflict: :replace_all, conflict_target: :nickname)
1219 def ap_enabled?(%User{local: true}), do: true
1220 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1221 def ap_enabled?(_), do: false
1223 @doc "Gets or fetch a user by uri or nickname."
1224 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1225 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1226 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1228 # wait a period of time and return newest version of the User structs
1229 # this is because we have synchronous follow APIs and need to simulate them
1230 # with an async handshake
1231 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1232 with %User{} = a <- User.get_cached_by_id(a.id),
1233 %User{} = b <- User.get_cached_by_id(b.id) do
1241 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1242 with :ok <- :timer.sleep(timeout),
1243 %User{} = a <- User.get_cached_by_id(a.id),
1244 %User{} = b <- User.get_cached_by_id(b.id) do
1252 def parse_bio(bio) when is_binary(bio) and bio != "" do
1254 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1258 def parse_bio(_), do: ""
1260 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1261 # TODO: get profile URLs other than user.ap_id
1262 profile_urls = [user.ap_id]
1265 |> CommonUtils.format_input("text/plain",
1266 mentions_format: :full,
1267 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1272 def parse_bio(_, _), do: ""
1274 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1275 Repo.transaction(fn ->
1276 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1280 def tag(nickname, tags) when is_binary(nickname),
1281 do: tag(get_by_nickname(nickname), tags)
1283 def tag(%User{} = user, tags),
1284 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1286 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1287 Repo.transaction(fn ->
1288 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1292 def untag(nickname, tags) when is_binary(nickname),
1293 do: untag(get_by_nickname(nickname), tags)
1295 def untag(%User{} = user, tags),
1296 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1298 defp update_tags(%User{} = user, new_tags) do
1299 {:ok, updated_user} =
1301 |> change(%{tags: new_tags})
1302 |> update_and_set_cache()
1307 defp normalize_tags(tags) do
1310 |> Enum.map(&String.downcase(&1))
1313 defp local_nickname_regex do
1314 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1315 @extended_local_nickname_regex
1317 @strict_local_nickname_regex
1321 def local_nickname(nickname_or_mention) do
1324 |> String.split("@")
1328 def full_nickname(nickname_or_mention),
1329 do: String.trim_leading(nickname_or_mention, "@")
1331 def error_user(ap_id) do
1336 nickname: "erroruser@example.com",
1337 inserted_at: NaiveDateTime.utc_now()
1341 @spec all_superusers() :: [User.t()]
1342 def all_superusers do
1343 User.Query.build(%{super_users: true, local: true, deactivated: false})
1347 def showing_reblogs?(%User{} = user, %User{} = target) do
1348 target.ap_id not in user.info.muted_reblogs
1352 The function returns a query to get users with no activity for given interval of days.
1353 Inactive users are those who didn't read any notification, or had any activity where
1354 the user is the activity's actor, during `inactivity_threshold` days.
1355 Deactivated users will not appear in this list.
1359 iex> Pleroma.User.list_inactive_users()
1362 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1363 def list_inactive_users_query(inactivity_threshold \\ 7) do
1364 negative_inactivity_threshold = -inactivity_threshold
1365 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1366 # Subqueries are not supported in `where` clauses, join gets too complicated.
1367 has_read_notifications =
1368 from(n in Pleroma.Notification,
1369 where: n.seen == true,
1371 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1374 |> Pleroma.Repo.all()
1376 from(u in Pleroma.User,
1377 left_join: a in Pleroma.Activity,
1378 on: u.ap_id == a.actor,
1379 where: not is_nil(u.nickname),
1380 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1381 where: u.id not in ^has_read_notifications,
1384 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1385 is_nil(max(a.inserted_at))
1390 Enable or disable email notifications for user
1394 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1395 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1397 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1398 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1400 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1401 {:ok, t()} | {:error, Ecto.Changeset.t()}
1402 def switch_email_notifications(user, type, status) do
1403 info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})
1406 |> put_embed(:info, info)
1407 |> update_and_set_cache()
1411 Set `last_digest_emailed_at` value for the user to current time
1413 @spec touch_last_digest_emailed_at(t()) :: t()
1414 def touch_last_digest_emailed_at(user) do
1415 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1417 {:ok, updated_user} =
1419 |> change(%{last_digest_emailed_at: now})
1420 |> update_and_set_cache()
1425 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1426 def toggle_confirmation(%User{} = user) do
1427 need_confirmation? = !user.info.confirmation_pending
1430 User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
1434 |> put_embed(:info, info_changeset)
1435 |> update_and_set_cache()
1438 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1442 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1443 # use instance-default
1444 config = Pleroma.Config.get([:assets, :mascots])
1445 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1446 mascot = Keyword.get(config, default_mascot)
1449 "id" => "default-mascot",
1450 "url" => mascot[:url],
1451 "preview_url" => mascot[:url],
1453 "mime_type" => mascot[:mime_type]
1458 def ensure_keys_present(%User{info: info} = user) do
1462 {:ok, pem} = Keys.generate_rsa_pem()
1465 |> Ecto.Changeset.change()
1466 |> Ecto.Changeset.put_embed(:info, User.Info.set_keys(info, pem))
1467 |> update_and_set_cache()
1471 def get_ap_ids_by_nicknames(nicknames) do
1473 where: u.nickname in ^nicknames,
1479 defdelegate search(query, opts \\ []), to: User.Search
1481 defp put_password_hash(
1482 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1484 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1487 defp put_password_hash(changeset), do: changeset
1489 def is_internal_user?(%User{nickname: nil}), do: true
1490 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1491 def is_internal_user?(_), do: false