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
14 alias Pleroma.Conversation.Participation
15 alias Pleroma.Delivery
17 alias Pleroma.Notification
19 alias Pleroma.Registration
21 alias Pleroma.RepoStreamer
24 alias Pleroma.Web.ActivityPub.ActivityPub
25 alias Pleroma.Web.ActivityPub.Utils
26 alias Pleroma.Web.CommonAPI
27 alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
28 alias Pleroma.Web.OAuth
29 alias Pleroma.Web.RelMe
30 alias Pleroma.Workers.BackgroundWorker
34 @type t :: %__MODULE__{}
36 @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
38 # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
39 @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])?)*$/
41 @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
42 @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
46 field(:email, :string)
48 field(:nickname, :string)
49 field(:password_hash, :string)
50 field(:password, :string, virtual: true)
51 field(:password_confirmation, :string, virtual: true)
53 field(:following, {:array, :string}, default: [])
54 field(:ap_id, :string)
56 field(:local, :boolean, default: true)
57 field(:follower_address, :string)
58 field(:following_address, :string)
59 field(:search_rank, :float, virtual: true)
60 field(:search_type, :integer, virtual: true)
61 field(:tags, {:array, :string}, default: [])
62 field(:last_refreshed_at, :naive_datetime_usec)
63 field(:last_digest_emailed_at, :naive_datetime)
64 has_many(:notifications, Notification)
65 has_many(:registrations, Registration)
66 has_many(:deliveries, Delivery)
67 embeds_one(:info, User.Info)
72 def auth_active?(%User{info: %User.Info{confirmation_pending: true}}),
73 do: !Pleroma.Config.get([:instance, :account_activation_required])
75 def auth_active?(%User{}), do: true
77 def visible_for?(user, for_user \\ nil)
79 def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
81 def visible_for?(%User{} = user, for_user) do
82 auth_active?(user) || superuser?(for_user)
85 def visible_for?(_, _), do: false
87 def superuser?(%User{local: true, info: %User.Info{is_admin: true}}), do: true
88 def superuser?(%User{local: true, info: %User.Info{is_moderator: true}}), do: true
89 def superuser?(_), do: false
91 def invisible?(%User{info: %User.Info{invisible: true}}), do: true
92 def invisible?(_), do: false
94 def avatar_url(user, options \\ []) do
96 %{"url" => [%{"href" => href} | _]} -> href
97 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
101 def banner_url(user, options \\ []) do
102 case user.info.banner do
103 %{"url" => [%{"href" => href} | _]} -> href
104 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
108 def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url
109 def profile_url(%User{ap_id: ap_id}), do: ap_id
110 def profile_url(_), do: nil
112 def ap_id(%User{nickname: nickname}), do: "#{Web.base_url()}/users/#{nickname}"
114 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
115 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
117 @spec ap_following(User.t()) :: Sring.t()
118 def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
119 def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
121 def user_info(%User{} = user, args \\ %{}) do
123 Map.get(args, :following_count, user.info.following_count || following_count(user))
125 follower_count = Map.get(args, :follower_count, user.info.follower_count)
128 note_count: user.info.note_count,
129 locked: user.info.locked,
130 confirmation_pending: user.info.confirmation_pending,
131 default_scope: user.info.default_scope
133 |> Map.put(:following_count, following_count)
134 |> Map.put(:follower_count, follower_count)
137 def follow_state(%User{} = user, %User{} = target) do
138 case Utils.fetch_latest_follow(user, target) do
139 %{data: %{"state" => state}} -> state
140 # Ideally this would be nil, but then Cachex does not commit the value
145 def get_cached_follow_state(user, target) do
146 key = "follow_state:#{user.ap_id}|#{target.ap_id}"
147 Cachex.fetch!(:user_cache, key, fn _ -> {:commit, follow_state(user, target)} end)
150 @spec set_follow_state_cache(String.t(), String.t(), String.t()) :: {:ok | :error, boolean()}
151 def set_follow_state_cache(user_ap_id, target_ap_id, state) do
152 Cachex.put(:user_cache, "follow_state:#{user_ap_id}|#{target_ap_id}", state)
155 def set_info_cache(user, args) do
156 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user, args))
159 @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
160 def restrict_deactivated(query) do
162 where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info)
166 def following_count(%User{following: []}), do: 0
168 def following_count(%User{} = user) do
170 |> get_friends_query()
171 |> Repo.aggregate(:count, :id)
174 defp truncate_if_exists(params, key, max_length) do
175 if Map.has_key?(params, key) and is_binary(params[key]) do
176 {value, _chopped} = String.split_at(params[key], max_length)
177 Map.put(params, key, value)
183 def remote_user_creation(params) do
184 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
185 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
189 |> Map.put(:info, params[:info] || %{})
190 |> truncate_if_exists(:name, name_limit)
191 |> truncate_if_exists(:bio, bio_limit)
195 |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
196 |> validate_required([:name, :ap_id])
197 |> unique_constraint(:nickname)
198 |> validate_format(:nickname, @email_regex)
199 |> validate_length(:bio, max: bio_limit)
200 |> validate_length(:name, max: name_limit)
201 |> change_info(&User.Info.remote_user_creation(&1, params[:info]))
203 case params[:info][:source_data] do
204 %{"followers" => followers, "following" => following} ->
206 |> put_change(:follower_address, followers)
207 |> put_change(:following_address, following)
210 followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
211 put_change(changeset, :follower_address, followers)
215 def update_changeset(struct, params \\ %{}) do
216 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
217 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
220 |> cast(params, [:bio, :name, :avatar, :following])
221 |> unique_constraint(:nickname)
222 |> validate_format(:nickname, local_nickname_regex())
223 |> validate_length(:bio, max: bio_limit)
224 |> validate_length(:name, min: 1, max: name_limit)
227 def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
228 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
229 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
231 params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
242 |> unique_constraint(:nickname)
243 |> validate_format(:nickname, local_nickname_regex())
244 |> validate_length(:bio, max: bio_limit)
245 |> validate_length(:name, max: name_limit)
246 |> change_info(&User.Info.user_upgrade(&1, params[:info], remote?))
249 def password_update_changeset(struct, params) do
251 |> cast(params, [:password, :password_confirmation])
252 |> validate_required([:password, :password_confirmation])
253 |> validate_confirmation(:password)
255 |> put_embed(:info, User.Info.set_password_reset_pending(struct.info, false))
258 @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
259 def reset_password(%User{id: user_id} = user, data) do
262 |> Multi.update(:user, password_update_changeset(user, data))
263 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
264 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
266 case Repo.transaction(multi) do
267 {:ok, %{user: user} = _} -> set_cache(user)
268 {:error, _, changeset, _} -> {:error, changeset}
272 def force_password_reset_async(user) do
273 BackgroundWorker.enqueue("force_password_reset", %{"user_id" => user.id})
276 @spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
277 def force_password_reset(user) do
278 info_cng = User.Info.set_password_reset_pending(user.info, true)
282 |> put_embed(:info, info_cng)
283 |> update_and_set_cache()
286 def register_changeset(struct, params \\ %{}, opts \\ []) do
287 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
288 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
291 if is_nil(opts[:need_confirmation]) do
292 Pleroma.Config.get([:instance, :account_activation_required])
294 opts[:need_confirmation]
298 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
299 |> validate_required([:name, :nickname, :password, :password_confirmation])
300 |> validate_confirmation(:password)
301 |> unique_constraint(:email)
302 |> unique_constraint(:nickname)
303 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
304 |> validate_format(:nickname, local_nickname_regex())
305 |> validate_format(:email, @email_regex)
306 |> validate_length(:bio, max: bio_limit)
307 |> validate_length(:name, min: 1, max: name_limit)
308 |> change_info(&User.Info.confirmation_changeset(&1, need_confirmation: need_confirmation?))
309 |> maybe_validate_required_email(opts[:external])
312 |> unique_constraint(:ap_id)
313 |> put_following_and_follower_address()
316 def maybe_validate_required_email(changeset, true), do: changeset
317 def maybe_validate_required_email(changeset, _), do: validate_required(changeset, [:email])
319 defp put_ap_id(changeset) do
320 ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
321 put_change(changeset, :ap_id, ap_id)
324 defp put_following_and_follower_address(changeset) do
325 followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
328 |> put_change(:following, [followers])
329 |> put_change(:follower_address, followers)
332 defp autofollow_users(user) do
333 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
336 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
339 follow_all(user, autofollowed_users)
342 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
343 def register(%Ecto.Changeset{} = changeset) do
344 with {:ok, user} <- Repo.insert(changeset) do
345 post_register_action(user)
349 def post_register_action(%User{} = user) do
350 with {:ok, user} <- autofollow_users(user),
351 {:ok, user} <- set_cache(user),
352 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
353 {:ok, _} <- try_send_confirmation_email(user) do
358 def try_send_confirmation_email(%User{} = user) do
359 if user.info.confirmation_pending &&
360 Pleroma.Config.get([:instance, :account_activation_required]) do
362 |> Pleroma.Emails.UserEmail.account_confirmation_email()
363 |> Pleroma.Emails.Mailer.deliver_async()
371 def needs_update?(%User{local: true}), do: false
373 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
375 def needs_update?(%User{local: false} = user) do
376 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
379 def needs_update?(_), do: true
381 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
382 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
386 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
387 follow(follower, followed)
390 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
391 if not ap_enabled?(followed) do
392 follow(follower, followed)
398 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
399 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
400 def follow_all(follower, followeds) do
403 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
404 |> Enum.map(fn %{follower_address: fa} -> fa end)
408 where: u.id == ^follower.id,
413 "array(select distinct unnest (array_cat(?, ?)))",
422 {1, [follower]} = Repo.update_all(q, [])
424 Enum.each(followeds, &update_follower_count/1)
429 def follow(%User{} = follower, %User{info: info} = followed) do
430 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
431 ap_followers = followed.follower_address
435 {:error, "Could not follow user: You are deactivated."}
437 deny_follow_blocked and blocks?(followed, follower) ->
438 {:error, "Could not follow user: #{followed.nickname} blocked you."}
443 where: u.id == ^follower.id,
444 update: [push: [following: ^ap_followers]],
448 {1, [follower]} = Repo.update_all(q, [])
450 follower = maybe_update_following_count(follower)
452 {:ok, _} = update_follower_count(followed)
458 def unfollow(%User{} = follower, %User{} = followed) do
459 ap_followers = followed.follower_address
461 if following?(follower, followed) and follower.ap_id != followed.ap_id do
464 where: u.id == ^follower.id,
465 update: [pull: [following: ^ap_followers]],
469 {1, [follower]} = Repo.update_all(q, [])
471 follower = maybe_update_following_count(follower)
473 {:ok, followed} = update_follower_count(followed)
477 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
479 {:error, "Not subscribed!"}
483 @spec following?(User.t(), User.t()) :: boolean
484 def following?(%User{} = follower, %User{} = followed) do
485 Enum.member?(follower.following, followed.follower_address)
488 def locked?(%User{} = user) do
489 user.info.locked || false
493 Repo.get_by(User, id: id)
496 def get_by_ap_id(ap_id) do
497 Repo.get_by(User, ap_id: ap_id)
500 def get_all_by_ap_id(ap_ids) do
501 from(u in __MODULE__,
502 where: u.ap_id in ^ap_ids
507 def get_all_by_ids(ids) do
508 from(u in __MODULE__, where: u.id in ^ids)
512 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
513 # of the ap_id and the domain and tries to get that user
514 def get_by_guessed_nickname(ap_id) do
515 domain = URI.parse(ap_id).host
516 name = List.last(String.split(ap_id, "/"))
517 nickname = "#{name}@#{domain}"
519 get_cached_by_nickname(nickname)
522 def set_cache({:ok, user}), do: set_cache(user)
523 def set_cache({:error, err}), do: {:error, err}
525 def set_cache(%User{} = user) do
526 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
527 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
528 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
532 def update_and_set_cache(changeset) do
533 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
538 def invalidate_cache(user) do
539 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
540 Cachex.del(:user_cache, "nickname:#{user.nickname}")
541 Cachex.del(:user_cache, "user_info:#{user.id}")
544 def get_cached_by_ap_id(ap_id) do
545 key = "ap_id:#{ap_id}"
546 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
549 def get_cached_by_id(id) do
553 Cachex.fetch!(:user_cache, key, fn _ ->
557 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
558 {:commit, user.ap_id}
564 get_cached_by_ap_id(ap_id)
567 def get_cached_by_nickname(nickname) do
568 key = "nickname:#{nickname}"
570 Cachex.fetch!(:user_cache, key, fn ->
571 case get_or_fetch_by_nickname(nickname) do
572 {:ok, user} -> {:commit, user}
573 {:error, _error} -> {:ignore, nil}
578 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
579 restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
582 is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
583 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
585 restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
586 get_cached_by_nickname(nickname_or_id)
588 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
589 get_cached_by_nickname(nickname_or_id)
596 def get_by_nickname(nickname) do
597 Repo.get_by(User, nickname: nickname) ||
598 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
599 Repo.get_by(User, nickname: local_nickname(nickname))
603 def get_by_email(email), do: Repo.get_by(User, email: email)
605 def get_by_nickname_or_email(nickname_or_email) do
606 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
609 def get_cached_user_info(user) do
610 key = "user_info:#{user.id}"
611 Cachex.fetch!(:user_cache, key, fn -> user_info(user) end)
614 def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname)
616 def get_or_fetch_by_nickname(nickname) do
617 with %User{} = user <- get_by_nickname(nickname) do
621 with [_nick, _domain] <- String.split(nickname, "@"),
622 {:ok, user} <- fetch_by_nickname(nickname) do
623 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
624 fetch_initial_posts(user)
629 _e -> {:error, "not found " <> nickname}
634 @doc "Fetch some posts when the user has just been federated with"
635 def fetch_initial_posts(user) do
636 BackgroundWorker.enqueue("fetch_initial_posts", %{"user_id" => user.id})
639 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
640 def get_followers_query(%User{} = user, nil) do
641 User.Query.build(%{followers: user, deactivated: false})
644 def get_followers_query(user, page) do
646 |> get_followers_query(nil)
647 |> User.Query.paginate(page, 20)
650 @spec get_followers_query(User.t()) :: Ecto.Query.t()
651 def get_followers_query(user), do: get_followers_query(user, nil)
653 @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
654 def get_followers(user, page \\ nil) do
656 |> get_followers_query(page)
660 @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
661 def get_external_followers(user, page \\ nil) do
663 |> get_followers_query(page)
664 |> User.Query.build(%{external: true})
668 def get_followers_ids(user, page \\ nil) do
670 |> get_followers_query(page)
675 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
676 def get_friends_query(%User{} = user, nil) do
677 User.Query.build(%{friends: user, deactivated: false})
680 def get_friends_query(user, page) do
682 |> get_friends_query(nil)
683 |> User.Query.paginate(page, 20)
686 @spec get_friends_query(User.t()) :: Ecto.Query.t()
687 def get_friends_query(user), do: get_friends_query(user, nil)
689 def get_friends(user, page \\ nil) do
691 |> get_friends_query(page)
695 def get_friends_ids(user, page \\ nil) do
697 |> get_friends_query(page)
702 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
703 def get_follow_requests(%User{} = user) do
705 |> Activity.follow_requests_for_actor()
706 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
707 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
708 |> group_by([a, u], u.id)
713 def increase_note_count(%User{} = user) do
715 |> where(id: ^user.id)
720 "safe_jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
727 |> Repo.update_all([])
729 {1, [user]} -> set_cache(user)
734 def decrease_note_count(%User{} = user) do
736 |> where(id: ^user.id)
741 "safe_jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
748 |> Repo.update_all([])
750 {1, [user]} -> set_cache(user)
755 def update_note_count(%User{} = user) do
759 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
764 update_info(user, &User.Info.set_note_count(&1, note_count))
767 def update_mascot(user, url) do
769 User.Info.mascot_update(
776 |> put_embed(:info, info_changeset)
777 |> update_and_set_cache()
780 @spec maybe_fetch_follow_information(User.t()) :: User.t()
781 def maybe_fetch_follow_information(user) do
782 with {:ok, user} <- fetch_follow_information(user) do
786 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
792 def fetch_follow_information(user) do
793 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
794 update_info(user, &User.Info.follow_information_update(&1, info))
798 def update_follower_count(%User{} = user) do
799 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
800 follower_count_query =
801 User.Query.build(%{followers: user, deactivated: false})
802 |> select([u], %{count: count(u.id)})
805 |> where(id: ^user.id)
806 |> join(:inner, [u], s in subquery(follower_count_query))
811 "safe_jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
818 |> Repo.update_all([])
820 {1, [user]} -> set_cache(user)
824 {:ok, maybe_fetch_follow_information(user)}
828 @spec maybe_update_following_count(User.t()) :: User.t()
829 def maybe_update_following_count(%User{local: false} = user) do
830 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
831 maybe_fetch_follow_information(user)
837 def maybe_update_following_count(user), do: user
839 def set_unread_conversation_count(%User{local: true} = user) do
840 unread_query = Participation.unread_conversation_count_for_user(user)
843 |> join(:inner, [u], p in subquery(unread_query))
848 "jsonb_set(?, '{unread_conversation_count}', ?::varchar::jsonb, true)",
854 |> where([u], u.id == ^user.id)
856 |> Repo.update_all([])
858 {1, [user]} -> set_cache(user)
863 def set_unread_conversation_count(_), do: :noop
865 def increment_unread_conversation_count(conversation, %User{local: true} = user) do
867 Participation.unread_conversation_count_for_user(user)
868 |> where([p], p.conversation_id == ^conversation.id)
871 |> join(:inner, [u], p in subquery(unread_query))
876 "jsonb_set(?, '{unread_conversation_count}', (coalesce((?->>'unread_conversation_count')::int, 0) + 1)::varchar::jsonb, true)",
882 |> where([u], u.id == ^user.id)
883 |> where([u, p], p.count == 0)
885 |> Repo.update_all([])
887 {1, [user]} -> set_cache(user)
892 def increment_unread_conversation_count(_, _), do: :noop
894 def remove_duplicated_following(%User{following: following} = user) do
895 uniq_following = Enum.uniq(following)
897 if length(following) == length(uniq_following) do
901 |> update_changeset(%{following: uniq_following})
902 |> update_and_set_cache()
906 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
907 def get_users_from_set(ap_ids, local_only \\ true) do
908 criteria = %{ap_id: ap_ids, deactivated: false}
909 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
911 User.Query.build(criteria)
915 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
916 def get_recipients_from_activity(%Activity{recipients: to}) do
917 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
921 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
922 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
923 update_info(muter, &User.Info.add_to_mutes(&1, ap_id, notifications?))
926 def unmute(muter, %{ap_id: ap_id}) do
927 update_info(muter, &User.Info.remove_from_mutes(&1, ap_id))
930 def subscribe(subscriber, %{ap_id: ap_id}) do
931 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
932 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
934 if blocks?(subscribed, subscriber) and deny_follow_blocked do
935 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
937 update_info(subscribed, &User.Info.add_to_subscribers(&1, subscriber.ap_id))
942 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
943 with %User{} = user <- get_cached_by_ap_id(ap_id) do
944 update_info(user, &User.Info.remove_from_subscribers(&1, unsubscriber.ap_id))
948 def block(blocker, %User{ap_id: ap_id} = blocked) do
949 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
951 if following?(blocker, blocked) do
952 {:ok, blocker, _} = unfollow(blocker, blocked)
958 # clear any requested follows as well
960 case CommonAPI.reject_follow_request(blocked, blocker) do
961 {:ok, %User{} = updated_blocked} -> updated_blocked
966 if subscribed_to?(blocked, blocker) do
967 {:ok, blocker} = unsubscribe(blocked, blocker)
973 if following?(blocked, blocker), do: unfollow(blocked, blocker)
975 {:ok, blocker} = update_follower_count(blocker)
977 update_info(blocker, &User.Info.add_to_block(&1, ap_id))
980 # helper to handle the block given only an actor's AP id
981 def block(blocker, %{ap_id: ap_id}) do
982 block(blocker, get_cached_by_ap_id(ap_id))
985 def unblock(blocker, %{ap_id: ap_id}) do
986 update_info(blocker, &User.Info.remove_from_block(&1, ap_id))
989 def mutes?(nil, _), do: false
990 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
992 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
993 def muted_notifications?(nil, _), do: false
995 def muted_notifications?(user, %{ap_id: ap_id}),
996 do: Enum.member?(user.info.muted_notifications, ap_id)
998 def blocks?(%User{} = user, %User{} = target) do
999 blocks_ap_id?(user, target) || blocks_domain?(user, target)
1002 def blocks?(nil, _), do: false
1004 def blocks_ap_id?(%User{} = user, %User{} = target) do
1005 Enum.member?(user.info.blocks, target.ap_id)
1008 def blocks_ap_id?(_, _), do: false
1010 def blocks_domain?(%User{} = user, %User{} = target) do
1011 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
1012 %{host: host} = URI.parse(target.ap_id)
1013 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1016 def blocks_domain?(_, _), do: false
1018 def subscribed_to?(user, %{ap_id: ap_id}) do
1019 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1020 Enum.member?(target.info.subscribers, user.ap_id)
1024 @spec muted_users(User.t()) :: [User.t()]
1025 def muted_users(user) do
1026 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
1030 @spec blocked_users(User.t()) :: [User.t()]
1031 def blocked_users(user) do
1032 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
1036 @spec subscribers(User.t()) :: [User.t()]
1037 def subscribers(user) do
1038 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
1042 def block_domain(user, domain) do
1043 update_info(user, &User.Info.add_to_domain_block(&1, domain))
1046 def unblock_domain(user, domain) do
1047 update_info(user, &User.Info.remove_from_domain_block(&1, domain))
1050 def deactivate_async(user, status \\ true) do
1051 BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
1054 def deactivate(user, status \\ true)
1056 def deactivate(users, status) when is_list(users) do
1057 Repo.transaction(fn ->
1058 for user <- users, do: deactivate(user, status)
1062 def deactivate(%User{} = user, status) do
1063 with {:ok, user} <- update_info(user, &User.Info.set_activation_status(&1, status)) do
1064 Enum.each(get_followers(user), &invalidate_cache/1)
1065 Enum.each(get_friends(user), &update_follower_count/1)
1071 def update_notification_settings(%User{} = user, settings \\ %{}) do
1072 update_info(user, &User.Info.update_notification_settings(&1, settings))
1075 def delete(users) when is_list(users) do
1076 for user <- users, do: delete(user)
1079 def delete(%User{} = user) do
1080 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1083 def perform(:force_password_reset, user), do: force_password_reset(user)
1085 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1086 def perform(:delete, %User{} = user) do
1087 {:ok, _user} = ActivityPub.delete(user)
1089 # Remove all relationships
1092 |> Enum.each(fn follower ->
1093 ActivityPub.unfollow(follower, user)
1094 unfollow(follower, user)
1099 |> Enum.each(fn followed ->
1100 ActivityPub.unfollow(user, followed)
1101 unfollow(user, followed)
1104 delete_user_activities(user)
1105 invalidate_cache(user)
1109 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1110 def perform(:fetch_initial_posts, %User{} = user) do
1111 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1113 # Insert all the posts in reverse order, so they're in the right order on the timeline
1114 user.info.source_data["outbox"]
1115 |> Utils.fetch_ordered_collection(pages)
1117 |> Enum.each(&Pleroma.Web.Federator.incoming_ap_doc/1)
1120 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1122 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1123 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1124 when is_list(blocked_identifiers) do
1126 blocked_identifiers,
1127 fn blocked_identifier ->
1128 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1129 {:ok, blocker} <- block(blocker, blocked),
1130 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1134 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1141 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1142 def perform(:follow_import, %User{} = follower, followed_identifiers)
1143 when is_list(followed_identifiers) do
1145 followed_identifiers,
1146 fn followed_identifier ->
1147 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1148 {:ok, follower} <- maybe_direct_follow(follower, followed),
1149 {:ok, _} <- ActivityPub.follow(follower, followed) do
1153 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1160 @spec external_users_query() :: Ecto.Query.t()
1161 def external_users_query do
1169 @spec external_users(keyword()) :: [User.t()]
1170 def external_users(opts \\ []) do
1172 external_users_query()
1173 |> select([u], struct(u, [:id, :ap_id, :info]))
1177 do: where(query, [u], u.id > ^opts[:max_id]),
1182 do: limit(query, ^opts[:limit]),
1188 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1189 BackgroundWorker.enqueue("blocks_import", %{
1190 "blocker_id" => blocker.id,
1191 "blocked_identifiers" => blocked_identifiers
1195 def follow_import(%User{} = follower, followed_identifiers)
1196 when is_list(followed_identifiers) do
1197 BackgroundWorker.enqueue("follow_import", %{
1198 "follower_id" => follower.id,
1199 "followed_identifiers" => followed_identifiers
1203 def delete_user_activities(%User{ap_id: ap_id}) do
1205 |> Activity.Queries.by_actor()
1206 |> RepoStreamer.chunk_stream(50)
1207 |> Stream.each(fn activities -> Enum.each(activities, &delete_activity/1) end)
1211 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1213 |> Object.normalize()
1214 |> ActivityPub.delete()
1217 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1218 object = Object.normalize(activity)
1221 |> get_cached_by_ap_id()
1222 |> ActivityPub.unlike(object)
1225 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1226 object = Object.normalize(activity)
1229 |> get_cached_by_ap_id()
1230 |> ActivityPub.unannounce(object)
1233 defp delete_activity(_activity), do: "Doing nothing"
1235 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1236 Pleroma.HTML.Scrubber.TwitterText
1239 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1241 def fetch_by_ap_id(ap_id), do: ActivityPub.make_user_from_ap_id(ap_id)
1243 def get_or_fetch_by_ap_id(ap_id) do
1244 user = get_cached_by_ap_id(ap_id)
1246 if !is_nil(user) and !needs_update?(user) do
1249 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1250 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1252 resp = fetch_by_ap_id(ap_id)
1254 if should_fetch_initial do
1255 with {:ok, %User{} = user} <- resp do
1256 fetch_initial_posts(user)
1264 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1265 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1266 with %User{} = user <- get_cached_by_ap_id(uri) do
1271 %User{info: %User.Info{}}
1272 |> cast(%{}, [:ap_id, :nickname, :local])
1273 |> put_change(:ap_id, uri)
1274 |> put_change(:nickname, nickname)
1275 |> put_change(:local, true)
1276 |> put_change(:follower_address, uri <> "/followers")
1284 def public_key_from_info(%{
1285 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1289 |> :public_key.pem_decode()
1291 |> :public_key.pem_entry_decode()
1296 def public_key_from_info(_), do: {:error, "not found key"}
1298 def get_public_key_for_ap_id(ap_id) do
1299 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1300 {:ok, public_key} <- public_key_from_info(user.info) do
1307 defp blank?(""), do: nil
1308 defp blank?(n), do: n
1310 def insert_or_update_user(data) do
1312 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1313 |> remote_user_creation()
1314 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1318 def ap_enabled?(%User{local: true}), do: true
1319 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1320 def ap_enabled?(_), do: false
1322 @doc "Gets or fetch a user by uri or nickname."
1323 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1324 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1325 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1327 # wait a period of time and return newest version of the User structs
1328 # this is because we have synchronous follow APIs and need to simulate them
1329 # with an async handshake
1330 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1331 with %User{} = a <- get_cached_by_id(a.id),
1332 %User{} = b <- get_cached_by_id(b.id) do
1339 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1340 with :ok <- :timer.sleep(timeout),
1341 %User{} = a <- get_cached_by_id(a.id),
1342 %User{} = b <- get_cached_by_id(b.id) do
1349 def parse_bio(bio) when is_binary(bio) and bio != "" do
1351 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1355 def parse_bio(_), do: ""
1357 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1358 # TODO: get profile URLs other than user.ap_id
1359 profile_urls = [user.ap_id]
1362 |> CommonUtils.format_input("text/plain",
1363 mentions_format: :full,
1364 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1369 def parse_bio(_, _), do: ""
1371 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1372 Repo.transaction(fn ->
1373 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1377 def tag(nickname, tags) when is_binary(nickname),
1378 do: tag(get_by_nickname(nickname), tags)
1380 def tag(%User{} = user, tags),
1381 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1383 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1384 Repo.transaction(fn ->
1385 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1389 def untag(nickname, tags) when is_binary(nickname),
1390 do: untag(get_by_nickname(nickname), tags)
1392 def untag(%User{} = user, tags),
1393 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1395 defp update_tags(%User{} = user, new_tags) do
1396 {:ok, updated_user} =
1398 |> change(%{tags: new_tags})
1399 |> update_and_set_cache()
1404 defp normalize_tags(tags) do
1407 |> Enum.map(&String.downcase/1)
1410 defp local_nickname_regex do
1411 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1412 @extended_local_nickname_regex
1414 @strict_local_nickname_regex
1418 def local_nickname(nickname_or_mention) do
1421 |> String.split("@")
1425 def full_nickname(nickname_or_mention),
1426 do: String.trim_leading(nickname_or_mention, "@")
1428 def error_user(ap_id) do
1433 nickname: "erroruser@example.com",
1434 inserted_at: NaiveDateTime.utc_now()
1438 @spec all_superusers() :: [User.t()]
1439 def all_superusers do
1440 User.Query.build(%{super_users: true, local: true, deactivated: false})
1444 def showing_reblogs?(%User{} = user, %User{} = target) do
1445 target.ap_id not in user.info.muted_reblogs
1449 The function returns a query to get users with no activity for given interval of days.
1450 Inactive users are those who didn't read any notification, or had any activity where
1451 the user is the activity's actor, during `inactivity_threshold` days.
1452 Deactivated users will not appear in this list.
1456 iex> Pleroma.User.list_inactive_users()
1459 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1460 def list_inactive_users_query(inactivity_threshold \\ 7) do
1461 negative_inactivity_threshold = -inactivity_threshold
1462 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1463 # Subqueries are not supported in `where` clauses, join gets too complicated.
1464 has_read_notifications =
1465 from(n in Pleroma.Notification,
1466 where: n.seen == true,
1468 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1471 |> Pleroma.Repo.all()
1473 from(u in Pleroma.User,
1474 left_join: a in Pleroma.Activity,
1475 on: u.ap_id == a.actor,
1476 where: not is_nil(u.nickname),
1477 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1478 where: u.id not in ^has_read_notifications,
1481 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1482 is_nil(max(a.inserted_at))
1487 Enable or disable email notifications for user
1491 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1492 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1494 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1495 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1497 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1498 {:ok, t()} | {:error, Ecto.Changeset.t()}
1499 def switch_email_notifications(user, type, status) do
1500 update_info(user, &User.Info.update_email_notifications(&1, %{type => status}))
1504 Set `last_digest_emailed_at` value for the user to current time
1506 @spec touch_last_digest_emailed_at(t()) :: t()
1507 def touch_last_digest_emailed_at(user) do
1508 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1510 {:ok, updated_user} =
1512 |> change(%{last_digest_emailed_at: now})
1513 |> update_and_set_cache()
1518 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1519 def toggle_confirmation(%User{} = user) do
1520 need_confirmation? = !user.info.confirmation_pending
1523 |> update_info(&User.Info.confirmation_changeset(&1, need_confirmation: need_confirmation?))
1526 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1530 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1531 # use instance-default
1532 config = Pleroma.Config.get([:assets, :mascots])
1533 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1534 mascot = Keyword.get(config, default_mascot)
1537 "id" => "default-mascot",
1538 "url" => mascot[:url],
1539 "preview_url" => mascot[:url],
1541 "mime_type" => mascot[:mime_type]
1546 def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
1548 def ensure_keys_present(%User{} = user) do
1549 with {:ok, pem} <- Keys.generate_rsa_pem() do
1551 |> cast(%{keys: pem}, [:keys])
1552 |> validate_required([:keys])
1553 |> update_and_set_cache()
1557 def get_ap_ids_by_nicknames(nicknames) do
1559 where: u.nickname in ^nicknames,
1565 defdelegate search(query, opts \\ []), to: User.Search
1567 defp put_password_hash(
1568 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1570 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1573 defp put_password_hash(changeset), do: changeset
1575 def is_internal_user?(%User{nickname: nil}), do: true
1576 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1577 def is_internal_user?(_), do: false
1579 # A hack because user delete activities have a fake id for whatever reason
1580 # TODO: Get rid of this
1581 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
1583 def get_delivered_users_by_object_id(object_id) do
1585 inner_join: delivery in assoc(u, :deliveries),
1586 where: delivery.object_id == ^object_id
1591 def change_email(user, email) do
1593 |> cast(%{email: email}, [:email])
1594 |> validate_required([:email])
1595 |> unique_constraint(:email)
1596 |> validate_format(:email, @email_regex)
1597 |> update_and_set_cache()
1601 Changes `user.info` and returns the user changeset.
1603 `fun` is called with the `user.info`.
1605 def change_info(user, fun) do
1606 changeset = change(user)
1607 info = get_field(changeset, :info) || %User.Info{}
1608 put_embed(changeset, :info, fun.(info))
1612 Updates `user.info` and sets cache.
1614 `fun` is called with the `user.info`.
1616 def update_info(users, fun) when is_list(users) do
1617 Repo.transaction(fn ->
1618 for user <- users, do: update_info(user, fun)
1622 def update_info(user, fun) do
1625 |> update_and_set_cache()