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 if !followed.local && follower.local && !ap_enabled?(followed) do
441 Websub.subscribe(follower, followed)
446 where: u.id == ^follower.id,
447 update: [push: [following: ^ap_followers]],
451 {1, [follower]} = Repo.update_all(q, [])
453 follower = maybe_update_following_count(follower)
455 {:ok, _} = update_follower_count(followed)
461 def unfollow(%User{} = follower, %User{} = followed) do
462 ap_followers = followed.follower_address
464 if following?(follower, followed) and follower.ap_id != followed.ap_id do
467 where: u.id == ^follower.id,
468 update: [pull: [following: ^ap_followers]],
472 {1, [follower]} = Repo.update_all(q, [])
474 follower = maybe_update_following_count(follower)
476 {:ok, followed} = update_follower_count(followed)
480 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
482 {:error, "Not subscribed!"}
486 @spec following?(User.t(), User.t()) :: boolean
487 def following?(%User{} = follower, %User{} = followed) do
488 Enum.member?(follower.following, followed.follower_address)
491 def locked?(%User{} = user) do
492 user.info.locked || false
496 Repo.get_by(User, id: id)
499 def get_by_ap_id(ap_id) do
500 Repo.get_by(User, ap_id: ap_id)
503 def get_all_by_ap_id(ap_ids) do
504 from(u in __MODULE__,
505 where: u.ap_id in ^ap_ids
510 def get_all_by_ids(ids) do
511 from(u in __MODULE__, where: u.id in ^ids)
515 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
516 # of the ap_id and the domain and tries to get that user
517 def get_by_guessed_nickname(ap_id) do
518 domain = URI.parse(ap_id).host
519 name = List.last(String.split(ap_id, "/"))
520 nickname = "#{name}@#{domain}"
522 get_cached_by_nickname(nickname)
525 def set_cache({:ok, user}), do: set_cache(user)
526 def set_cache({:error, err}), do: {:error, err}
528 def set_cache(%User{} = user) do
529 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
530 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
531 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
535 def update_and_set_cache(changeset) do
536 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
541 def invalidate_cache(user) do
542 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
543 Cachex.del(:user_cache, "nickname:#{user.nickname}")
544 Cachex.del(:user_cache, "user_info:#{user.id}")
547 def get_cached_by_ap_id(ap_id) do
548 key = "ap_id:#{ap_id}"
549 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
552 def get_cached_by_id(id) do
556 Cachex.fetch!(:user_cache, key, fn _ ->
560 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
561 {:commit, user.ap_id}
567 get_cached_by_ap_id(ap_id)
570 def get_cached_by_nickname(nickname) do
571 key = "nickname:#{nickname}"
573 Cachex.fetch!(:user_cache, key, fn ->
574 case get_or_fetch_by_nickname(nickname) do
575 {:ok, user} -> {:commit, user}
576 {:error, _error} -> {:ignore, nil}
581 def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
582 restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
585 is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
586 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
588 restrict_to_local == false or not String.contains?(nickname_or_id, "@") ->
589 get_cached_by_nickname(nickname_or_id)
591 restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
592 get_cached_by_nickname(nickname_or_id)
599 def get_by_nickname(nickname) do
600 Repo.get_by(User, nickname: nickname) ||
601 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
602 Repo.get_by(User, nickname: local_nickname(nickname))
606 def get_by_email(email), do: Repo.get_by(User, email: email)
608 def get_by_nickname_or_email(nickname_or_email) do
609 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
612 def get_cached_user_info(user) do
613 key = "user_info:#{user.id}"
614 Cachex.fetch!(:user_cache, key, fn -> user_info(user) end)
617 def fetch_by_nickname(nickname) do
618 case ActivityPub.make_user_from_nickname(nickname) do
619 {:ok, user} -> {:ok, user}
620 _ -> OStatus.make_user(nickname)
624 def get_or_fetch_by_nickname(nickname) do
625 with %User{} = user <- get_by_nickname(nickname) do
629 with [_nick, _domain] <- String.split(nickname, "@"),
630 {:ok, user} <- fetch_by_nickname(nickname) do
631 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
632 fetch_initial_posts(user)
637 _e -> {:error, "not found " <> nickname}
642 @doc "Fetch some posts when the user has just been federated with"
643 def fetch_initial_posts(user) do
644 BackgroundWorker.enqueue("fetch_initial_posts", %{"user_id" => user.id})
647 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
648 def get_followers_query(%User{} = user, nil) do
649 User.Query.build(%{followers: user, deactivated: false})
652 def get_followers_query(user, page) do
654 |> get_followers_query(nil)
655 |> User.Query.paginate(page, 20)
658 @spec get_followers_query(User.t()) :: Ecto.Query.t()
659 def get_followers_query(user), do: get_followers_query(user, nil)
661 @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
662 def get_followers(user, page \\ nil) do
664 |> get_followers_query(page)
668 @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
669 def get_external_followers(user, page \\ nil) do
671 |> get_followers_query(page)
672 |> User.Query.build(%{external: true})
676 def get_followers_ids(user, page \\ nil) do
678 |> get_followers_query(page)
683 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
684 def get_friends_query(%User{} = user, nil) do
685 User.Query.build(%{friends: user, deactivated: false})
688 def get_friends_query(user, page) do
690 |> get_friends_query(nil)
691 |> User.Query.paginate(page, 20)
694 @spec get_friends_query(User.t()) :: Ecto.Query.t()
695 def get_friends_query(user), do: get_friends_query(user, nil)
697 def get_friends(user, page \\ nil) do
699 |> get_friends_query(page)
703 def get_friends_ids(user, page \\ nil) do
705 |> get_friends_query(page)
710 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
711 def get_follow_requests(%User{} = user) do
713 |> Activity.follow_requests_for_actor()
714 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
715 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
716 |> group_by([a, u], u.id)
721 def increase_note_count(%User{} = user) do
723 |> where(id: ^user.id)
728 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
735 |> Repo.update_all([])
737 {1, [user]} -> set_cache(user)
742 def decrease_note_count(%User{} = user) do
744 |> where(id: ^user.id)
749 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
756 |> Repo.update_all([])
758 {1, [user]} -> set_cache(user)
763 def update_note_count(%User{} = user) do
767 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
772 update_info(user, &User.Info.set_note_count(&1, note_count))
775 def update_mascot(user, url) do
777 User.Info.mascot_update(
784 |> put_embed(:info, info_changeset)
785 |> update_and_set_cache()
788 @spec maybe_fetch_follow_information(User.t()) :: User.t()
789 def maybe_fetch_follow_information(user) do
790 with {:ok, user} <- fetch_follow_information(user) do
794 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
800 def fetch_follow_information(user) do
801 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
802 update_info(user, &User.Info.follow_information_update(&1, info))
806 def update_follower_count(%User{} = user) do
807 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
808 follower_count_query =
809 User.Query.build(%{followers: user, deactivated: false})
810 |> select([u], %{count: count(u.id)})
813 |> where(id: ^user.id)
814 |> join(:inner, [u], s in subquery(follower_count_query))
819 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
826 |> Repo.update_all([])
828 {1, [user]} -> set_cache(user)
832 {:ok, maybe_fetch_follow_information(user)}
836 @spec maybe_update_following_count(User.t()) :: User.t()
837 def maybe_update_following_count(%User{local: false} = user) do
838 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
839 maybe_fetch_follow_information(user)
845 def maybe_update_following_count(user), do: user
847 def set_unread_conversation_count(%User{local: true} = user) do
848 unread_query = Participation.unread_conversation_count_for_user(user)
851 |> join(:inner, [u], p in subquery(unread_query))
856 "jsonb_set(?, '{unread_conversation_count}', ?::varchar::jsonb, true)",
862 |> where([u], u.id == ^user.id)
864 |> Repo.update_all([])
866 {1, [user]} -> set_cache(user)
871 def set_unread_conversation_count(_), do: :noop
873 def increment_unread_conversation_count(conversation, %User{local: true} = user) do
875 Participation.unread_conversation_count_for_user(user)
876 |> where([p], p.conversation_id == ^conversation.id)
879 |> join(:inner, [u], p in subquery(unread_query))
884 "jsonb_set(?, '{unread_conversation_count}', (coalesce((?->>'unread_conversation_count')::int, 0) + 1)::varchar::jsonb, true)",
890 |> where([u], u.id == ^user.id)
891 |> where([u, p], p.count == 0)
893 |> Repo.update_all([])
895 {1, [user]} -> set_cache(user)
900 def increment_unread_conversation_count(_, _), do: :noop
902 def remove_duplicated_following(%User{following: following} = user) do
903 uniq_following = Enum.uniq(following)
905 if length(following) == length(uniq_following) do
909 |> update_changeset(%{following: uniq_following})
910 |> update_and_set_cache()
914 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
915 def get_users_from_set(ap_ids, local_only \\ true) do
916 criteria = %{ap_id: ap_ids, deactivated: false}
917 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
919 User.Query.build(criteria)
923 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
924 def get_recipients_from_activity(%Activity{recipients: to}) do
925 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
929 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
930 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
931 update_info(muter, &User.Info.add_to_mutes(&1, ap_id, notifications?))
934 def unmute(muter, %{ap_id: ap_id}) do
935 update_info(muter, &User.Info.remove_from_mutes(&1, ap_id))
938 def subscribe(subscriber, %{ap_id: ap_id}) do
939 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
940 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
942 if blocks?(subscribed, subscriber) and deny_follow_blocked do
943 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
945 update_info(subscribed, &User.Info.add_to_subscribers(&1, subscriber.ap_id))
950 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
951 with %User{} = user <- get_cached_by_ap_id(ap_id) do
952 update_info(user, &User.Info.remove_from_subscribers(&1, unsubscriber.ap_id))
956 def block(blocker, %User{ap_id: ap_id} = blocked) do
957 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
959 if following?(blocker, blocked) do
960 {:ok, blocker, _} = unfollow(blocker, blocked)
966 # clear any requested follows as well
968 case CommonAPI.reject_follow_request(blocked, blocker) do
969 {:ok, %User{} = updated_blocked} -> updated_blocked
974 if subscribed_to?(blocked, blocker) do
975 {:ok, blocker} = unsubscribe(blocked, blocker)
981 if following?(blocked, blocker), do: unfollow(blocked, blocker)
983 {:ok, blocker} = update_follower_count(blocker)
985 update_info(blocker, &User.Info.add_to_block(&1, ap_id))
988 # helper to handle the block given only an actor's AP id
989 def block(blocker, %{ap_id: ap_id}) do
990 block(blocker, get_cached_by_ap_id(ap_id))
993 def unblock(blocker, %{ap_id: ap_id}) do
994 update_info(blocker, &User.Info.remove_from_block(&1, ap_id))
997 def mutes?(nil, _), do: false
998 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
1000 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
1001 def muted_notifications?(nil, _), do: false
1003 def muted_notifications?(user, %{ap_id: ap_id}),
1004 do: Enum.member?(user.info.muted_notifications, ap_id)
1006 def blocks?(%User{} = user, %User{} = target) do
1007 blocks_ap_id?(user, target) || blocks_domain?(user, target)
1010 def blocks?(nil, _), do: false
1012 def blocks_ap_id?(%User{} = user, %User{} = target) do
1013 Enum.member?(user.info.blocks, target.ap_id)
1016 def blocks_ap_id?(_, _), do: false
1018 def blocks_domain?(%User{} = user, %User{} = target) do
1019 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
1020 %{host: host} = URI.parse(target.ap_id)
1021 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
1024 def blocks_domain?(_, _), do: false
1026 def subscribed_to?(user, %{ap_id: ap_id}) do
1027 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1028 Enum.member?(target.info.subscribers, user.ap_id)
1032 @spec muted_users(User.t()) :: [User.t()]
1033 def muted_users(user) do
1034 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
1038 @spec blocked_users(User.t()) :: [User.t()]
1039 def blocked_users(user) do
1040 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
1044 @spec subscribers(User.t()) :: [User.t()]
1045 def subscribers(user) do
1046 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
1050 def block_domain(user, domain) do
1051 update_info(user, &User.Info.add_to_domain_block(&1, domain))
1054 def unblock_domain(user, domain) do
1055 update_info(user, &User.Info.remove_from_domain_block(&1, domain))
1058 def deactivate_async(user, status \\ true) do
1059 BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
1062 def deactivate(user, status \\ true)
1064 def deactivate(users, status) when is_list(users) do
1065 Repo.transaction(fn ->
1066 for user <- users, do: deactivate(user, status)
1070 def deactivate(%User{} = user, status) do
1071 with {:ok, user} <- update_info(user, &User.Info.set_activation_status(&1, status)) do
1072 Enum.each(get_followers(user), &invalidate_cache/1)
1073 Enum.each(get_friends(user), &update_follower_count/1)
1079 def update_notification_settings(%User{} = user, settings \\ %{}) do
1080 update_info(user, &User.Info.update_notification_settings(&1, settings))
1083 def delete(users) when is_list(users) do
1084 for user <- users, do: delete(user)
1087 def delete(%User{} = user) do
1088 BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
1091 def perform(:force_password_reset, user), do: force_password_reset(user)
1093 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1094 def perform(:delete, %User{} = user) do
1095 {:ok, _user} = ActivityPub.delete(user)
1097 # Remove all relationships
1100 |> Enum.each(fn follower ->
1101 ActivityPub.unfollow(follower, user)
1102 unfollow(follower, user)
1107 |> Enum.each(fn followed ->
1108 ActivityPub.unfollow(user, followed)
1109 unfollow(user, followed)
1112 delete_user_activities(user)
1113 invalidate_cache(user)
1117 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1118 def perform(:fetch_initial_posts, %User{} = user) do
1119 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1121 # Insert all the posts in reverse order, so they're in the right order on the timeline
1122 user.info.source_data["outbox"]
1123 |> Utils.fetch_ordered_collection(pages)
1125 |> Enum.each(&Pleroma.Web.Federator.incoming_ap_doc/1)
1128 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1130 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1131 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1132 when is_list(blocked_identifiers) do
1134 blocked_identifiers,
1135 fn blocked_identifier ->
1136 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1137 {:ok, blocker} <- block(blocker, blocked),
1138 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1142 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1149 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1150 def perform(:follow_import, %User{} = follower, followed_identifiers)
1151 when is_list(followed_identifiers) do
1153 followed_identifiers,
1154 fn followed_identifier ->
1155 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1156 {:ok, follower} <- maybe_direct_follow(follower, followed),
1157 {:ok, _} <- ActivityPub.follow(follower, followed) do
1161 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1168 @spec external_users_query() :: Ecto.Query.t()
1169 def external_users_query do
1177 @spec external_users(keyword()) :: [User.t()]
1178 def external_users(opts \\ []) do
1180 external_users_query()
1181 |> select([u], struct(u, [:id, :ap_id, :info]))
1185 do: where(query, [u], u.id > ^opts[:max_id]),
1190 do: limit(query, ^opts[:limit]),
1196 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
1197 BackgroundWorker.enqueue("blocks_import", %{
1198 "blocker_id" => blocker.id,
1199 "blocked_identifiers" => blocked_identifiers
1203 def follow_import(%User{} = follower, followed_identifiers)
1204 when is_list(followed_identifiers) do
1205 BackgroundWorker.enqueue("follow_import", %{
1206 "follower_id" => follower.id,
1207 "followed_identifiers" => followed_identifiers
1211 def delete_user_activities(%User{ap_id: ap_id}) do
1213 |> Activity.Queries.by_actor()
1214 |> RepoStreamer.chunk_stream(50)
1215 |> Stream.each(fn activities -> Enum.each(activities, &delete_activity/1) end)
1219 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1221 |> Object.normalize()
1222 |> ActivityPub.delete()
1225 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1226 object = Object.normalize(activity)
1229 |> get_cached_by_ap_id()
1230 |> ActivityPub.unlike(object)
1233 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1234 object = Object.normalize(activity)
1237 |> get_cached_by_ap_id()
1238 |> ActivityPub.unannounce(object)
1241 defp delete_activity(_activity), do: "Doing nothing"
1243 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1244 Pleroma.HTML.Scrubber.TwitterText
1247 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1249 def fetch_by_ap_id(ap_id) do
1250 case ActivityPub.make_user_from_ap_id(ap_id) do
1255 case OStatus.make_user(ap_id) do
1256 {:ok, user} -> {:ok, user}
1257 _ -> {:error, "Could not fetch by AP id"}
1262 def get_or_fetch_by_ap_id(ap_id) do
1263 user = get_cached_by_ap_id(ap_id)
1265 if !is_nil(user) and !needs_update?(user) do
1268 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1269 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1271 resp = fetch_by_ap_id(ap_id)
1273 if should_fetch_initial do
1274 with {:ok, %User{} = user} <- resp do
1275 fetch_initial_posts(user)
1283 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1284 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1285 with %User{} = user <- get_cached_by_ap_id(uri) do
1290 %User{info: %User.Info{}}
1291 |> cast(%{}, [:ap_id, :nickname, :local])
1292 |> put_change(:ap_id, uri)
1293 |> put_change(:nickname, nickname)
1294 |> put_change(:local, true)
1295 |> put_change(:follower_address, uri <> "/followers")
1303 def public_key_from_info(%{
1304 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1308 |> :public_key.pem_decode()
1310 |> :public_key.pem_entry_decode()
1316 def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
1317 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1320 def public_key_from_info(_), do: {:error, "not found key"}
1322 def get_public_key_for_ap_id(ap_id) do
1323 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1324 {:ok, public_key} <- public_key_from_info(user.info) do
1331 defp blank?(""), do: nil
1332 defp blank?(n), do: n
1334 def insert_or_update_user(data) do
1336 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1337 |> remote_user_creation()
1338 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1342 def ap_enabled?(%User{local: true}), do: true
1343 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1344 def ap_enabled?(_), do: false
1346 @doc "Gets or fetch a user by uri or nickname."
1347 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1348 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1349 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1351 # wait a period of time and return newest version of the User structs
1352 # this is because we have synchronous follow APIs and need to simulate them
1353 # with an async handshake
1354 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1355 with %User{} = a <- get_cached_by_id(a.id),
1356 %User{} = b <- get_cached_by_id(b.id) do
1363 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1364 with :ok <- :timer.sleep(timeout),
1365 %User{} = a <- get_cached_by_id(a.id),
1366 %User{} = b <- get_cached_by_id(b.id) do
1373 def parse_bio(bio) when is_binary(bio) and bio != "" do
1375 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1379 def parse_bio(_), do: ""
1381 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1382 # TODO: get profile URLs other than user.ap_id
1383 profile_urls = [user.ap_id]
1386 |> CommonUtils.format_input("text/plain",
1387 mentions_format: :full,
1388 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1393 def parse_bio(_, _), do: ""
1395 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1396 Repo.transaction(fn ->
1397 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1401 def tag(nickname, tags) when is_binary(nickname),
1402 do: tag(get_by_nickname(nickname), tags)
1404 def tag(%User{} = user, tags),
1405 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1407 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1408 Repo.transaction(fn ->
1409 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1413 def untag(nickname, tags) when is_binary(nickname),
1414 do: untag(get_by_nickname(nickname), tags)
1416 def untag(%User{} = user, tags),
1417 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1419 defp update_tags(%User{} = user, new_tags) do
1420 {:ok, updated_user} =
1422 |> change(%{tags: new_tags})
1423 |> update_and_set_cache()
1428 defp normalize_tags(tags) do
1431 |> Enum.map(&String.downcase/1)
1434 defp local_nickname_regex do
1435 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1436 @extended_local_nickname_regex
1438 @strict_local_nickname_regex
1442 def local_nickname(nickname_or_mention) do
1445 |> String.split("@")
1449 def full_nickname(nickname_or_mention),
1450 do: String.trim_leading(nickname_or_mention, "@")
1452 def error_user(ap_id) do
1457 nickname: "erroruser@example.com",
1458 inserted_at: NaiveDateTime.utc_now()
1462 @spec all_superusers() :: [User.t()]
1463 def all_superusers do
1464 User.Query.build(%{super_users: true, local: true, deactivated: false})
1468 def showing_reblogs?(%User{} = user, %User{} = target) do
1469 target.ap_id not in user.info.muted_reblogs
1473 The function returns a query to get users with no activity for given interval of days.
1474 Inactive users are those who didn't read any notification, or had any activity where
1475 the user is the activity's actor, during `inactivity_threshold` days.
1476 Deactivated users will not appear in this list.
1480 iex> Pleroma.User.list_inactive_users()
1483 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1484 def list_inactive_users_query(inactivity_threshold \\ 7) do
1485 negative_inactivity_threshold = -inactivity_threshold
1486 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1487 # Subqueries are not supported in `where` clauses, join gets too complicated.
1488 has_read_notifications =
1489 from(n in Pleroma.Notification,
1490 where: n.seen == true,
1492 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1495 |> Pleroma.Repo.all()
1497 from(u in Pleroma.User,
1498 left_join: a in Pleroma.Activity,
1499 on: u.ap_id == a.actor,
1500 where: not is_nil(u.nickname),
1501 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1502 where: u.id not in ^has_read_notifications,
1505 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1506 is_nil(max(a.inserted_at))
1511 Enable or disable email notifications for user
1515 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1516 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1518 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1519 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1521 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1522 {:ok, t()} | {:error, Ecto.Changeset.t()}
1523 def switch_email_notifications(user, type, status) do
1524 update_info(user, &User.Info.update_email_notifications(&1, %{type => status}))
1528 Set `last_digest_emailed_at` value for the user to current time
1530 @spec touch_last_digest_emailed_at(t()) :: t()
1531 def touch_last_digest_emailed_at(user) do
1532 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1534 {:ok, updated_user} =
1536 |> change(%{last_digest_emailed_at: now})
1537 |> update_and_set_cache()
1542 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1543 def toggle_confirmation(%User{} = user) do
1544 need_confirmation? = !user.info.confirmation_pending
1547 |> update_info(&User.Info.confirmation_changeset(&1, need_confirmation: need_confirmation?))
1550 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1554 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1555 # use instance-default
1556 config = Pleroma.Config.get([:assets, :mascots])
1557 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1558 mascot = Keyword.get(config, default_mascot)
1561 "id" => "default-mascot",
1562 "url" => mascot[:url],
1563 "preview_url" => mascot[:url],
1565 "mime_type" => mascot[:mime_type]
1570 def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
1572 def ensure_keys_present(%User{} = user) do
1573 with {:ok, pem} <- Keys.generate_rsa_pem() do
1575 |> cast(%{keys: pem}, [:keys])
1576 |> validate_required([:keys])
1577 |> update_and_set_cache()
1581 def get_ap_ids_by_nicknames(nicknames) do
1583 where: u.nickname in ^nicknames,
1589 defdelegate search(query, opts \\ []), to: User.Search
1591 defp put_password_hash(
1592 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1594 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1597 defp put_password_hash(changeset), do: changeset
1599 def is_internal_user?(%User{nickname: nil}), do: true
1600 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1601 def is_internal_user?(_), do: false
1603 # A hack because user delete activities have a fake id for whatever reason
1604 # TODO: Get rid of this
1605 def get_delivered_users_by_object_id("pleroma:fake_object_id"), do: []
1607 def get_delivered_users_by_object_id(object_id) do
1609 inner_join: delivery in assoc(u, :deliveries),
1610 where: delivery.object_id == ^object_id
1615 def change_email(user, email) do
1617 |> cast(%{email: email}, [:email])
1618 |> validate_required([:email])
1619 |> unique_constraint(:email)
1620 |> validate_format(:email, @email_regex)
1621 |> update_and_set_cache()
1625 Changes `user.info` and returns the user changeset.
1627 `fun` is called with the `user.info`.
1629 def change_info(user, fun) do
1630 changeset = change(user)
1631 info = get_field(changeset, :info) || %User.Info{}
1632 put_embed(changeset, :info, fun.(info))
1636 Updates `user.info` and sets cache.
1638 `fun` is called with the `user.info`.
1640 def update_info(users, fun) when is_list(users) do
1641 Repo.transaction(fn ->
1642 for user <- users, do: update_info(user, fun)
1646 def update_info(user, fun) do
1649 |> update_and_set_cache()