1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.User do
13 alias Pleroma.Activity
15 alias Pleroma.Notification
17 alias Pleroma.Registration
19 alias Pleroma.RepoStreamer
22 alias Pleroma.Web.ActivityPub.ActivityPub
23 alias Pleroma.Web.ActivityPub.Utils
24 alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
25 alias Pleroma.Web.OAuth
26 alias Pleroma.Web.OStatus
27 alias Pleroma.Web.RelMe
28 alias Pleroma.Web.Websub
32 @type t :: %__MODULE__{}
34 @primary_key {:id, Pleroma.FlakeId, autogenerate: true}
36 # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
37 @email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
39 @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
40 @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
44 field(:email, :string)
46 field(:nickname, :string)
47 field(:password_hash, :string)
48 field(:password, :string, virtual: true)
49 field(:password_confirmation, :string, virtual: true)
50 field(:following, {:array, :string}, default: [])
51 field(:ap_id, :string)
53 field(:local, :boolean, default: true)
54 field(:follower_address, :string)
55 field(:following_address, :string)
56 field(:search_rank, :float, virtual: true)
57 field(:search_type, :integer, virtual: true)
58 field(:tags, {:array, :string}, default: [])
59 field(:last_refreshed_at, :naive_datetime_usec)
60 field(:last_digest_emailed_at, :naive_datetime)
61 has_many(:notifications, Notification)
62 has_many(:registrations, Registration)
63 embeds_one(:info, User.Info)
68 def auth_active?(%User{info: %User.Info{confirmation_pending: true}}),
69 do: !Pleroma.Config.get([:instance, :account_activation_required])
71 def auth_active?(%User{}), do: true
73 def visible_for?(user, for_user \\ nil)
75 def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
77 def visible_for?(%User{} = user, for_user) do
78 auth_active?(user) || superuser?(for_user)
81 def visible_for?(_, _), do: false
83 def superuser?(%User{local: true, info: %User.Info{is_admin: true}}), do: true
84 def superuser?(%User{local: true, info: %User.Info{is_moderator: true}}), do: true
85 def superuser?(_), do: false
87 def avatar_url(user, options \\ []) do
89 %{"url" => [%{"href" => href} | _]} -> href
90 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
94 def banner_url(user, options \\ []) do
95 case user.info.banner do
96 %{"url" => [%{"href" => href} | _]} -> href
97 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
101 def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url
102 def profile_url(%User{ap_id: ap_id}), do: ap_id
103 def profile_url(_), do: nil
105 def ap_id(%User{nickname: nickname}) do
106 "#{Web.base_url()}/users/#{nickname}"
109 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
110 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
112 @spec ap_following(User.t()) :: Sring.t()
113 def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
114 def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
116 def user_info(%User{} = user, args \\ %{}) do
118 if args[:following_count],
119 do: args[:following_count],
120 else: user.info.following_count || following_count(user)
123 if args[:follower_count], do: args[:follower_count], else: 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 set_info_cache(user, args) do
136 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user, args))
139 @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
140 def restrict_deactivated(query) do
142 where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info)
146 def following_count(%User{following: []}), do: 0
148 def following_count(%User{} = user) do
150 |> get_friends_query()
151 |> Repo.aggregate(:count, :id)
154 def remote_user_creation(params) do
155 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
156 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
158 params = Map.put(params, :info, params[:info] || %{})
159 info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])
163 |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
164 |> validate_required([:name, :ap_id])
165 |> unique_constraint(:nickname)
166 |> validate_format(:nickname, @email_regex)
167 |> validate_length(:bio, max: bio_limit)
168 |> validate_length(:name, max: name_limit)
169 |> put_change(:local, false)
170 |> put_embed(:info, info_cng)
173 case info_cng.changes[:source_data] do
174 %{"followers" => followers, "following" => following} ->
176 |> put_change(:follower_address, followers)
177 |> put_change(:following_address, following)
180 followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
183 |> put_change(:follower_address, followers)
190 def update_changeset(struct, params \\ %{}) do
191 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
192 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
195 |> cast(params, [:bio, :name, :avatar, :following])
196 |> unique_constraint(:nickname)
197 |> validate_format(:nickname, local_nickname_regex())
198 |> validate_length(:bio, max: bio_limit)
199 |> validate_length(:name, min: 1, max: name_limit)
202 def upgrade_changeset(struct, params \\ %{}) do
203 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
204 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
206 params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
207 info_cng = User.Info.user_upgrade(struct.info, params[:info])
218 |> unique_constraint(:nickname)
219 |> validate_format(:nickname, local_nickname_regex())
220 |> validate_length(:bio, max: bio_limit)
221 |> validate_length(:name, max: name_limit)
222 |> put_embed(:info, info_cng)
225 def password_update_changeset(struct, params) do
227 |> cast(params, [:password, :password_confirmation])
228 |> validate_required([:password, :password_confirmation])
229 |> validate_confirmation(:password)
233 @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
234 def reset_password(%User{id: user_id} = user, data) do
237 |> Multi.update(:user, password_update_changeset(user, data))
238 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
239 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
241 case Repo.transaction(multi) do
242 {:ok, %{user: user} = _} -> set_cache(user)
243 {:error, _, changeset, _} -> {:error, changeset}
247 def register_changeset(struct, params \\ %{}, opts \\ []) do
248 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
249 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
252 if is_nil(opts[:need_confirmation]) do
253 Pleroma.Config.get([:instance, :account_activation_required])
255 opts[:need_confirmation]
259 User.Info.confirmation_changeset(%User.Info{}, need_confirmation: need_confirmation?)
263 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
264 |> validate_required([:name, :nickname, :password, :password_confirmation])
265 |> validate_confirmation(:password)
266 |> unique_constraint(:email)
267 |> unique_constraint(:nickname)
268 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
269 |> validate_format(:nickname, local_nickname_regex())
270 |> validate_format(:email, @email_regex)
271 |> validate_length(:bio, max: bio_limit)
272 |> validate_length(:name, min: 1, max: name_limit)
273 |> put_change(:info, info_change)
276 if opts[:external] do
279 validate_required(changeset, [:email])
282 if changeset.valid? do
283 ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
284 followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
288 |> put_change(:ap_id, ap_id)
289 |> unique_constraint(:ap_id)
290 |> put_change(:following, [followers])
291 |> put_change(:follower_address, followers)
297 defp autofollow_users(user) do
298 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
301 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
304 follow_all(user, autofollowed_users)
307 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
308 def register(%Ecto.Changeset{} = changeset) do
309 with {:ok, user} <- Repo.insert(changeset),
310 {:ok, user} <- autofollow_users(user),
311 {:ok, user} <- set_cache(user),
312 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
313 {:ok, _} <- try_send_confirmation_email(user) do
318 def try_send_confirmation_email(%User{} = user) do
319 if user.info.confirmation_pending &&
320 Pleroma.Config.get([:instance, :account_activation_required]) do
322 |> Pleroma.Emails.UserEmail.account_confirmation_email()
323 |> Pleroma.Emails.Mailer.deliver_async()
331 def needs_update?(%User{local: true}), do: false
333 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
335 def needs_update?(%User{local: false} = user) do
336 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
339 def needs_update?(_), do: true
341 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
342 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
346 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
347 follow(follower, followed)
350 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
351 if not User.ap_enabled?(followed) do
352 follow(follower, followed)
358 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
359 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
360 def follow_all(follower, followeds) do
363 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
364 |> Enum.map(fn %{follower_address: fa} -> fa end)
368 where: u.id == ^follower.id,
373 "array(select distinct unnest (array_cat(?, ?)))",
382 {1, [follower]} = Repo.update_all(q, [])
384 Enum.each(followeds, fn followed ->
385 update_follower_count(followed)
391 def follow(%User{} = follower, %User{info: info} = followed) do
392 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
393 ap_followers = followed.follower_address
397 {:error, "Could not follow user: You are deactivated."}
399 deny_follow_blocked and blocks?(followed, follower) ->
400 {:error, "Could not follow user: #{followed.nickname} blocked you."}
403 if !followed.local && follower.local && !ap_enabled?(followed) do
404 Websub.subscribe(follower, followed)
409 where: u.id == ^follower.id,
410 update: [push: [following: ^ap_followers]],
414 {1, [follower]} = Repo.update_all(q, [])
416 follower = maybe_update_following_count(follower)
418 {:ok, _} = update_follower_count(followed)
424 def unfollow(%User{} = follower, %User{} = followed) do
425 ap_followers = followed.follower_address
427 if following?(follower, followed) and follower.ap_id != followed.ap_id do
430 where: u.id == ^follower.id,
431 update: [pull: [following: ^ap_followers]],
435 {1, [follower]} = Repo.update_all(q, [])
437 follower = maybe_update_following_count(follower)
439 {:ok, followed} = update_follower_count(followed)
443 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
445 {:error, "Not subscribed!"}
449 @spec following?(User.t(), User.t()) :: boolean
450 def following?(%User{} = follower, %User{} = followed) do
451 Enum.member?(follower.following, followed.follower_address)
454 def locked?(%User{} = user) do
455 user.info.locked || false
459 Repo.get_by(User, id: id)
462 def get_by_ap_id(ap_id) do
463 Repo.get_by(User, ap_id: ap_id)
466 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
467 # of the ap_id and the domain and tries to get that user
468 def get_by_guessed_nickname(ap_id) do
469 domain = URI.parse(ap_id).host
470 name = List.last(String.split(ap_id, "/"))
471 nickname = "#{name}@#{domain}"
473 get_cached_by_nickname(nickname)
476 def set_cache({:ok, user}), do: set_cache(user)
477 def set_cache({:error, err}), do: {:error, err}
479 def set_cache(%User{} = user) do
480 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
481 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
482 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
486 def update_and_set_cache(changeset) do
487 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
494 def invalidate_cache(user) do
495 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
496 Cachex.del(:user_cache, "nickname:#{user.nickname}")
497 Cachex.del(:user_cache, "user_info:#{user.id}")
500 def get_cached_by_ap_id(ap_id) do
501 key = "ap_id:#{ap_id}"
502 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
505 def get_cached_by_id(id) do
509 Cachex.fetch!(:user_cache, key, fn _ ->
513 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
514 {:commit, user.ap_id}
520 get_cached_by_ap_id(ap_id)
523 def get_cached_by_nickname(nickname) do
524 key = "nickname:#{nickname}"
526 Cachex.fetch!(:user_cache, key, fn ->
527 user_result = get_or_fetch_by_nickname(nickname)
530 {:ok, user} -> {:commit, user}
531 {:error, _error} -> {:ignore, nil}
536 def get_cached_by_nickname_or_id(nickname_or_id) do
537 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
540 def get_by_nickname(nickname) do
541 Repo.get_by(User, nickname: nickname) ||
542 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
543 Repo.get_by(User, nickname: local_nickname(nickname))
547 def get_by_email(email), do: Repo.get_by(User, email: email)
549 def get_by_nickname_or_email(nickname_or_email) do
550 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
553 def get_cached_user_info(user) do
554 key = "user_info:#{user.id}"
555 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
558 def fetch_by_nickname(nickname) do
559 ap_try = ActivityPub.make_user_from_nickname(nickname)
562 {:ok, user} -> {:ok, user}
563 _ -> OStatus.make_user(nickname)
567 def get_or_fetch_by_nickname(nickname) do
568 with %User{} = user <- get_by_nickname(nickname) do
572 with [_nick, _domain] <- String.split(nickname, "@"),
573 {:ok, user} <- fetch_by_nickname(nickname) do
574 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
575 fetch_initial_posts(user)
580 _e -> {:error, "not found " <> nickname}
585 @doc "Fetch some posts when the user has just been federated with"
586 def fetch_initial_posts(user),
587 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:fetch_initial_posts, user])
589 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
590 def get_followers_query(%User{} = user, nil) do
591 User.Query.build(%{followers: user, deactivated: false})
594 def get_followers_query(user, page) do
595 from(u in get_followers_query(user, nil))
596 |> User.Query.paginate(page, 20)
599 @spec get_followers_query(User.t()) :: Ecto.Query.t()
600 def get_followers_query(user), do: get_followers_query(user, nil)
602 @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
603 def get_followers(user, page \\ nil) do
604 q = get_followers_query(user, page)
609 @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
610 def get_external_followers(user, page \\ nil) do
613 |> get_followers_query(page)
614 |> User.Query.build(%{external: true})
619 def get_followers_ids(user, page \\ nil) do
620 q = get_followers_query(user, page)
622 Repo.all(from(u in q, select: u.id))
625 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
626 def get_friends_query(%User{} = user, nil) do
627 User.Query.build(%{friends: user, deactivated: false})
630 def get_friends_query(user, page) do
631 from(u in get_friends_query(user, nil))
632 |> User.Query.paginate(page, 20)
635 @spec get_friends_query(User.t()) :: Ecto.Query.t()
636 def get_friends_query(user), do: get_friends_query(user, nil)
638 def get_friends(user, page \\ nil) do
639 q = get_friends_query(user, page)
644 def get_friends_ids(user, page \\ nil) do
645 q = get_friends_query(user, page)
647 Repo.all(from(u in q, select: u.id))
650 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
651 def get_follow_requests(%User{} = user) do
653 Activity.follow_requests_for_actor(user)
654 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
655 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
656 |> group_by([a, u], u.id)
663 def increase_note_count(%User{} = user) do
665 |> where(id: ^user.id)
670 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
677 |> Repo.update_all([])
679 {1, [user]} -> set_cache(user)
684 def decrease_note_count(%User{} = user) do
686 |> where(id: ^user.id)
691 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
698 |> Repo.update_all([])
700 {1, [user]} -> set_cache(user)
705 def update_note_count(%User{} = user) do
709 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
713 note_count = Repo.one(note_count_query)
715 info_cng = User.Info.set_note_count(user.info, note_count)
719 |> put_embed(:info, info_cng)
720 |> update_and_set_cache()
723 def maybe_fetch_follow_information(user) do
724 with {:ok, user} <- fetch_follow_information(user) do
728 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
734 def fetch_follow_information(user) do
735 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
736 info_cng = User.Info.follow_information_update(user.info, info)
741 |> put_embed(:info, info_cng)
743 update_and_set_cache(changeset)
750 def update_follower_count(%User{} = user) do
751 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
752 follower_count_query =
753 User.Query.build(%{followers: user, deactivated: false})
754 |> select([u], %{count: count(u.id)})
757 |> where(id: ^user.id)
758 |> join(:inner, [u], s in subquery(follower_count_query))
763 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
770 |> Repo.update_all([])
772 {1, [user]} -> set_cache(user)
776 {:ok, maybe_fetch_follow_information(user)}
780 def maybe_update_following_count(%User{local: false} = user) do
781 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
782 {:ok, maybe_fetch_follow_information(user)}
788 def maybe_update_following_count(user), do: user
790 def remove_duplicated_following(%User{following: following} = user) do
791 uniq_following = Enum.uniq(following)
793 if length(following) == length(uniq_following) do
797 |> update_changeset(%{following: uniq_following})
798 |> update_and_set_cache()
802 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
803 def get_users_from_set(ap_ids, local_only \\ true) do
804 criteria = %{ap_id: ap_ids, deactivated: false}
805 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
807 User.Query.build(criteria)
811 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
812 def get_recipients_from_activity(%Activity{recipients: to}) do
813 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
817 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
818 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
822 User.Info.add_to_mutes(info, ap_id)
823 |> User.Info.add_to_muted_notifications(info, ap_id, notifications?)
827 |> put_embed(:info, info_cng)
829 update_and_set_cache(cng)
832 def unmute(muter, %{ap_id: ap_id}) do
836 User.Info.remove_from_mutes(info, ap_id)
837 |> User.Info.remove_from_muted_notifications(info, ap_id)
841 |> put_embed(:info, info_cng)
843 update_and_set_cache(cng)
846 def subscribe(subscriber, %{ap_id: ap_id}) do
847 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
849 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
850 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
853 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
857 |> User.Info.add_to_subscribers(subscriber.ap_id)
860 |> put_embed(:info, info_cng)
861 |> update_and_set_cache()
866 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
867 with %User{} = user <- get_cached_by_ap_id(ap_id) do
870 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
873 |> put_embed(:info, info_cng)
874 |> update_and_set_cache()
878 def block(blocker, %User{ap_id: ap_id} = blocked) do
879 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
881 if following?(blocker, blocked) do
882 {:ok, blocker, _} = unfollow(blocker, blocked)
889 if subscribed_to?(blocked, blocker) do
890 {:ok, blocker} = unsubscribe(blocked, blocker)
896 if following?(blocked, blocker) do
897 unfollow(blocked, blocker)
900 {:ok, blocker} = update_follower_count(blocker)
904 |> User.Info.add_to_block(ap_id)
908 |> put_embed(:info, info_cng)
910 update_and_set_cache(cng)
913 # helper to handle the block given only an actor's AP id
914 def block(blocker, %{ap_id: ap_id}) do
915 block(blocker, get_cached_by_ap_id(ap_id))
918 def unblock(blocker, %{ap_id: ap_id}) do
921 |> User.Info.remove_from_block(ap_id)
925 |> put_embed(:info, info_cng)
927 update_and_set_cache(cng)
930 def mutes?(nil, _), do: false
931 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
933 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
934 def muted_notifications?(nil, _), do: false
936 def muted_notifications?(user, %{ap_id: ap_id}),
937 do: Enum.member?(user.info.muted_notifications, ap_id)
939 def blocks?(%User{} = user, %User{} = target) do
940 blocks_ap_id?(user, target) || blocks_domain?(user, target)
943 def blocks?(nil, _), do: false
945 def blocks_ap_id?(%User{} = user, %User{} = target) do
946 Enum.member?(user.info.blocks, target.ap_id)
949 def blocks_ap_id?(_, _), do: false
951 def blocks_domain?(%User{} = user, %User{} = target) do
952 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
953 %{host: host} = URI.parse(target.ap_id)
954 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
957 def blocks_domain?(_, _), do: false
959 def subscribed_to?(user, %{ap_id: ap_id}) do
960 with %User{} = target <- get_cached_by_ap_id(ap_id) do
961 Enum.member?(target.info.subscribers, user.ap_id)
965 @spec muted_users(User.t()) :: [User.t()]
966 def muted_users(user) do
967 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
971 @spec blocked_users(User.t()) :: [User.t()]
972 def blocked_users(user) do
973 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
977 @spec subscribers(User.t()) :: [User.t()]
978 def subscribers(user) do
979 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
983 def block_domain(user, domain) do
986 |> User.Info.add_to_domain_block(domain)
990 |> put_embed(:info, info_cng)
992 update_and_set_cache(cng)
995 def unblock_domain(user, domain) do
998 |> User.Info.remove_from_domain_block(domain)
1002 |> put_embed(:info, info_cng)
1004 update_and_set_cache(cng)
1007 def deactivate_async(user, status \\ true) do
1008 PleromaJobQueue.enqueue(:background, __MODULE__, [:deactivate_async, user, status])
1011 def deactivate(%User{} = user, status \\ true) do
1012 info_cng = User.Info.set_activation_status(user.info, status)
1014 with {:ok, friends} <- User.get_friends(user),
1015 {:ok, followers} <- User.get_followers(user),
1019 |> put_embed(:info, info_cng)
1020 |> update_and_set_cache() do
1021 Enum.each(followers, &invalidate_cache(&1))
1022 Enum.each(friends, &update_follower_count(&1))
1028 def update_notification_settings(%User{} = user, settings \\ %{}) do
1029 info_changeset = User.Info.update_notification_settings(user.info, settings)
1032 |> put_embed(:info, info_changeset)
1033 |> update_and_set_cache()
1036 @spec delete(User.t()) :: :ok
1037 def delete(%User{} = user),
1038 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:delete, user])
1040 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1041 def perform(:delete, %User{} = user) do
1042 {:ok, _user} = ActivityPub.delete(user)
1044 # Remove all relationships
1045 {:ok, followers} = User.get_followers(user)
1047 Enum.each(followers, fn follower ->
1048 ActivityPub.unfollow(follower, user)
1049 User.unfollow(follower, user)
1052 {:ok, friends} = User.get_friends(user)
1054 Enum.each(friends, fn followed ->
1055 ActivityPub.unfollow(user, followed)
1056 User.unfollow(user, followed)
1059 delete_user_activities(user)
1060 invalidate_cache(user)
1064 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1065 def perform(:fetch_initial_posts, %User{} = user) do
1066 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1069 # Insert all the posts in reverse order, so they're in the right order on the timeline
1070 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
1071 &Pleroma.Web.Federator.incoming_ap_doc/1
1077 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1079 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1080 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1081 when is_list(blocked_identifiers) do
1083 blocked_identifiers,
1084 fn blocked_identifier ->
1085 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1086 {:ok, blocker} <- block(blocker, blocked),
1087 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1091 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1098 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1099 def perform(:follow_import, %User{} = follower, followed_identifiers)
1100 when is_list(followed_identifiers) do
1102 followed_identifiers,
1103 fn followed_identifier ->
1104 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1105 {:ok, follower} <- maybe_direct_follow(follower, followed),
1106 {:ok, _} <- ActivityPub.follow(follower, followed) do
1110 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1117 @spec external_users_query() :: Ecto.Query.t()
1118 def external_users_query do
1126 @spec external_users(keyword()) :: [User.t()]
1127 def external_users(opts \\ []) do
1129 external_users_query()
1130 |> select([u], struct(u, [:id, :ap_id, :info]))
1134 do: where(query, [u], u.id > ^opts[:max_id]),
1139 do: limit(query, ^opts[:limit]),
1145 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers),
1147 PleromaJobQueue.enqueue(:background, __MODULE__, [
1153 def follow_import(%User{} = follower, followed_identifiers) when is_list(followed_identifiers),
1155 PleromaJobQueue.enqueue(:background, __MODULE__, [
1158 followed_identifiers
1161 def delete_user_activities(%User{ap_id: ap_id} = user) do
1163 |> Activity.query_by_actor()
1164 |> RepoStreamer.chunk_stream(50)
1165 |> Stream.each(fn activities ->
1166 Enum.each(activities, &delete_activity(&1))
1173 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1175 |> Object.normalize()
1176 |> ActivityPub.delete()
1179 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1180 user = get_cached_by_ap_id(activity.actor)
1181 object = Object.normalize(activity)
1183 ActivityPub.unlike(user, object)
1186 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1187 user = get_cached_by_ap_id(activity.actor)
1188 object = Object.normalize(activity)
1190 ActivityPub.unannounce(user, object)
1193 defp delete_activity(_activity), do: "Doing nothing"
1195 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1196 Pleroma.HTML.Scrubber.TwitterText
1199 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1201 def fetch_by_ap_id(ap_id) do
1202 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1209 case OStatus.make_user(ap_id) do
1210 {:ok, user} -> {:ok, user}
1211 _ -> {:error, "Could not fetch by AP id"}
1216 def get_or_fetch_by_ap_id(ap_id) do
1217 user = get_cached_by_ap_id(ap_id)
1219 if !is_nil(user) and !User.needs_update?(user) do
1222 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1223 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1225 resp = fetch_by_ap_id(ap_id)
1227 if should_fetch_initial do
1228 with {:ok, %User{} = user} <- resp do
1229 fetch_initial_posts(user)
1237 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1238 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1239 if user = get_cached_by_ap_id(uri) do
1243 %User{info: %User.Info{}}
1244 |> cast(%{}, [:ap_id, :nickname, :local])
1245 |> put_change(:ap_id, uri)
1246 |> put_change(:nickname, nickname)
1247 |> put_change(:local, true)
1248 |> put_change(:follower_address, uri <> "/followers")
1250 {:ok, user} = Repo.insert(changes)
1256 def public_key_from_info(%{
1257 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1261 |> :public_key.pem_decode()
1263 |> :public_key.pem_entry_decode()
1269 def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
1270 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1273 def public_key_from_info(_), do: {:error, "not found key"}
1275 def get_public_key_for_ap_id(ap_id) do
1276 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1277 {:ok, public_key} <- public_key_from_info(user.info) do
1284 defp blank?(""), do: nil
1285 defp blank?(n), do: n
1287 def insert_or_update_user(data) do
1289 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1290 |> remote_user_creation()
1291 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1295 def ap_enabled?(%User{local: true}), do: true
1296 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1297 def ap_enabled?(_), do: false
1299 @doc "Gets or fetch a user by uri or nickname."
1300 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1301 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1302 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1304 # wait a period of time and return newest version of the User structs
1305 # this is because we have synchronous follow APIs and need to simulate them
1306 # with an async handshake
1307 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1308 with %User{} = a <- User.get_cached_by_id(a.id),
1309 %User{} = b <- User.get_cached_by_id(b.id) do
1317 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1318 with :ok <- :timer.sleep(timeout),
1319 %User{} = a <- User.get_cached_by_id(a.id),
1320 %User{} = b <- User.get_cached_by_id(b.id) do
1328 def parse_bio(bio) when is_binary(bio) and bio != "" do
1330 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1334 def parse_bio(_), do: ""
1336 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1337 # TODO: get profile URLs other than user.ap_id
1338 profile_urls = [user.ap_id]
1341 |> CommonUtils.format_input("text/plain",
1342 mentions_format: :full,
1343 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1348 def parse_bio(_, _), do: ""
1350 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1351 Repo.transaction(fn ->
1352 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1356 def tag(nickname, tags) when is_binary(nickname),
1357 do: tag(get_by_nickname(nickname), tags)
1359 def tag(%User{} = user, tags),
1360 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1362 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1363 Repo.transaction(fn ->
1364 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1368 def untag(nickname, tags) when is_binary(nickname),
1369 do: untag(get_by_nickname(nickname), tags)
1371 def untag(%User{} = user, tags),
1372 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1374 defp update_tags(%User{} = user, new_tags) do
1375 {:ok, updated_user} =
1377 |> change(%{tags: new_tags})
1378 |> update_and_set_cache()
1383 defp normalize_tags(tags) do
1386 |> Enum.map(&String.downcase(&1))
1389 defp local_nickname_regex do
1390 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1391 @extended_local_nickname_regex
1393 @strict_local_nickname_regex
1397 def local_nickname(nickname_or_mention) do
1400 |> String.split("@")
1404 def full_nickname(nickname_or_mention),
1405 do: String.trim_leading(nickname_or_mention, "@")
1407 def error_user(ap_id) do
1412 nickname: "erroruser@example.com",
1413 inserted_at: NaiveDateTime.utc_now()
1417 @spec all_superusers() :: [User.t()]
1418 def all_superusers do
1419 User.Query.build(%{super_users: true, local: true, deactivated: false})
1423 def showing_reblogs?(%User{} = user, %User{} = target) do
1424 target.ap_id not in user.info.muted_reblogs
1428 The function returns a query to get users with no activity for given interval of days.
1429 Inactive users are those who didn't read any notification, or had any activity where
1430 the user is the activity's actor, during `inactivity_threshold` days.
1431 Deactivated users will not appear in this list.
1435 iex> Pleroma.User.list_inactive_users()
1438 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1439 def list_inactive_users_query(inactivity_threshold \\ 7) do
1440 negative_inactivity_threshold = -inactivity_threshold
1441 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1442 # Subqueries are not supported in `where` clauses, join gets too complicated.
1443 has_read_notifications =
1444 from(n in Pleroma.Notification,
1445 where: n.seen == true,
1447 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1450 |> Pleroma.Repo.all()
1452 from(u in Pleroma.User,
1453 left_join: a in Pleroma.Activity,
1454 on: u.ap_id == a.actor,
1455 where: not is_nil(u.nickname),
1456 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1457 where: u.id not in ^has_read_notifications,
1460 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1461 is_nil(max(a.inserted_at))
1466 Enable or disable email notifications for user
1470 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1471 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1473 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1474 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1476 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1477 {:ok, t()} | {:error, Ecto.Changeset.t()}
1478 def switch_email_notifications(user, type, status) do
1479 info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})
1482 |> put_embed(:info, info)
1483 |> update_and_set_cache()
1487 Set `last_digest_emailed_at` value for the user to current time
1489 @spec touch_last_digest_emailed_at(t()) :: t()
1490 def touch_last_digest_emailed_at(user) do
1491 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1493 {:ok, updated_user} =
1495 |> change(%{last_digest_emailed_at: now})
1496 |> update_and_set_cache()
1501 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1502 def toggle_confirmation(%User{} = user) do
1503 need_confirmation? = !user.info.confirmation_pending
1506 User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
1510 |> put_embed(:info, info_changeset)
1511 |> update_and_set_cache()
1514 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1518 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1519 # use instance-default
1520 config = Pleroma.Config.get([:assets, :mascots])
1521 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1522 mascot = Keyword.get(config, default_mascot)
1525 "id" => "default-mascot",
1526 "url" => mascot[:url],
1527 "preview_url" => mascot[:url],
1529 "mime_type" => mascot[:mime_type]
1534 def ensure_keys_present(%User{info: info} = user) do
1538 {:ok, pem} = Keys.generate_rsa_pem()
1541 |> Ecto.Changeset.change()
1542 |> Ecto.Changeset.put_embed(:info, User.Info.set_keys(info, pem))
1543 |> update_and_set_cache()
1547 def get_ap_ids_by_nicknames(nicknames) do
1549 where: u.nickname in ^nicknames,
1555 defdelegate search(query, opts \\ []), to: User.Search
1557 defp put_password_hash(
1558 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1560 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1563 defp put_password_hash(changeset), do: changeset
1565 def is_internal_user?(%User{nickname: nil}), do: true
1566 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1567 def is_internal_user?(_), do: false