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.Web.Websub
32 alias Pleroma.Workers.BackgroundWorker
36 @type t :: %__MODULE__{}
38 @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
40 # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
41 @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])?)*$/
43 @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
44 @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
48 field(:email, :string)
50 field(:nickname, :string)
51 field(:password_hash, :string)
52 field(:password, :string, virtual: true)
53 field(:password_confirmation, :string, virtual: true)
55 field(:following, {:array, :string}, default: [])
56 field(:ap_id, :string)
58 field(:local, :boolean, default: true)
59 field(:follower_address, :string)
60 field(:following_address, :string)
61 field(:search_rank, :float, virtual: true)
62 field(:search_type, :integer, virtual: true)
63 field(:tags, {:array, :string}, default: [])
64 field(:last_refreshed_at, :naive_datetime_usec)
65 field(:last_digest_emailed_at, :naive_datetime)
66 has_many(:notifications, Notification)
67 has_many(:registrations, Registration)
68 has_many(:deliveries, Delivery)
69 embeds_one(:info, User.Info)
74 def auth_active?(%User{info: %User.Info{confirmation_pending: true}}),
75 do: !Pleroma.Config.get([:instance, :account_activation_required])
77 def auth_active?(%User{}), do: true
79 def visible_for?(user, for_user \\ nil)
81 def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
83 def visible_for?(%User{} = user, for_user) do
84 auth_active?(user) || superuser?(for_user)
87 def visible_for?(_, _), do: false
89 def superuser?(%User{local: true, info: %User.Info{is_admin: true}}), do: true
90 def superuser?(%User{local: true, info: %User.Info{is_moderator: true}}), do: true
91 def superuser?(_), do: false
93 def avatar_url(user, options \\ []) do
95 %{"url" => [%{"href" => href} | _]} -> href
96 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
100 def banner_url(user, options \\ []) do
101 case user.info.banner do
102 %{"url" => [%{"href" => href} | _]} -> href
103 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
107 def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url
108 def profile_url(%User{ap_id: ap_id}), do: ap_id
109 def profile_url(_), do: nil
111 def ap_id(%User{nickname: nickname}), do: "#{Web.base_url()}/users/#{nickname}"
113 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
114 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
116 @spec ap_following(User.t()) :: Sring.t()
117 def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
118 def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
120 def user_info(%User{} = user, args \\ %{}) do
122 Map.get(args, :following_count, user.info.following_count || following_count(user))
124 follower_count = Map.get(args, :follower_count, user.info.follower_count)
127 note_count: user.info.note_count,
128 locked: user.info.locked,
129 confirmation_pending: user.info.confirmation_pending,
130 default_scope: user.info.default_scope
132 |> Map.put(:following_count, following_count)
133 |> Map.put(:follower_count, follower_count)
136 def follow_state(%User{} = user, %User{} = target) do
137 case Utils.fetch_latest_follow(user, target) do
138 %{data: %{"state" => state}} -> state
139 # Ideally this would be nil, but then Cachex does not commit the value
144 def get_cached_follow_state(user, target) do
145 key = "follow_state:#{user.ap_id}|#{target.ap_id}"
146 Cachex.fetch!(:user_cache, key, fn _ -> {:commit, follow_state(user, target)} end)
149 @spec set_follow_state_cache(String.t(), String.t(), String.t()) :: {:ok | :error, boolean()}
150 def set_follow_state_cache(user_ap_id, target_ap_id, state) do
151 Cachex.put(:user_cache, "follow_state:#{user_ap_id}|#{target_ap_id}", state)
154 def set_info_cache(user, args) do
155 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user, args))
158 @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
159 def restrict_deactivated(query) do
161 where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info)
165 def following_count(%User{following: []}), do: 0
167 def following_count(%User{} = user) do
169 |> get_friends_query()
170 |> Repo.aggregate(:count, :id)
173 defp truncate_if_exists(params, key, max_length) do
174 if Map.has_key?(params, key) and is_binary(params[key]) do
175 {value, _chopped} = String.split_at(params[key], max_length)
176 Map.put(params, key, value)
182 def remote_user_creation(params) do
183 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
184 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
188 |> Map.put(:info, params[:info] || %{})
189 |> truncate_if_exists(:name, name_limit)
190 |> truncate_if_exists(:bio, bio_limit)
194 |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
195 |> validate_required([:name, :ap_id])
196 |> unique_constraint(:nickname)
197 |> validate_format(:nickname, @email_regex)
198 |> validate_length(:bio, max: bio_limit)
199 |> validate_length(:name, max: name_limit)
200 |> change_info(&User.Info.remote_user_creation(&1, params[:info]))
202 case params[:info][:source_data] do
203 %{"followers" => followers, "following" => following} ->
205 |> put_change(:follower_address, followers)
206 |> put_change(:following_address, following)
209 followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
210 put_change(changeset, :follower_address, followers)
214 def update_changeset(struct, params \\ %{}) do
215 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
216 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
219 |> cast(params, [:bio, :name, :avatar, :following])
220 |> unique_constraint(:nickname)
221 |> validate_format(:nickname, local_nickname_regex())
222 |> validate_length(:bio, max: bio_limit)
223 |> validate_length(:name, min: 1, max: name_limit)
226 def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
227 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
228 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
230 params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
241 |> unique_constraint(:nickname)
242 |> validate_format(:nickname, local_nickname_regex())
243 |> validate_length(:bio, max: bio_limit)
244 |> validate_length(:name, max: name_limit)
245 |> change_info(&User.Info.user_upgrade(&1, params[:info], remote?))
248 def password_update_changeset(struct, params) do
250 |> cast(params, [:password, :password_confirmation])
251 |> validate_required([:password, :password_confirmation])
252 |> validate_confirmation(:password)
254 |> put_embed(:info, User.Info.set_password_reset_pending(struct.info, false))
257 @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
258 def reset_password(%User{id: user_id} = user, data) do
261 |> Multi.update(:user, password_update_changeset(user, data))
262 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
263 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
265 case Repo.transaction(multi) do
266 {:ok, %{user: user} = _} -> set_cache(user)
267 {:error, _, changeset, _} -> {:error, changeset}
271 def force_password_reset_async(user) do
272 BackgroundWorker.enqueue("force_password_reset", %{"user_id" => user.id})
275 @spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
276 def force_password_reset(user) do
277 info_cng = User.Info.set_password_reset_pending(user.info, true)
281 |> put_embed(:info, info_cng)
282 |> update_and_set_cache()
285 def register_changeset(struct, params \\ %{}, opts \\ []) do
286 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
287 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
290 if is_nil(opts[:need_confirmation]) do
291 Pleroma.Config.get([:instance, :account_activation_required])
293 opts[:need_confirmation]
297 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
298 |> validate_required([:name, :nickname, :password, :password_confirmation])
299 |> validate_confirmation(:password)
300 |> unique_constraint(:email)
301 |> unique_constraint(:nickname)
302 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
303 |> validate_format(:nickname, local_nickname_regex())
304 |> validate_format(:email, @email_regex)
305 |> validate_length(:bio, max: bio_limit)
306 |> validate_length(:name, min: 1, max: name_limit)
307 |> change_info(&User.Info.confirmation_changeset(&1, need_confirmation: need_confirmation?))
308 |> maybe_validate_required_email(opts[:external])
311 |> unique_constraint(:ap_id)
312 |> put_following_and_follower_address()
315 def maybe_validate_required_email(changeset, true), do: changeset
316 def maybe_validate_required_email(changeset, _), do: validate_required(changeset, [:email])
318 defp put_ap_id(changeset) do
319 ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
320 put_change(changeset, :ap_id, ap_id)
323 defp put_following_and_follower_address(changeset) do
324 followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
327 |> put_change(:following, [followers])
328 |> put_change(:follower_address, followers)
331 defp autofollow_users(user) do
332 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
335 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
338 follow_all(user, autofollowed_users)
341 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
342 def register(%Ecto.Changeset{} = changeset) do
343 with {:ok, user} <- Repo.insert(changeset) do
344 post_register_action(user)
348 def post_register_action(%User{} = user) do
349 with {:ok, user} <- autofollow_users(user),
350 {:ok, user} <- set_cache(user),
351 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
352 {:ok, _} <- try_send_confirmation_email(user) do
357 def try_send_confirmation_email(%User{} = user) do
358 if user.info.confirmation_pending &&
359 Pleroma.Config.get([:instance, :account_activation_required]) do
361 |> Pleroma.Emails.UserEmail.account_confirmation_email()
362 |> Pleroma.Emails.Mailer.deliver_async()
370 def needs_update?(%User{local: true}), do: false
372 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
374 def needs_update?(%User{local: false} = user) do
375 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
378 def needs_update?(_), do: true
380 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
381 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
385 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
386 follow(follower, followed)
389 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
390 if not ap_enabled?(followed) do
391 follow(follower, followed)
397 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
398 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
399 def follow_all(follower, followeds) do
402 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
403 |> Enum.map(fn %{follower_address: fa} -> fa end)
407 where: u.id == ^follower.id,
412 "array(select distinct unnest (array_cat(?, ?)))",
421 {1, [follower]} = Repo.update_all(q, [])
423 Enum.each(followeds, &update_follower_count/1)
428 def follow(%User{} = follower, %User{info: info} = followed) do
429 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
430 ap_followers = followed.follower_address
434 {:error, "Could not follow user: You are deactivated."}
436 deny_follow_blocked and blocks?(followed, follower) ->
437 {:error, "Could not follow user: #{followed.nickname} blocked you."}
440 benchmark? = Pleroma.Config.get([:env]) == :benchmark
442 if !followed.local && follower.local && !ap_enabled?(followed) && !benchmark? do
443 Websub.subscribe(follower, followed)
448 where: u.id == ^follower.id,
449 update: [push: [following: ^ap_followers]],
453 {1, [follower]} = Repo.update_all(q, [])
455 follower = maybe_update_following_count(follower)
457 {:ok, _} = update_follower_count(followed)
463 def unfollow(%User{} = follower, %User{} = followed) do
464 ap_followers = followed.follower_address
466 if following?(follower, followed) and follower.ap_id != followed.ap_id do
469 where: u.id == ^follower.id,
470 update: [pull: [following: ^ap_followers]],
474 {1, [follower]} = Repo.update_all(q, [])
476 follower = maybe_update_following_count(follower)
478 {:ok, followed} = update_follower_count(followed)
482 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
484 {:error, "Not subscribed!"}
488 @spec following?(User.t(), User.t()) :: boolean
489 def following?(%User{} = follower, %User{} = followed) do
490 Enum.member?(follower.following, followed.follower_address)
493 def locked?(%User{} = user) do
494 user.info.locked || false
498 Repo.get_by(User, id: id)
501 def get_by_ap_id(ap_id) do
502 Repo.get_by(User, ap_id: ap_id)
505 def get_all_by_ap_id(ap_ids) do
506 from(u in __MODULE__,
507 where: u.ap_id in ^ap_ids
512 def get_all_by_ids(ids) do
513 from(u in __MODULE__, where: u.id in ^ids)
517 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
518 # of the ap_id and the domain and tries to get that user
519 def get_by_guessed_nickname(ap_id) do
520 domain = URI.parse(ap_id).host
521 name = List.last(String.split(ap_id, "/"))
522 nickname = "#{name}@#{domain}"
524 get_cached_by_nickname(nickname)
527 def set_cache({:ok, user}), do: set_cache(user)
528 def set_cache({:error, err}), do: {:error, err}
530 def set_cache(%User{} = user) do
531 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
532 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
533 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
537 def update_and_set_cache(changeset) do
538 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
543 def invalidate_cache(user) do
544 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
545 Cachex.del(:user_cache, "nickname:#{user.nickname}")
546 Cachex.del(:user_cache, "user_info:#{user.id}")
549 def get_cached_by_ap_id(ap_id) do
550 key = "ap_id:#{ap_id}"
551 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
554 def get_cached_by_id(id) do
558 Cachex.fetch!(:user_cache, key, fn _ ->
562 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
563 {:commit, user.ap_id}
569 get_cached_by_ap_id(ap_id)
572 def get_cached_by_nickname(nickname) do
573 key = "nickname:#{nickname}"
575 Cachex.fetch!(:user_cache, key, fn ->
576 case get_or_fetch_by_nickname(nickname) do
577 {:ok, user} -> {:commit, user}
578 {:error, _error} -> {:ignore, nil}
583 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
584 restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
587 is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
588 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
590 restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
591 get_cached_by_nickname(nickname_or_id)
593 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
594 get_cached_by_nickname(nickname_or_id)
601 def get_by_nickname(nickname) do
602 Repo.get_by(User, nickname: nickname) ||
603 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
604 Repo.get_by(User, nickname: local_nickname(nickname))
608 def get_by_email(email), do: Repo.get_by(User, email: email)
610 def get_by_nickname_or_email(nickname_or_email) do
611 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
614 def get_cached_user_info(user) do
615 key = "user_info:#{user.id}"
616 Cachex.fetch!(:user_cache, key, fn -> user_info(user) end)
619 def fetch_by_nickname(nickname) do
620 case ActivityPub.make_user_from_nickname(nickname) do
621 {:ok, user} -> {:ok, user}
622 _ -> OStatus.make_user(nickname)
626 def get_or_fetch_by_nickname(nickname) do
627 with %User{} = user <- get_by_nickname(nickname) do
631 with [_nick, _domain] <- String.split(nickname, "@"),
632 {:ok, user} <- fetch_by_nickname(nickname) do
633 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
634 fetch_initial_posts(user)
639 _e -> {:error, "not found " <> nickname}
644 @doc "Fetch some posts when the user has just been federated with"
645 def fetch_initial_posts(user) do
646 BackgroundWorker.enqueue("fetch_initial_posts", %{"user_id" => user.id})
649 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
650 def get_followers_query(%User{} = user, nil) do
651 User.Query.build(%{followers: user, deactivated: false})
654 def get_followers_query(user, page) do
656 |> get_followers_query(nil)
657 |> User.Query.paginate(page, 20)
660 @spec get_followers_query(User.t()) :: Ecto.Query.t()
661 def get_followers_query(user), do: get_followers_query(user, nil)
663 @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
664 def get_followers(user, page \\ nil) do
666 |> get_followers_query(page)
670 @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
671 def get_external_followers(user, page \\ nil) do
673 |> get_followers_query(page)
674 |> User.Query.build(%{external: true})
678 def get_followers_ids(user, page \\ nil) do
680 |> get_followers_query(page)
685 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
686 def get_friends_query(%User{} = user, nil) do
687 User.Query.build(%{friends: user, deactivated: false})
690 def get_friends_query(user, page) do
692 |> get_friends_query(nil)
693 |> User.Query.paginate(page, 20)
696 @spec get_friends_query(User.t()) :: Ecto.Query.t()
697 def get_friends_query(user), do: get_friends_query(user, nil)
699 def get_friends(user, page \\ nil) do
701 |> get_friends_query(page)
705 def get_friends_ids(user, page \\ nil) do
707 |> get_friends_query(page)
712 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
713 def get_follow_requests(%User{} = user) do
715 |> Activity.follow_requests_for_actor()
716 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
717 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
718 |> group_by([a, u], u.id)
723 def increase_note_count(%User{} = user) do
725 |> where(id: ^user.id)
730 "safe_jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
737 |> Repo.update_all([])
739 {1, [user]} -> set_cache(user)
744 def decrease_note_count(%User{} = user) do
746 |> where(id: ^user.id)
751 "safe_jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
758 |> Repo.update_all([])
760 {1, [user]} -> set_cache(user)
765 def update_note_count(%User{} = user) do
769 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
774 update_info(user, &User.Info.set_note_count(&1, note_count))
777 def update_mascot(user, url) do
779 User.Info.mascot_update(
786 |> put_embed(:info, info_changeset)
787 |> update_and_set_cache()
790 @spec maybe_fetch_follow_information(User.t()) :: User.t()
791 def maybe_fetch_follow_information(user) do
792 with {:ok, user} <- fetch_follow_information(user) do
796 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
802 def fetch_follow_information(user) do
803 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
804 update_info(user, &User.Info.follow_information_update(&1, info))
808 def update_follower_count(%User{} = user) do
809 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
810 follower_count_query =
811 User.Query.build(%{followers: user, deactivated: false})
812 |> select([u], %{count: count(u.id)})
815 |> where(id: ^user.id)
816 |> join(:inner, [u], s in subquery(follower_count_query))
821 "safe_jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
828 |> Repo.update_all([])
830 {1, [user]} -> set_cache(user)
834 {:ok, maybe_fetch_follow_information(user)}
838 @spec maybe_update_following_count(User.t()) :: User.t()
839 def maybe_update_following_count(%User{local: false} = user) do
840 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
841 maybe_fetch_follow_information(user)
847 def maybe_update_following_count(user), do: user
849 def set_unread_conversation_count(%User{local: true} = user) do
850 unread_query = Participation.unread_conversation_count_for_user(user)
853 |> join(:inner, [u], p in subquery(unread_query))
858 "jsonb_set(?, '{unread_conversation_count}', ?::varchar::jsonb, true)",
864 |> where([u], u.id == ^user.id)
866 |> Repo.update_all([])
868 {1, [user]} -> set_cache(user)
873 def set_unread_conversation_count(_), do: :noop
875 def increment_unread_conversation_count(conversation, %User{local: true} = user) do
877 Participation.unread_conversation_count_for_user(user)
878 |> where([p], p.conversation_id == ^conversation.id)
881 |> join(:inner, [u], p in subquery(unread_query))
886 "jsonb_set(?, '{unread_conversation_count}', (coalesce((?->>'unread_conversation_count')::int, 0) + 1)::varchar::jsonb, true)",
892 |> where([u], u.id == ^user.id)
893 |> where([u, p], p.count == 0)
895 |> Repo.update_all([])
897 {1, [user]} -> set_cache(user)
902 def increment_unread_conversation_count(_, _), do: :noop
904 def remove_duplicated_following(%User{following: following} = user) do
905 uniq_following = Enum.uniq(following)
907 if length(following) == length(uniq_following) do
911 |> update_changeset(%{following: uniq_following})
912 |> update_and_set_cache()
916 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
917 def get_users_from_set(ap_ids, local_only \\ true) do
918 criteria = %{ap_id: ap_ids, deactivated: false}
919 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
921 User.Query.build(criteria)
925 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
926 def get_recipients_from_activity(%Activity{recipients: to}) do
927 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
931 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
932 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
933 update_info(muter, &User.Info.add_to_mutes(&1, ap_id, notifications?))
936 def unmute(muter, %{ap_id: ap_id}) do
937 update_info(muter, &User.Info.remove_from_mutes(&1, ap_id))
940 def subscribe(subscriber, %{ap_id: ap_id}) do
941 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
942 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
944 if blocks?(subscribed, subscriber) and deny_follow_blocked do
945 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
947 update_info(subscribed, &User.Info.add_to_subscribers(&1, subscriber.ap_id))
952 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
953 with %User{} = user <- get_cached_by_ap_id(ap_id) do
954 update_info(user, &User.Info.remove_from_subscribers(&1, unsubscriber.ap_id))
958 def block(blocker, %User{ap_id: ap_id} = blocked) do
959 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
961 if following?(blocker, blocked) do
962 {:ok, blocker, _} = unfollow(blocker, blocked)
968 # clear any requested follows as well
970 case CommonAPI.reject_follow_request(blocked, blocker) do
971 {:ok, %User{} = updated_blocked} -> updated_blocked
976 if subscribed_to?(blocked, blocker) do
977 {:ok, blocker} = unsubscribe(blocked, blocker)
983 if following?(blocked, blocker), do: unfollow(blocked, blocker)
985 {:ok, blocker} = update_follower_count(blocker)
987 update_info(blocker, &User.Info.add_to_block(&1, ap_id))
990 # helper to handle the block given only an actor's AP id
991 def block(blocker, %{ap_id: ap_id}) do
992 block(blocker, get_cached_by_ap_id(ap_id))
995 def unblock(blocker, %{ap_id: ap_id}) do
996 update_info(blocker, &User.Info.remove_from_block(&1, ap_id))
999 def mutes?(nil, _), do: false
1000 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
1002 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
1003 def muted_notifications?(nil, _), do: false
1005 def muted_notifications?(user, %{ap_id: ap_id}),
1006 do: Enum.member?(user.info.muted_notifications, ap_id)
1008 def blocks?(%User{} = user, %User{} = target) do
1009 blocks_ap_id?(user, target) || blocks_domain?(user, target)
1012 def blocks?(nil, _), do: false
1014 def blocks_ap_id?(%User{} = user, %User{} = target) do
1015 Enum.member?(user.info.blocks, target.ap_id)
1018 def blocks_ap_id?(_, _), do: false
1020 def blocks_domain?(%User{} = user, %User{} = target) do
1021 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
1022 %{host: host} = URI.parse(target.ap_id)
1023 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1026 def blocks_domain?(_, _), do: false
1028 def subscribed_to?(user, %{ap_id: ap_id}) do
1029 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1030 Enum.member?(target.info.subscribers, user.ap_id)
1034 @spec muted_users(User.t()) :: [User.t()]
1035 def muted_users(user) do
1036 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
1040 @spec blocked_users(User.t()) :: [User.t()]
1041 def blocked_users(user) do
1042 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
1046 @spec subscribers(User.t()) :: [User.t()]
1047 def subscribers(user) do
1048 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
1052 def block_domain(user, domain) do
1053 update_info(user, &User.Info.add_to_domain_block(&1, domain))
1056 def unblock_domain(user, domain) do
1057 update_info(user, &User.Info.remove_from_domain_block(&1, domain))
1060 def deactivate_async(user, status \\ true) do
1061 BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
1064 def deactivate(user, status \\ true)
1066 def deactivate(users, status) when is_list(users) do
1067 Repo.transaction(fn ->
1068 for user <- users, do: deactivate(user, status)
1072 def deactivate(%User{} = user, status) do
1073 with {:ok, user} <- update_info(user, &User.Info.set_activation_status(&1, status)) do
1074 Enum.each(get_followers(user), &invalidate_cache/1)
1075 Enum.each(get_friends(user), &update_follower_count/1)
1081 def update_notification_settings(%User{} = user, settings \\ %{}) do
1082 update_info(user, &User.Info.update_notification_settings(&1, settings))
1085 def delete(users) when is_list(users) do
1086 for user <- users, do: delete(user)
1089 def delete(%User{} = user) do
1090 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1093 def perform(:force_password_reset, user), do: force_password_reset(user)
1095 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1096 def perform(:delete, %User{} = user) do
1097 {:ok, _user} = ActivityPub.delete(user)
1099 # Remove all relationships
1102 |> Enum.each(fn follower ->
1103 ActivityPub.unfollow(follower, user)
1104 unfollow(follower, user)
1109 |> Enum.each(fn followed ->
1110 ActivityPub.unfollow(user, followed)
1111 unfollow(user, followed)
1114 delete_user_activities(user)
1115 invalidate_cache(user)
1119 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1120 def perform(:fetch_initial_posts, %User{} = user) do
1121 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1123 # Insert all the posts in reverse order, so they're in the right order on the timeline
1124 user.info.source_data["outbox"]
1125 |> Utils.fetch_ordered_collection(pages)
1127 |> Enum.each(&Pleroma.Web.Federator.incoming_ap_doc/1)
1130 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1132 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1133 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1134 when is_list(blocked_identifiers) do
1136 blocked_identifiers,
1137 fn blocked_identifier ->
1138 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1139 {:ok, blocker} <- block(blocker, blocked),
1140 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1144 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1151 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1152 def perform(:follow_import, %User{} = follower, followed_identifiers)
1153 when is_list(followed_identifiers) do
1155 followed_identifiers,
1156 fn followed_identifier ->
1157 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1158 {:ok, follower} <- maybe_direct_follow(follower, followed),
1159 {:ok, _} <- ActivityPub.follow(follower, followed) do
1163 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1170 @spec external_users_query() :: Ecto.Query.t()
1171 def external_users_query do
1179 @spec external_users(keyword()) :: [User.t()]
1180 def external_users(opts \\ []) do
1182 external_users_query()
1183 |> select([u], struct(u, [:id, :ap_id, :info]))
1187 do: where(query, [u], u.id > ^opts[:max_id]),
1192 do: limit(query, ^opts[:limit]),
1198 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1199 BackgroundWorker.enqueue("blocks_import", %{
1200 "blocker_id" => blocker.id,
1201 "blocked_identifiers" => blocked_identifiers
1205 def follow_import(%User{} = follower, followed_identifiers)
1206 when is_list(followed_identifiers) do
1207 BackgroundWorker.enqueue("follow_import", %{
1208 "follower_id" => follower.id,
1209 "followed_identifiers" => followed_identifiers
1213 def delete_user_activities(%User{ap_id: ap_id}) do
1215 |> Activity.Queries.by_actor()
1216 |> RepoStreamer.chunk_stream(50)
1217 |> Stream.each(fn activities -> Enum.each(activities, &delete_activity/1) end)
1221 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1223 |> Object.normalize()
1224 |> ActivityPub.delete()
1227 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1228 object = Object.normalize(activity)
1231 |> get_cached_by_ap_id()
1232 |> ActivityPub.unlike(object)
1235 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1236 object = Object.normalize(activity)
1239 |> get_cached_by_ap_id()
1240 |> ActivityPub.unannounce(object)
1243 defp delete_activity(_activity), do: "Doing nothing"
1245 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1246 Pleroma.HTML.Scrubber.TwitterText
1249 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1251 def fetch_by_ap_id(ap_id) do
1252 case ActivityPub.make_user_from_ap_id(ap_id) do
1257 case OStatus.make_user(ap_id) do
1258 {:ok, user} -> {:ok, user}
1259 _ -> {:error, "Could not fetch by AP id"}
1264 def get_or_fetch_by_ap_id(ap_id) do
1265 user = get_cached_by_ap_id(ap_id)
1267 if !is_nil(user) and !needs_update?(user) do
1270 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1271 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1273 resp = fetch_by_ap_id(ap_id)
1275 if should_fetch_initial do
1276 with {:ok, %User{} = user} <- resp do
1277 fetch_initial_posts(user)
1285 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1286 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1287 with %User{} = user <- get_cached_by_ap_id(uri) do
1292 %User{info: %User.Info{}}
1293 |> cast(%{}, [:ap_id, :nickname, :local])
1294 |> put_change(:ap_id, uri)
1295 |> put_change(:nickname, nickname)
1296 |> put_change(:local, true)
1297 |> put_change(:follower_address, uri <> "/followers")
1305 def public_key_from_info(%{
1306 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1310 |> :public_key.pem_decode()
1312 |> :public_key.pem_entry_decode()
1318 def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
1319 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1322 def public_key_from_info(_), do: {:error, "not found key"}
1324 def get_public_key_for_ap_id(ap_id) do
1325 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1326 {:ok, public_key} <- public_key_from_info(user.info) do
1333 defp blank?(""), do: nil
1334 defp blank?(n), do: n
1336 def insert_or_update_user(data) do
1338 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1339 |> remote_user_creation()
1340 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1344 def ap_enabled?(%User{local: true}), do: true
1345 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1346 def ap_enabled?(_), do: false
1348 @doc "Gets or fetch a user by uri or nickname."
1349 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1350 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1351 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1353 # wait a period of time and return newest version of the User structs
1354 # this is because we have synchronous follow APIs and need to simulate them
1355 # with an async handshake
1356 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1357 with %User{} = a <- get_cached_by_id(a.id),
1358 %User{} = b <- get_cached_by_id(b.id) do
1365 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1366 with :ok <- :timer.sleep(timeout),
1367 %User{} = a <- get_cached_by_id(a.id),
1368 %User{} = b <- get_cached_by_id(b.id) do
1375 def parse_bio(bio) when is_binary(bio) and bio != "" do
1377 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1381 def parse_bio(_), do: ""
1383 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1384 # TODO: get profile URLs other than user.ap_id
1385 profile_urls = [user.ap_id]
1388 |> CommonUtils.format_input("text/plain",
1389 mentions_format: :full,
1390 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1395 def parse_bio(_, _), do: ""
1397 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1398 Repo.transaction(fn ->
1399 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1403 def tag(nickname, tags) when is_binary(nickname),
1404 do: tag(get_by_nickname(nickname), tags)
1406 def tag(%User{} = user, tags),
1407 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1409 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1410 Repo.transaction(fn ->
1411 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1415 def untag(nickname, tags) when is_binary(nickname),
1416 do: untag(get_by_nickname(nickname), tags)
1418 def untag(%User{} = user, tags),
1419 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1421 defp update_tags(%User{} = user, new_tags) do
1422 {:ok, updated_user} =
1424 |> change(%{tags: new_tags})
1425 |> update_and_set_cache()
1430 defp normalize_tags(tags) do
1433 |> Enum.map(&String.downcase/1)
1436 defp local_nickname_regex do
1437 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1438 @extended_local_nickname_regex
1440 @strict_local_nickname_regex
1444 def local_nickname(nickname_or_mention) do
1447 |> String.split("@")
1451 def full_nickname(nickname_or_mention),
1452 do: String.trim_leading(nickname_or_mention, "@")
1454 def error_user(ap_id) do
1459 nickname: "erroruser@example.com",
1460 inserted_at: NaiveDateTime.utc_now()
1464 @spec all_superusers() :: [User.t()]
1465 def all_superusers do
1466 User.Query.build(%{super_users: true, local: true, deactivated: false})
1470 def showing_reblogs?(%User{} = user, %User{} = target) do
1471 target.ap_id not in user.info.muted_reblogs
1475 The function returns a query to get users with no activity for given interval of days.
1476 Inactive users are those who didn't read any notification, or had any activity where
1477 the user is the activity's actor, during `inactivity_threshold` days.
1478 Deactivated users will not appear in this list.
1482 iex> Pleroma.User.list_inactive_users()
1485 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1486 def list_inactive_users_query(inactivity_threshold \\ 7) do
1487 negative_inactivity_threshold = -inactivity_threshold
1488 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1489 # Subqueries are not supported in `where` clauses, join gets too complicated.
1490 has_read_notifications =
1491 from(n in Pleroma.Notification,
1492 where: n.seen == true,
1494 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1497 |> Pleroma.Repo.all()
1499 from(u in Pleroma.User,
1500 left_join: a in Pleroma.Activity,
1501 on: u.ap_id == a.actor,
1502 where: not is_nil(u.nickname),
1503 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1504 where: u.id not in ^has_read_notifications,
1507 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1508 is_nil(max(a.inserted_at))
1513 Enable or disable email notifications for user
1517 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1518 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1520 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1521 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1523 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1524 {:ok, t()} | {:error, Ecto.Changeset.t()}
1525 def switch_email_notifications(user, type, status) do
1526 update_info(user, &User.Info.update_email_notifications(&1, %{type => status}))
1530 Set `last_digest_emailed_at` value for the user to current time
1532 @spec touch_last_digest_emailed_at(t()) :: t()
1533 def touch_last_digest_emailed_at(user) do
1534 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1536 {:ok, updated_user} =
1538 |> change(%{last_digest_emailed_at: now})
1539 |> update_and_set_cache()
1544 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1545 def toggle_confirmation(%User{} = user) do
1546 need_confirmation? = !user.info.confirmation_pending
1549 |> update_info(&User.Info.confirmation_changeset(&1, need_confirmation: need_confirmation?))
1552 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1556 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1557 # use instance-default
1558 config = Pleroma.Config.get([:assets, :mascots])
1559 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1560 mascot = Keyword.get(config, default_mascot)
1563 "id" => "default-mascot",
1564 "url" => mascot[:url],
1565 "preview_url" => mascot[:url],
1567 "mime_type" => mascot[:mime_type]
1572 def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
1574 def ensure_keys_present(%User{} = user) do
1575 with {:ok, pem} <- Keys.generate_rsa_pem() do
1577 |> cast(%{keys: pem}, [:keys])
1578 |> validate_required([:keys])
1579 |> update_and_set_cache()
1583 def get_ap_ids_by_nicknames(nicknames) do
1585 where: u.nickname in ^nicknames,
1591 defdelegate search(query, opts \\ []), to: User.Search
1593 defp put_password_hash(
1594 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1596 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1599 defp put_password_hash(changeset), do: changeset
1601 def is_internal_user?(%User{nickname: nil}), do: true
1602 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1603 def is_internal_user?(_), do: false
1605 # A hack because user delete activities have a fake id for whatever reason
1606 # TODO: Get rid of this
1607 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
1609 def get_delivered_users_by_object_id(object_id) do
1611 inner_join: delivery in assoc(u, :deliveries),
1612 where: delivery.object_id == ^object_id
1617 def change_email(user, email) do
1619 |> cast(%{email: email}, [:email])
1620 |> validate_required([:email])
1621 |> unique_constraint(:email)
1622 |> validate_format(:email, @email_regex)
1623 |> update_and_set_cache()
1627 Changes `user.info` and returns the user changeset.
1629 `fun` is called with the `user.info`.
1631 def change_info(user, fun) do
1632 changeset = change(user)
1633 info = get_field(changeset, :info) || %User.Info{}
1634 put_embed(changeset, :info, fun.(info))
1638 Updates `user.info` and sets cache.
1640 `fun` is called with the `user.info`.
1642 def update_info(users, fun) when is_list(users) do
1643 Repo.transaction(fn ->
1644 for user <- users, do: update_info(user, fun)
1648 def update_info(user, fun) do
1651 |> update_and_set_cache()