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.OStatus
30 alias Pleroma.Web.RelMe
31 alias Pleroma.Workers.BackgroundWorker
35 @type t :: %__MODULE__{}
37 @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
39 # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
40 @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])?)*$/
42 @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
43 @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
47 field(:email, :string)
49 field(:nickname, :string)
50 field(:password_hash, :string)
51 field(:password, :string, virtual: true)
52 field(:password_confirmation, :string, virtual: true)
54 field(:following, {:array, :string}, default: [])
55 field(:ap_id, :string)
57 field(:local, :boolean, default: true)
58 field(:follower_address, :string)
59 field(:following_address, :string)
60 field(:search_rank, :float, virtual: true)
61 field(:search_type, :integer, virtual: true)
62 field(:tags, {:array, :string}, default: [])
63 field(:last_refreshed_at, :naive_datetime_usec)
64 field(:last_digest_emailed_at, :naive_datetime)
65 has_many(:notifications, Notification)
66 has_many(:registrations, Registration)
67 has_many(:deliveries, Delivery)
68 embeds_one(:info, User.Info)
73 def auth_active?(%User{info: %User.Info{confirmation_pending: true}}),
74 do: !Pleroma.Config.get([:instance, :account_activation_required])
76 def auth_active?(%User{}), do: true
78 def visible_for?(user, for_user \\ nil)
80 def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
82 def visible_for?(%User{} = user, for_user) do
83 auth_active?(user) || superuser?(for_user)
86 def visible_for?(_, _), do: false
88 def superuser?(%User{local: true, info: %User.Info{is_admin: true}}), do: true
89 def superuser?(%User{local: true, info: %User.Info{is_moderator: true}}), do: true
90 def superuser?(_), do: false
92 def avatar_url(user, options \\ []) do
94 %{"url" => [%{"href" => href} | _]} -> href
95 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
99 def banner_url(user, options \\ []) do
100 case user.info.banner do
101 %{"url" => [%{"href" => href} | _]} -> href
102 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
106 def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url
107 def profile_url(%User{ap_id: ap_id}), do: ap_id
108 def profile_url(_), do: nil
110 def ap_id(%User{nickname: nickname}), do: "#{Web.base_url()}/users/#{nickname}"
112 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
113 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
115 @spec ap_following(User.t()) :: Sring.t()
116 def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
117 def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
119 def user_info(%User{} = user, args \\ %{}) do
121 Map.get(args, :following_count, user.info.following_count || following_count(user))
123 follower_count = Map.get(args, :follower_count, user.info.follower_count)
126 note_count: user.info.note_count,
127 locked: user.info.locked,
128 confirmation_pending: user.info.confirmation_pending,
129 default_scope: user.info.default_scope
131 |> Map.put(:following_count, following_count)
132 |> Map.put(:follower_count, follower_count)
135 def follow_state(%User{} = user, %User{} = target) do
136 case Utils.fetch_latest_follow(user, target) do
137 %{data: %{"state" => state}} -> state
138 # Ideally this would be nil, but then Cachex does not commit the value
143 def get_cached_follow_state(user, target) do
144 key = "follow_state:#{user.ap_id}|#{target.ap_id}"
145 Cachex.fetch!(:user_cache, key, fn _ -> {:commit, follow_state(user, target)} end)
148 @spec set_follow_state_cache(String.t(), String.t(), String.t()) :: {:ok | :error, boolean()}
149 def set_follow_state_cache(user_ap_id, target_ap_id, state) do
150 Cachex.put(:user_cache, "follow_state:#{user_ap_id}|#{target_ap_id}", state)
153 def set_info_cache(user, args) do
154 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user, args))
157 @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
158 def restrict_deactivated(query) do
160 where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info)
164 def following_count(%User{following: []}), do: 0
166 def following_count(%User{} = user) do
168 |> get_friends_query()
169 |> Repo.aggregate(:count, :id)
172 defp truncate_if_exists(params, key, max_length) do
173 if Map.has_key?(params, key) and is_binary(params[key]) do
174 {value, _chopped} = String.split_at(params[key], max_length)
175 Map.put(params, key, value)
181 def remote_user_creation(params) do
182 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
183 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
187 |> Map.put(:info, params[:info] || %{})
188 |> truncate_if_exists(:name, name_limit)
189 |> truncate_if_exists(:bio, bio_limit)
193 |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
194 |> validate_required([:name, :ap_id])
195 |> unique_constraint(:nickname)
196 |> validate_format(:nickname, @email_regex)
197 |> validate_length(:bio, max: bio_limit)
198 |> validate_length(:name, max: name_limit)
199 |> change_info(&User.Info.remote_user_creation(&1, params[:info]))
201 case params[:info][:source_data] do
202 %{"followers" => followers, "following" => following} ->
204 |> put_change(:follower_address, followers)
205 |> put_change(:following_address, following)
208 followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
209 put_change(changeset, :follower_address, followers)
213 def update_changeset(struct, params \\ %{}) do
214 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
215 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
218 |> cast(params, [:bio, :name, :avatar, :following])
219 |> unique_constraint(:nickname)
220 |> validate_format(:nickname, local_nickname_regex())
221 |> validate_length(:bio, max: bio_limit)
222 |> validate_length(:name, min: 1, max: name_limit)
225 def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
226 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
227 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
229 params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
240 |> unique_constraint(:nickname)
241 |> validate_format(:nickname, local_nickname_regex())
242 |> validate_length(:bio, max: bio_limit)
243 |> validate_length(:name, max: name_limit)
244 |> change_info(&User.Info.user_upgrade(&1, params[:info], remote?))
247 def password_update_changeset(struct, params) do
249 |> cast(params, [:password, :password_confirmation])
250 |> validate_required([:password, :password_confirmation])
251 |> validate_confirmation(:password)
253 |> put_embed(:info, User.Info.set_password_reset_pending(struct.info, false))
256 @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
257 def reset_password(%User{id: user_id} = user, data) do
260 |> Multi.update(:user, password_update_changeset(user, data))
261 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
262 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
264 case Repo.transaction(multi) do
265 {:ok, %{user: user} = _} -> set_cache(user)
266 {:error, _, changeset, _} -> {:error, changeset}
270 def force_password_reset_async(user) do
271 BackgroundWorker.enqueue("force_password_reset", %{"user_id" => user.id})
274 @spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
275 def force_password_reset(user) do
276 info_cng = User.Info.set_password_reset_pending(user.info, true)
280 |> put_embed(:info, info_cng)
281 |> update_and_set_cache()
284 def register_changeset(struct, params \\ %{}, opts \\ []) do
285 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
286 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
289 if is_nil(opts[:need_confirmation]) do
290 Pleroma.Config.get([:instance, :account_activation_required])
292 opts[:need_confirmation]
296 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
297 |> validate_required([:name, :nickname, :password, :password_confirmation])
298 |> validate_confirmation(:password)
299 |> unique_constraint(:email)
300 |> unique_constraint(:nickname)
301 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
302 |> validate_format(:nickname, local_nickname_regex())
303 |> validate_format(:email, @email_regex)
304 |> validate_length(:bio, max: bio_limit)
305 |> validate_length(:name, min: 1, max: name_limit)
306 |> change_info(&User.Info.confirmation_changeset(&1, need_confirmation: need_confirmation?))
307 |> maybe_validate_required_email(opts[:external])
310 |> unique_constraint(:ap_id)
311 |> put_following_and_follower_address()
314 def maybe_validate_required_email(changeset, true), do: changeset
315 def maybe_validate_required_email(changeset, _), do: validate_required(changeset, [:email])
317 defp put_ap_id(changeset) do
318 ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
319 put_change(changeset, :ap_id, ap_id)
322 defp put_following_and_follower_address(changeset) do
323 followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
326 |> put_change(:following, [followers])
327 |> put_change(:follower_address, followers)
330 defp autofollow_users(user) do
331 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
334 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
337 follow_all(user, autofollowed_users)
340 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
341 def register(%Ecto.Changeset{} = changeset) do
342 with {:ok, user} <- Repo.insert(changeset) do
343 post_register_action(user)
347 def post_register_action(%User{} = user) do
348 with {:ok, user} <- autofollow_users(user),
349 {:ok, user} <- set_cache(user),
350 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
351 {:ok, _} <- try_send_confirmation_email(user) do
356 def try_send_confirmation_email(%User{} = user) do
357 if user.info.confirmation_pending &&
358 Pleroma.Config.get([:instance, :account_activation_required]) do
360 |> Pleroma.Emails.UserEmail.account_confirmation_email()
361 |> Pleroma.Emails.Mailer.deliver_async()
369 def needs_update?(%User{local: true}), do: false
371 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
373 def needs_update?(%User{local: false} = user) do
374 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
377 def needs_update?(_), do: true
379 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
380 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
384 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
385 follow(follower, followed)
388 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
389 if not ap_enabled?(followed) do
390 follow(follower, followed)
396 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
397 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
398 def follow_all(follower, followeds) do
401 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
402 |> Enum.map(fn %{follower_address: fa} -> fa end)
406 where: u.id == ^follower.id,
411 "array(select distinct unnest (array_cat(?, ?)))",
420 {1, [follower]} = Repo.update_all(q, [])
422 Enum.each(followeds, &update_follower_count/1)
427 def follow(%User{} = follower, %User{info: info} = followed) do
428 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
429 ap_followers = followed.follower_address
433 {:error, "Could not follow user: You are deactivated."}
435 deny_follow_blocked and blocks?(followed, follower) ->
436 {:error, "Could not follow user: #{followed.nickname} blocked you."}
441 where: u.id == ^follower.id,
442 update: [push: [following: ^ap_followers]],
446 {1, [follower]} = Repo.update_all(q, [])
448 follower = maybe_update_following_count(follower)
450 {:ok, _} = update_follower_count(followed)
456 def unfollow(%User{} = follower, %User{} = followed) do
457 ap_followers = followed.follower_address
459 if following?(follower, followed) and follower.ap_id != followed.ap_id do
462 where: u.id == ^follower.id,
463 update: [pull: [following: ^ap_followers]],
467 {1, [follower]} = Repo.update_all(q, [])
469 follower = maybe_update_following_count(follower)
471 {:ok, followed} = update_follower_count(followed)
475 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
477 {:error, "Not subscribed!"}
481 @spec following?(User.t(), User.t()) :: boolean
482 def following?(%User{} = follower, %User{} = followed) do
483 Enum.member?(follower.following, followed.follower_address)
486 def locked?(%User{} = user) do
487 user.info.locked || false
491 Repo.get_by(User, id: id)
494 def get_by_ap_id(ap_id) do
495 Repo.get_by(User, ap_id: ap_id)
498 def get_all_by_ap_id(ap_ids) do
499 from(u in __MODULE__,
500 where: u.ap_id in ^ap_ids
505 def get_all_by_ids(ids) do
506 from(u in __MODULE__, where: u.id in ^ids)
510 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
511 # of the ap_id and the domain and tries to get that user
512 def get_by_guessed_nickname(ap_id) do
513 domain = URI.parse(ap_id).host
514 name = List.last(String.split(ap_id, "/"))
515 nickname = "#{name}@#{domain}"
517 get_cached_by_nickname(nickname)
520 def set_cache({:ok, user}), do: set_cache(user)
521 def set_cache({:error, err}), do: {:error, err}
523 def set_cache(%User{} = user) do
524 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
525 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
526 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
530 def update_and_set_cache(changeset) do
531 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
536 def invalidate_cache(user) do
537 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
538 Cachex.del(:user_cache, "nickname:#{user.nickname}")
539 Cachex.del(:user_cache, "user_info:#{user.id}")
542 def get_cached_by_ap_id(ap_id) do
543 key = "ap_id:#{ap_id}"
544 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
547 def get_cached_by_id(id) do
551 Cachex.fetch!(:user_cache, key, fn _ ->
555 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
556 {:commit, user.ap_id}
562 get_cached_by_ap_id(ap_id)
565 def get_cached_by_nickname(nickname) do
566 key = "nickname:#{nickname}"
568 Cachex.fetch!(:user_cache, key, fn ->
569 case get_or_fetch_by_nickname(nickname) do
570 {:ok, user} -> {:commit, user}
571 {:error, _error} -> {:ignore, nil}
576 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
577 restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
580 is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
581 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
583 restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
584 get_cached_by_nickname(nickname_or_id)
586 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
587 get_cached_by_nickname(nickname_or_id)
594 def get_by_nickname(nickname) do
595 Repo.get_by(User, nickname: nickname) ||
596 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
597 Repo.get_by(User, nickname: local_nickname(nickname))
601 def get_by_email(email), do: Repo.get_by(User, email: email)
603 def get_by_nickname_or_email(nickname_or_email) do
604 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
607 def get_cached_user_info(user) do
608 key = "user_info:#{user.id}"
609 Cachex.fetch!(:user_cache, key, fn -> user_info(user) end)
612 def fetch_by_nickname(nickname) do
613 case ActivityPub.make_user_from_nickname(nickname) do
614 {:ok, user} -> {:ok, user}
615 _ -> OStatus.make_user(nickname)
619 def get_or_fetch_by_nickname(nickname) do
620 with %User{} = user <- get_by_nickname(nickname) do
624 with [_nick, _domain] <- String.split(nickname, "@"),
625 {:ok, user} <- fetch_by_nickname(nickname) do
626 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
627 fetch_initial_posts(user)
632 _e -> {:error, "not found " <> nickname}
637 @doc "Fetch some posts when the user has just been federated with"
638 def fetch_initial_posts(user) do
639 BackgroundWorker.enqueue("fetch_initial_posts", %{"user_id" => user.id})
642 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
643 def get_followers_query(%User{} = user, nil) do
644 User.Query.build(%{followers: user, deactivated: false})
647 def get_followers_query(user, page) do
649 |> get_followers_query(nil)
650 |> User.Query.paginate(page, 20)
653 @spec get_followers_query(User.t()) :: Ecto.Query.t()
654 def get_followers_query(user), do: get_followers_query(user, nil)
656 @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
657 def get_followers(user, page \\ nil) do
659 |> get_followers_query(page)
663 @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
664 def get_external_followers(user, page \\ nil) do
666 |> get_followers_query(page)
667 |> User.Query.build(%{external: true})
671 def get_followers_ids(user, page \\ nil) do
673 |> get_followers_query(page)
678 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
679 def get_friends_query(%User{} = user, nil) do
680 User.Query.build(%{friends: user, deactivated: false})
683 def get_friends_query(user, page) do
685 |> get_friends_query(nil)
686 |> User.Query.paginate(page, 20)
689 @spec get_friends_query(User.t()) :: Ecto.Query.t()
690 def get_friends_query(user), do: get_friends_query(user, nil)
692 def get_friends(user, page \\ nil) do
694 |> get_friends_query(page)
698 def get_friends_ids(user, page \\ nil) do
700 |> get_friends_query(page)
705 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
706 def get_follow_requests(%User{} = user) do
708 |> Activity.follow_requests_for_actor()
709 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
710 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
711 |> group_by([a, u], u.id)
716 def increase_note_count(%User{} = user) do
718 |> where(id: ^user.id)
723 "safe_jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
730 |> Repo.update_all([])
732 {1, [user]} -> set_cache(user)
737 def decrease_note_count(%User{} = user) do
739 |> where(id: ^user.id)
744 "safe_jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
751 |> Repo.update_all([])
753 {1, [user]} -> set_cache(user)
758 def update_note_count(%User{} = user) do
762 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
767 update_info(user, &User.Info.set_note_count(&1, note_count))
770 def update_mascot(user, url) do
772 User.Info.mascot_update(
779 |> put_embed(:info, info_changeset)
780 |> update_and_set_cache()
783 @spec maybe_fetch_follow_information(User.t()) :: User.t()
784 def maybe_fetch_follow_information(user) do
785 with {:ok, user} <- fetch_follow_information(user) do
789 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
795 def fetch_follow_information(user) do
796 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
797 update_info(user, &User.Info.follow_information_update(&1, info))
801 def update_follower_count(%User{} = user) do
802 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
803 follower_count_query =
804 User.Query.build(%{followers: user, deactivated: false})
805 |> select([u], %{count: count(u.id)})
808 |> where(id: ^user.id)
809 |> join(:inner, [u], s in subquery(follower_count_query))
814 "safe_jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
821 |> Repo.update_all([])
823 {1, [user]} -> set_cache(user)
827 {:ok, maybe_fetch_follow_information(user)}
831 @spec maybe_update_following_count(User.t()) :: User.t()
832 def maybe_update_following_count(%User{local: false} = user) do
833 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
834 maybe_fetch_follow_information(user)
840 def maybe_update_following_count(user), do: user
842 def set_unread_conversation_count(%User{local: true} = user) do
843 unread_query = Participation.unread_conversation_count_for_user(user)
846 |> join(:inner, [u], p in subquery(unread_query))
851 "jsonb_set(?, '{unread_conversation_count}', ?::varchar::jsonb, true)",
857 |> where([u], u.id == ^user.id)
859 |> Repo.update_all([])
861 {1, [user]} -> set_cache(user)
866 def set_unread_conversation_count(_), do: :noop
868 def increment_unread_conversation_count(conversation, %User{local: true} = user) do
870 Participation.unread_conversation_count_for_user(user)
871 |> where([p], p.conversation_id == ^conversation.id)
874 |> join(:inner, [u], p in subquery(unread_query))
879 "jsonb_set(?, '{unread_conversation_count}', (coalesce((?->>'unread_conversation_count')::int, 0) + 1)::varchar::jsonb, true)",
885 |> where([u], u.id == ^user.id)
886 |> where([u, p], p.count == 0)
888 |> Repo.update_all([])
890 {1, [user]} -> set_cache(user)
895 def increment_unread_conversation_count(_, _), do: :noop
897 def remove_duplicated_following(%User{following: following} = user) do
898 uniq_following = Enum.uniq(following)
900 if length(following) == length(uniq_following) do
904 |> update_changeset(%{following: uniq_following})
905 |> update_and_set_cache()
909 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
910 def get_users_from_set(ap_ids, local_only \\ true) do
911 criteria = %{ap_id: ap_ids, deactivated: false}
912 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
914 User.Query.build(criteria)
918 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
919 def get_recipients_from_activity(%Activity{recipients: to}) do
920 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
924 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
925 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
926 update_info(muter, &User.Info.add_to_mutes(&1, ap_id, notifications?))
929 def unmute(muter, %{ap_id: ap_id}) do
930 update_info(muter, &User.Info.remove_from_mutes(&1, ap_id))
933 def subscribe(subscriber, %{ap_id: ap_id}) do
934 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
935 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
937 if blocks?(subscribed, subscriber) and deny_follow_blocked do
938 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
940 update_info(subscribed, &User.Info.add_to_subscribers(&1, subscriber.ap_id))
945 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
946 with %User{} = user <- get_cached_by_ap_id(ap_id) do
947 update_info(user, &User.Info.remove_from_subscribers(&1, unsubscriber.ap_id))
951 def block(blocker, %User{ap_id: ap_id} = blocked) do
952 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
954 if following?(blocker, blocked) do
955 {:ok, blocker, _} = unfollow(blocker, blocked)
961 # clear any requested follows as well
963 case CommonAPI.reject_follow_request(blocked, blocker) do
964 {:ok, %User{} = updated_blocked} -> updated_blocked
969 if subscribed_to?(blocked, blocker) do
970 {:ok, blocker} = unsubscribe(blocked, blocker)
976 if following?(blocked, blocker), do: unfollow(blocked, blocker)
978 {:ok, blocker} = update_follower_count(blocker)
980 update_info(blocker, &User.Info.add_to_block(&1, ap_id))
983 # helper to handle the block given only an actor's AP id
984 def block(blocker, %{ap_id: ap_id}) do
985 block(blocker, get_cached_by_ap_id(ap_id))
988 def unblock(blocker, %{ap_id: ap_id}) do
989 update_info(blocker, &User.Info.remove_from_block(&1, ap_id))
992 def mutes?(nil, _), do: false
993 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
995 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
996 def muted_notifications?(nil, _), do: false
998 def muted_notifications?(user, %{ap_id: ap_id}),
999 do: Enum.member?(user.info.muted_notifications, ap_id)
1001 def blocks?(%User{} = user, %User{} = target) do
1002 blocks_ap_id?(user, target) || blocks_domain?(user, target)
1005 def blocks?(nil, _), do: false
1007 def blocks_ap_id?(%User{} = user, %User{} = target) do
1008 Enum.member?(user.info.blocks, target.ap_id)
1011 def blocks_ap_id?(_, _), do: false
1013 def blocks_domain?(%User{} = user, %User{} = target) do
1014 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
1015 %{host: host} = URI.parse(target.ap_id)
1016 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1019 def blocks_domain?(_, _), do: false
1021 def subscribed_to?(user, %{ap_id: ap_id}) do
1022 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1023 Enum.member?(target.info.subscribers, user.ap_id)
1027 @spec muted_users(User.t()) :: [User.t()]
1028 def muted_users(user) do
1029 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
1033 @spec blocked_users(User.t()) :: [User.t()]
1034 def blocked_users(user) do
1035 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
1039 @spec subscribers(User.t()) :: [User.t()]
1040 def subscribers(user) do
1041 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
1045 def block_domain(user, domain) do
1046 update_info(user, &User.Info.add_to_domain_block(&1, domain))
1049 def unblock_domain(user, domain) do
1050 update_info(user, &User.Info.remove_from_domain_block(&1, domain))
1053 def deactivate_async(user, status \\ true) do
1054 BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
1057 def deactivate(user, status \\ true)
1059 def deactivate(users, status) when is_list(users) do
1060 Repo.transaction(fn ->
1061 for user <- users, do: deactivate(user, status)
1065 def deactivate(%User{} = user, status) do
1066 with {:ok, user} <- update_info(user, &User.Info.set_activation_status(&1, status)) do
1067 Enum.each(get_followers(user), &invalidate_cache/1)
1068 Enum.each(get_friends(user), &update_follower_count/1)
1074 def update_notification_settings(%User{} = user, settings \\ %{}) do
1075 update_info(user, &User.Info.update_notification_settings(&1, settings))
1078 def delete(users) when is_list(users) do
1079 for user <- users, do: delete(user)
1082 def delete(%User{} = user) do
1083 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1086 def perform(:force_password_reset, user), do: force_password_reset(user)
1088 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1089 def perform(:delete, %User{} = user) do
1090 {:ok, _user} = ActivityPub.delete(user)
1092 # Remove all relationships
1095 |> Enum.each(fn follower ->
1096 ActivityPub.unfollow(follower, user)
1097 unfollow(follower, user)
1102 |> Enum.each(fn followed ->
1103 ActivityPub.unfollow(user, followed)
1104 unfollow(user, followed)
1107 delete_user_activities(user)
1108 invalidate_cache(user)
1112 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1113 def perform(:fetch_initial_posts, %User{} = user) do
1114 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1116 # Insert all the posts in reverse order, so they're in the right order on the timeline
1117 user.info.source_data["outbox"]
1118 |> Utils.fetch_ordered_collection(pages)
1120 |> Enum.each(&Pleroma.Web.Federator.incoming_ap_doc/1)
1123 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1125 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1126 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1127 when is_list(blocked_identifiers) do
1129 blocked_identifiers,
1130 fn blocked_identifier ->
1131 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1132 {:ok, blocker} <- block(blocker, blocked),
1133 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1137 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1144 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1145 def perform(:follow_import, %User{} = follower, followed_identifiers)
1146 when is_list(followed_identifiers) do
1148 followed_identifiers,
1149 fn followed_identifier ->
1150 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1151 {:ok, follower} <- maybe_direct_follow(follower, followed),
1152 {:ok, _} <- ActivityPub.follow(follower, followed) do
1156 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1163 @spec external_users_query() :: Ecto.Query.t()
1164 def external_users_query do
1172 @spec external_users(keyword()) :: [User.t()]
1173 def external_users(opts \\ []) do
1175 external_users_query()
1176 |> select([u], struct(u, [:id, :ap_id, :info]))
1180 do: where(query, [u], u.id > ^opts[:max_id]),
1185 do: limit(query, ^opts[:limit]),
1191 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1192 BackgroundWorker.enqueue("blocks_import", %{
1193 "blocker_id" => blocker.id,
1194 "blocked_identifiers" => blocked_identifiers
1198 def follow_import(%User{} = follower, followed_identifiers)
1199 when is_list(followed_identifiers) do
1200 BackgroundWorker.enqueue("follow_import", %{
1201 "follower_id" => follower.id,
1202 "followed_identifiers" => followed_identifiers
1206 def delete_user_activities(%User{ap_id: ap_id}) do
1208 |> Activity.Queries.by_actor()
1209 |> RepoStreamer.chunk_stream(50)
1210 |> Stream.each(fn activities -> Enum.each(activities, &delete_activity/1) end)
1214 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1216 |> Object.normalize()
1217 |> ActivityPub.delete()
1220 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1221 object = Object.normalize(activity)
1224 |> get_cached_by_ap_id()
1225 |> ActivityPub.unlike(object)
1228 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1229 object = Object.normalize(activity)
1232 |> get_cached_by_ap_id()
1233 |> ActivityPub.unannounce(object)
1236 defp delete_activity(_activity), do: "Doing nothing"
1238 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1239 Pleroma.HTML.Scrubber.TwitterText
1242 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1244 def fetch_by_ap_id(ap_id) do
1245 case ActivityPub.make_user_from_ap_id(ap_id) do
1250 case OStatus.make_user(ap_id) do
1251 {:ok, user} -> {:ok, user}
1252 _ -> {:error, "Could not fetch by AP id"}
1257 def get_or_fetch_by_ap_id(ap_id) do
1258 user = get_cached_by_ap_id(ap_id)
1260 if !is_nil(user) and !needs_update?(user) do
1263 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1264 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1266 resp = fetch_by_ap_id(ap_id)
1268 if should_fetch_initial do
1269 with {:ok, %User{} = user} <- resp do
1270 fetch_initial_posts(user)
1278 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1279 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1280 with %User{} = user <- get_cached_by_ap_id(uri) do
1285 %User{info: %User.Info{}}
1286 |> cast(%{}, [:ap_id, :nickname, :local])
1287 |> put_change(:ap_id, uri)
1288 |> put_change(:nickname, nickname)
1289 |> put_change(:local, true)
1290 |> put_change(:follower_address, uri <> "/followers")
1298 def public_key_from_info(%{
1299 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1303 |> :public_key.pem_decode()
1305 |> :public_key.pem_entry_decode()
1311 def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
1312 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1315 def public_key_from_info(_), do: {:error, "not found key"}
1317 def get_public_key_for_ap_id(ap_id) do
1318 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1319 {:ok, public_key} <- public_key_from_info(user.info) do
1326 defp blank?(""), do: nil
1327 defp blank?(n), do: n
1329 def insert_or_update_user(data) do
1331 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1332 |> remote_user_creation()
1333 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1337 def ap_enabled?(%User{local: true}), do: true
1338 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1339 def ap_enabled?(_), do: false
1341 @doc "Gets or fetch a user by uri or nickname."
1342 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1343 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1344 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1346 # wait a period of time and return newest version of the User structs
1347 # this is because we have synchronous follow APIs and need to simulate them
1348 # with an async handshake
1349 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1350 with %User{} = a <- get_cached_by_id(a.id),
1351 %User{} = b <- get_cached_by_id(b.id) do
1358 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1359 with :ok <- :timer.sleep(timeout),
1360 %User{} = a <- get_cached_by_id(a.id),
1361 %User{} = b <- get_cached_by_id(b.id) do
1368 def parse_bio(bio) when is_binary(bio) and bio != "" do
1370 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1374 def parse_bio(_), do: ""
1376 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1377 # TODO: get profile URLs other than user.ap_id
1378 profile_urls = [user.ap_id]
1381 |> CommonUtils.format_input("text/plain",
1382 mentions_format: :full,
1383 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1388 def parse_bio(_, _), do: ""
1390 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1391 Repo.transaction(fn ->
1392 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1396 def tag(nickname, tags) when is_binary(nickname),
1397 do: tag(get_by_nickname(nickname), tags)
1399 def tag(%User{} = user, tags),
1400 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1402 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1403 Repo.transaction(fn ->
1404 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1408 def untag(nickname, tags) when is_binary(nickname),
1409 do: untag(get_by_nickname(nickname), tags)
1411 def untag(%User{} = user, tags),
1412 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1414 defp update_tags(%User{} = user, new_tags) do
1415 {:ok, updated_user} =
1417 |> change(%{tags: new_tags})
1418 |> update_and_set_cache()
1423 defp normalize_tags(tags) do
1426 |> Enum.map(&String.downcase/1)
1429 defp local_nickname_regex do
1430 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1431 @extended_local_nickname_regex
1433 @strict_local_nickname_regex
1437 def local_nickname(nickname_or_mention) do
1440 |> String.split("@")
1444 def full_nickname(nickname_or_mention),
1445 do: String.trim_leading(nickname_or_mention, "@")
1447 def error_user(ap_id) do
1452 nickname: "erroruser@example.com",
1453 inserted_at: NaiveDateTime.utc_now()
1457 @spec all_superusers() :: [User.t()]
1458 def all_superusers do
1459 User.Query.build(%{super_users: true, local: true, deactivated: false})
1463 def showing_reblogs?(%User{} = user, %User{} = target) do
1464 target.ap_id not in user.info.muted_reblogs
1468 The function returns a query to get users with no activity for given interval of days.
1469 Inactive users are those who didn't read any notification, or had any activity where
1470 the user is the activity's actor, during `inactivity_threshold` days.
1471 Deactivated users will not appear in this list.
1475 iex> Pleroma.User.list_inactive_users()
1478 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1479 def list_inactive_users_query(inactivity_threshold \\ 7) do
1480 negative_inactivity_threshold = -inactivity_threshold
1481 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1482 # Subqueries are not supported in `where` clauses, join gets too complicated.
1483 has_read_notifications =
1484 from(n in Pleroma.Notification,
1485 where: n.seen == true,
1487 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1490 |> Pleroma.Repo.all()
1492 from(u in Pleroma.User,
1493 left_join: a in Pleroma.Activity,
1494 on: u.ap_id == a.actor,
1495 where: not is_nil(u.nickname),
1496 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1497 where: u.id not in ^has_read_notifications,
1500 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1501 is_nil(max(a.inserted_at))
1506 Enable or disable email notifications for user
1510 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1511 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1513 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1514 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1516 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1517 {:ok, t()} | {:error, Ecto.Changeset.t()}
1518 def switch_email_notifications(user, type, status) do
1519 update_info(user, &User.Info.update_email_notifications(&1, %{type => status}))
1523 Set `last_digest_emailed_at` value for the user to current time
1525 @spec touch_last_digest_emailed_at(t()) :: t()
1526 def touch_last_digest_emailed_at(user) do
1527 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1529 {:ok, updated_user} =
1531 |> change(%{last_digest_emailed_at: now})
1532 |> update_and_set_cache()
1537 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1538 def toggle_confirmation(%User{} = user) do
1539 need_confirmation? = !user.info.confirmation_pending
1542 |> update_info(&User.Info.confirmation_changeset(&1, need_confirmation: need_confirmation?))
1545 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1549 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1550 # use instance-default
1551 config = Pleroma.Config.get([:assets, :mascots])
1552 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1553 mascot = Keyword.get(config, default_mascot)
1556 "id" => "default-mascot",
1557 "url" => mascot[:url],
1558 "preview_url" => mascot[:url],
1560 "mime_type" => mascot[:mime_type]
1565 def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
1567 def ensure_keys_present(%User{} = user) do
1568 with {:ok, pem} <- Keys.generate_rsa_pem() do
1570 |> cast(%{keys: pem}, [:keys])
1571 |> validate_required([:keys])
1572 |> update_and_set_cache()
1576 def get_ap_ids_by_nicknames(nicknames) do
1578 where: u.nickname in ^nicknames,
1584 defdelegate search(query, opts \\ []), to: User.Search
1586 defp put_password_hash(
1587 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1589 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1592 defp put_password_hash(changeset), do: changeset
1594 def is_internal_user?(%User{nickname: nil}), do: true
1595 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1596 def is_internal_user?(_), do: false
1598 # A hack because user delete activities have a fake id for whatever reason
1599 # TODO: Get rid of this
1600 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
1602 def get_delivered_users_by_object_id(object_id) do
1604 inner_join: delivery in assoc(u, :deliveries),
1605 where: delivery.object_id == ^object_id
1610 def change_email(user, email) do
1612 |> cast(%{email: email}, [:email])
1613 |> validate_required([:email])
1614 |> unique_constraint(:email)
1615 |> validate_format(:email, @email_regex)
1616 |> update_and_set_cache()
1620 Changes `user.info` and returns the user changeset.
1622 `fun` is called with the `user.info`.
1624 def change_info(user, fun) do
1625 changeset = change(user)
1626 info = get_field(changeset, :info) || %User.Info{}
1627 put_embed(changeset, :info, fun.(info))
1631 Updates `user.info` and sets cache.
1633 `fun` is called with the `user.info`.
1635 def update_info(users, fun) when is_list(users) do
1636 Repo.transaction(fn ->
1637 for user <- users, do: update_info(user, fun)
1641 def update_info(user, fun) do
1644 |> update_and_set_cache()