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
12 alias Pleroma.Activity
13 alias Pleroma.Notification
15 alias Pleroma.Registration
19 alias Pleroma.Web.ActivityPub.ActivityPub
20 alias Pleroma.Web.ActivityPub.Utils
21 alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
22 alias Pleroma.Web.OAuth
23 alias Pleroma.Web.OStatus
24 alias Pleroma.Web.RelMe
25 alias Pleroma.Web.Websub
29 @type t :: %__MODULE__{}
31 @primary_key {:id, Pleroma.FlakeId, autogenerate: true}
33 # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
34 @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])?)*$/
36 @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
37 @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
41 field(:email, :string)
43 field(:nickname, :string)
44 field(:password_hash, :string)
45 field(:password, :string, virtual: true)
46 field(:password_confirmation, :string, virtual: true)
47 field(:following, {:array, :string}, default: [])
48 field(:ap_id, :string)
50 field(:local, :boolean, default: true)
51 field(:follower_address, :string)
52 field(:search_rank, :float, virtual: true)
53 field(:search_type, :integer, virtual: true)
54 field(:tags, {:array, :string}, default: [])
55 field(:last_refreshed_at, :naive_datetime_usec)
56 field(:last_digest_emailed_at, :naive_datetime)
57 has_many(:notifications, Notification)
58 has_many(:registrations, Registration)
59 embeds_one(:info, Pleroma.User.Info)
64 def auth_active?(%User{info: %User.Info{confirmation_pending: true}}),
65 do: !Pleroma.Config.get([:instance, :account_activation_required])
67 def auth_active?(%User{}), do: true
69 def visible_for?(user, for_user \\ nil)
71 def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
73 def visible_for?(%User{} = user, for_user) do
74 auth_active?(user) || superuser?(for_user)
77 def visible_for?(_, _), do: false
79 def superuser?(%User{local: true, info: %User.Info{is_admin: true}}), do: true
80 def superuser?(%User{local: true, info: %User.Info{is_moderator: true}}), do: true
81 def superuser?(_), do: false
83 def avatar_url(user, options \\ []) do
85 %{"url" => [%{"href" => href} | _]} -> href
86 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
90 def banner_url(user, options \\ []) do
91 case user.info.banner do
92 %{"url" => [%{"href" => href} | _]} -> href
93 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
97 def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url
98 def profile_url(%User{ap_id: ap_id}), do: ap_id
99 def profile_url(_), do: nil
101 def ap_id(%User{nickname: nickname}) do
102 "#{Web.base_url()}/users/#{nickname}"
105 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
106 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
108 def user_info(%User{} = user) do
109 oneself = if user.local, do: 1, else: 0
112 following_count: length(user.following) - oneself,
113 note_count: user.info.note_count,
114 follower_count: user.info.follower_count,
115 locked: user.info.locked,
116 confirmation_pending: user.info.confirmation_pending,
117 default_scope: user.info.default_scope
121 def remote_user_creation(params) do
124 |> Map.put(:info, params[:info] || %{})
126 info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])
130 |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
131 |> validate_required([:name, :ap_id])
132 |> unique_constraint(:nickname)
133 |> validate_format(:nickname, @email_regex)
134 |> validate_length(:bio, max: 5000)
135 |> validate_length(:name, max: 100)
136 |> put_change(:local, false)
137 |> put_embed(:info, info_cng)
140 case info_cng.changes[:source_data] do
141 %{"followers" => followers} ->
143 |> put_change(:follower_address, followers)
146 followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
149 |> put_change(:follower_address, followers)
156 def update_changeset(struct, params \\ %{}) do
158 |> cast(params, [:bio, :name, :avatar])
159 |> unique_constraint(:nickname)
160 |> validate_format(:nickname, local_nickname_regex())
161 |> validate_length(:bio, max: 5000)
162 |> validate_length(:name, min: 1, max: 100)
165 def upgrade_changeset(struct, params \\ %{}) do
168 |> Map.put(:last_refreshed_at, NaiveDateTime.utc_now())
172 |> User.Info.user_upgrade(params[:info])
175 |> cast(params, [:bio, :name, :follower_address, :avatar, :last_refreshed_at])
176 |> unique_constraint(:nickname)
177 |> validate_format(:nickname, local_nickname_regex())
178 |> validate_length(:bio, max: 5000)
179 |> validate_length(:name, max: 100)
180 |> put_embed(:info, info_cng)
183 def password_update_changeset(struct, params) do
186 |> cast(params, [:password, :password_confirmation])
187 |> validate_required([:password, :password_confirmation])
188 |> validate_confirmation(:password)
190 OAuth.Token.delete_user_tokens(struct)
191 OAuth.Authorization.delete_user_authorizations(struct)
193 if changeset.valid? do
194 hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
197 |> put_change(:password_hash, hashed)
203 def reset_password(user, data) do
204 update_and_set_cache(password_update_changeset(user, data))
207 def register_changeset(struct, params \\ %{}, opts \\ []) do
208 confirmation_status =
209 if opts[:confirmed] || !Pleroma.Config.get([:instance, :account_activation_required]) do
215 info_change = User.Info.confirmation_changeset(%User.Info{}, confirmation_status)
219 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
220 |> validate_required([:name, :nickname, :password, :password_confirmation])
221 |> validate_confirmation(:password)
222 |> unique_constraint(:email)
223 |> unique_constraint(:nickname)
224 |> validate_exclusion(:nickname, Pleroma.Config.get([Pleroma.User, :restricted_nicknames]))
225 |> validate_format(:nickname, local_nickname_regex())
226 |> validate_format(:email, @email_regex)
227 |> validate_length(:bio, max: 1000)
228 |> validate_length(:name, min: 1, max: 100)
229 |> put_change(:info, info_change)
232 if opts[:external] do
235 validate_required(changeset, [:email])
238 if changeset.valid? do
239 hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
240 ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
241 followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
244 |> put_change(:password_hash, hashed)
245 |> put_change(:ap_id, ap_id)
246 |> unique_constraint(:ap_id)
247 |> put_change(:following, [followers])
248 |> put_change(:follower_address, followers)
254 defp autofollow_users(user) do
255 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
259 where: u.local == true,
260 where: u.nickname in ^candidates
264 follow_all(user, autofollowed_users)
267 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
268 def register(%Ecto.Changeset{} = changeset) do
269 with {:ok, user} <- Repo.insert(changeset),
270 {:ok, user} <- autofollow_users(user),
271 {:ok, user} <- set_cache(user),
272 {:ok, _} <- Pleroma.User.WelcomeMessage.post_welcome_message_to_user(user),
273 {:ok, _} <- try_send_confirmation_email(user) do
278 def try_send_confirmation_email(%User{} = user) do
279 if user.info.confirmation_pending &&
280 Pleroma.Config.get([:instance, :account_activation_required]) do
282 |> Pleroma.Emails.UserEmail.account_confirmation_email()
283 |> Pleroma.Emails.Mailer.deliver_async()
291 def needs_update?(%User{local: true}), do: false
293 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
295 def needs_update?(%User{local: false} = user) do
296 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
299 def needs_update?(_), do: true
301 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
305 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
306 follow(follower, followed)
309 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
310 if not User.ap_enabled?(followed) do
311 follow(follower, followed)
317 def maybe_follow(%User{} = follower, %User{info: _info} = followed) do
318 if not following?(follower, followed) do
319 follow(follower, followed)
325 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
326 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
327 def follow_all(follower, followeds) do
330 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
331 |> Enum.map(fn %{follower_address: fa} -> fa end)
335 where: u.id == ^follower.id,
340 "array(select distinct unnest (array_cat(?, ?)))",
349 {1, [follower]} = Repo.update_all(q, [])
351 Enum.each(followeds, fn followed ->
352 update_follower_count(followed)
358 def follow(%User{} = follower, %User{info: info} = followed) do
359 user_config = Application.get_env(:pleroma, :user)
360 deny_follow_blocked = Keyword.get(user_config, :deny_follow_blocked)
362 ap_followers = followed.follower_address
365 following?(follower, followed) or info.deactivated ->
366 {:error, "Could not follow user: #{followed.nickname} is already on your list."}
368 deny_follow_blocked and blocks?(followed, follower) ->
369 {:error, "Could not follow user: #{followed.nickname} blocked you."}
372 if !followed.local && follower.local && !ap_enabled?(followed) do
373 Websub.subscribe(follower, followed)
378 where: u.id == ^follower.id,
379 update: [push: [following: ^ap_followers]],
383 {1, [follower]} = Repo.update_all(q, [])
385 {:ok, _} = update_follower_count(followed)
391 def unfollow(%User{} = follower, %User{} = followed) do
392 ap_followers = followed.follower_address
394 if following?(follower, followed) and follower.ap_id != followed.ap_id do
397 where: u.id == ^follower.id,
398 update: [pull: [following: ^ap_followers]],
402 {1, [follower]} = Repo.update_all(q, [])
404 {:ok, followed} = update_follower_count(followed)
408 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
410 {:error, "Not subscribed!"}
414 @spec following?(User.t(), User.t()) :: boolean
415 def following?(%User{} = follower, %User{} = followed) do
416 Enum.member?(follower.following, followed.follower_address)
419 def follow_import(%User{} = follower, followed_identifiers)
420 when is_list(followed_identifiers) do
422 followed_identifiers,
423 fn followed_identifier ->
424 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
425 {:ok, follower} <- maybe_direct_follow(follower, followed),
426 {:ok, _} <- ActivityPub.follow(follower, followed) do
430 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
437 def locked?(%User{} = user) do
438 user.info.locked || false
442 Repo.get_by(User, id: id)
445 def get_by_ap_id(ap_id) do
446 Repo.get_by(User, ap_id: ap_id)
449 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
450 # of the ap_id and the domain and tries to get that user
451 def get_by_guessed_nickname(ap_id) do
452 domain = URI.parse(ap_id).host
453 name = List.last(String.split(ap_id, "/"))
454 nickname = "#{name}@#{domain}"
456 get_cached_by_nickname(nickname)
459 def set_cache({:ok, user}), do: set_cache(user)
460 def set_cache({:error, err}), do: {:error, err}
462 def set_cache(%User{} = user) do
463 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
464 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
465 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
469 def update_and_set_cache(changeset) do
470 with {:ok, user} <- Repo.update(changeset) do
477 def invalidate_cache(user) do
478 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
479 Cachex.del(:user_cache, "nickname:#{user.nickname}")
480 Cachex.del(:user_cache, "user_info:#{user.id}")
483 def get_cached_by_ap_id(ap_id) do
484 key = "ap_id:#{ap_id}"
485 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
488 def get_cached_by_id(id) do
492 Cachex.fetch!(:user_cache, key, fn _ ->
496 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
497 {:commit, user.ap_id}
503 get_cached_by_ap_id(ap_id)
506 def get_cached_by_nickname(nickname) do
507 key = "nickname:#{nickname}"
509 Cachex.fetch!(:user_cache, key, fn ->
510 user_result = get_or_fetch_by_nickname(nickname)
513 {:ok, user} -> {:commit, user}
514 {:error, _error} -> {:ignore, nil}
519 def get_cached_by_nickname_or_id(nickname_or_id) do
520 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
523 def get_by_nickname(nickname) do
524 Repo.get_by(User, nickname: nickname) ||
525 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
526 Repo.get_by(User, nickname: local_nickname(nickname))
530 def get_by_email(email), do: Repo.get_by(User, email: email)
532 def get_by_nickname_or_email(nickname_or_email) do
533 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
536 def get_cached_user_info(user) do
537 key = "user_info:#{user.id}"
538 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
541 def fetch_by_nickname(nickname) do
542 ap_try = ActivityPub.make_user_from_nickname(nickname)
545 {:ok, user} -> {:ok, user}
546 _ -> OStatus.make_user(nickname)
550 def get_or_fetch_by_nickname(nickname) do
551 with %User{} = user <- get_by_nickname(nickname) do
555 with [_nick, _domain] <- String.split(nickname, "@"),
556 {:ok, user} <- fetch_by_nickname(nickname) do
557 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
559 {:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])
564 _e -> {:error, "not found " <> nickname}
569 @doc "Fetch some posts when the user has just been federated with"
570 def fetch_initial_posts(user) do
571 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
574 # Insert all the posts in reverse order, so they're in the right order on the timeline
575 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
576 &Pleroma.Web.Federator.incoming_ap_doc/1
580 def get_followers_query(%User{id: id, follower_address: follower_address}, nil) do
583 where: fragment("? <@ ?", ^[follower_address], u.following),
588 def get_followers_query(user, page) do
589 from(u in get_followers_query(user, nil))
590 |> paginate(page, 20)
593 def get_followers_query(user), do: get_followers_query(user, nil)
595 def get_followers(user, page \\ nil) do
596 q = get_followers_query(user, page)
601 def get_followers_ids(user, page \\ nil) do
602 q = get_followers_query(user, page)
604 Repo.all(from(u in q, select: u.id))
607 def get_friends_query(%User{id: id, following: following}, nil) do
610 where: u.follower_address in ^following,
615 def get_friends_query(user, page) do
616 from(u in get_friends_query(user, nil))
617 |> paginate(page, 20)
620 def get_friends_query(user), do: get_friends_query(user, nil)
622 def get_friends(user, page \\ nil) do
623 q = get_friends_query(user, page)
628 def get_friends_ids(user, page \\ nil) do
629 q = get_friends_query(user, page)
631 Repo.all(from(u in q, select: u.id))
634 def get_follow_requests_query(%User{} = user) do
639 "? ->> 'type' = 'Follow'",
644 "? ->> 'state' = 'pending'",
649 "coalesce((?)->'object'->>'id', (?)->>'object') = ?",
657 def get_follow_requests(%User{} = user) do
660 |> User.get_follow_requests_query()
661 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
662 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
663 |> group_by([a, u], u.id)
670 def increase_note_count(%User{} = user) do
672 |> where(id: ^user.id)
677 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
684 |> Repo.update_all([])
686 {1, [user]} -> set_cache(user)
691 def decrease_note_count(%User{} = user) do
693 |> where(id: ^user.id)
698 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
705 |> Repo.update_all([])
707 {1, [user]} -> set_cache(user)
712 def update_note_count(%User{} = user) do
716 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
720 note_count = Repo.one(note_count_query)
722 info_cng = User.Info.set_note_count(user.info, note_count)
726 |> put_embed(:info, info_cng)
728 update_and_set_cache(cng)
731 def update_follower_count(%User{} = user) do
732 follower_count_query =
734 |> where([u], ^user.follower_address in u.following)
735 |> where([u], u.id != ^user.id)
736 |> select([u], %{count: count(u.id)})
739 |> where(id: ^user.id)
740 |> join(:inner, [u], s in subquery(follower_count_query))
745 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
752 |> Repo.update_all([])
754 {1, [user]} -> set_cache(user)
759 def get_users_from_set_query(ap_ids, false) do
762 where: u.ap_id in ^ap_ids
766 def get_users_from_set_query(ap_ids, true) do
767 query = get_users_from_set_query(ap_ids, false)
771 where: u.local == true
775 def get_users_from_set(ap_ids, local_only \\ true) do
776 get_users_from_set_query(ap_ids, local_only)
780 def get_recipients_from_activity(%Activity{recipients: to}) do
784 where: u.ap_id in ^to,
785 or_where: fragment("? && ?", u.following, ^to)
788 query = from(u in query, where: u.local == true)
793 def search(query, resolve \\ false, for_user \\ nil) do
794 # Strip the beginning @ off if there is a query
795 query = String.trim_leading(query, "@")
797 if resolve, do: get_or_fetch(query)
800 Repo.transaction(fn ->
801 Ecto.Adapters.SQL.query(Repo, "select set_limit(0.25)", [])
802 Repo.all(search_query(query, for_user))
808 def search_query(query, for_user) do
809 fts_subquery = fts_search_subquery(query)
810 trigram_subquery = trigram_search_subquery(query)
811 union_query = from(s in trigram_subquery, union_all: ^fts_subquery)
812 distinct_query = from(s in subquery(union_query), order_by: s.search_type, distinct: s.id)
814 from(s in subquery(boost_search_rank_query(distinct_query, for_user)),
815 order_by: [desc: s.search_rank],
820 defp boost_search_rank_query(query, nil), do: query
822 defp boost_search_rank_query(query, for_user) do
823 friends_ids = get_friends_ids(for_user)
824 followers_ids = get_followers_ids(for_user)
826 from(u in subquery(query),
831 CASE WHEN (?) THEN (?) * 1.3
832 WHEN (?) THEN (?) * 1.2
833 WHEN (?) THEN (?) * 1.1
836 u.id in ^friends_ids and u.id in ^followers_ids,
838 u.id in ^friends_ids,
840 u.id in ^followers_ids,
848 defp fts_search_subquery(term, query \\ User) do
851 |> String.replace(~r/\W+/, " ")
854 |> Enum.map(&(&1 <> ":*"))
865 setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
866 setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B'),
867 to_tsquery('simple', ?),
879 (setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
880 setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B')) @@ to_tsquery('simple', ?)
889 defp trigram_search_subquery(term) do
893 # ^1 gives 'Postgrex expected a binary, got 1' for some weird reason
894 search_type: fragment("?", 1),
897 "similarity(?, trim(? || ' ' || coalesce(?, '')))",
903 where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term)
907 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
910 fn blocked_identifier ->
911 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
912 {:ok, blocker} <- block(blocker, blocked),
913 {:ok, _} <- ActivityPub.block(blocker, blocked) do
917 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
924 def mute(muter, %User{ap_id: ap_id}) do
927 |> User.Info.add_to_mutes(ap_id)
931 |> put_embed(:info, info_cng)
933 update_and_set_cache(cng)
936 def unmute(muter, %{ap_id: ap_id}) do
939 |> User.Info.remove_from_mutes(ap_id)
943 |> put_embed(:info, info_cng)
945 update_and_set_cache(cng)
948 def subscribe(subscriber, %{ap_id: ap_id}) do
949 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
951 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
952 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
955 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
959 |> User.Info.add_to_subscribers(subscriber.ap_id)
962 |> put_embed(:info, info_cng)
963 |> update_and_set_cache()
968 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
969 with %User{} = user <- get_cached_by_ap_id(ap_id) do
972 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
975 |> put_embed(:info, info_cng)
976 |> update_and_set_cache()
980 def block(blocker, %User{ap_id: ap_id} = blocked) do
981 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
983 if following?(blocker, blocked) do
984 {:ok, blocker, _} = unfollow(blocker, blocked)
991 if subscribed_to?(blocked, blocker) do
992 {:ok, blocker} = unsubscribe(blocked, blocker)
998 if following?(blocked, blocker) do
999 unfollow(blocked, blocker)
1002 {:ok, blocker} = update_follower_count(blocker)
1006 |> User.Info.add_to_block(ap_id)
1010 |> put_embed(:info, info_cng)
1012 update_and_set_cache(cng)
1015 # helper to handle the block given only an actor's AP id
1016 def block(blocker, %{ap_id: ap_id}) do
1017 block(blocker, get_cached_by_ap_id(ap_id))
1020 def unblock(blocker, %{ap_id: ap_id}) do
1023 |> User.Info.remove_from_block(ap_id)
1027 |> put_embed(:info, info_cng)
1029 update_and_set_cache(cng)
1032 def mutes?(nil, _), do: false
1033 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
1035 def blocks?(user, %{ap_id: ap_id}) do
1036 blocks = user.info.blocks
1037 domain_blocks = user.info.domain_blocks
1038 %{host: host} = URI.parse(ap_id)
1040 Enum.member?(blocks, ap_id) ||
1041 Enum.any?(domain_blocks, fn domain ->
1046 def subscribed_to?(user, %{ap_id: ap_id}) do
1047 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1048 Enum.member?(target.info.subscribers, user.ap_id)
1052 def muted_users(user),
1053 do: Repo.all(from(u in User, where: u.ap_id in ^user.info.mutes))
1055 def blocked_users(user),
1056 do: Repo.all(from(u in User, where: u.ap_id in ^user.info.blocks))
1058 def subscribers(user),
1059 do: Repo.all(from(u in User, where: u.ap_id in ^user.info.subscribers))
1061 def block_domain(user, domain) do
1064 |> User.Info.add_to_domain_block(domain)
1068 |> put_embed(:info, info_cng)
1070 update_and_set_cache(cng)
1073 def unblock_domain(user, domain) do
1076 |> User.Info.remove_from_domain_block(domain)
1080 |> put_embed(:info, info_cng)
1082 update_and_set_cache(cng)
1085 def maybe_local_user_query(query, local) do
1086 if local, do: local_user_query(query), else: query
1089 def local_user_query(query \\ User) do
1092 where: u.local == true,
1093 where: not is_nil(u.nickname)
1097 def maybe_external_user_query(query, external) do
1098 if external, do: external_user_query(query), else: query
1101 def external_user_query(query \\ User) do
1104 where: u.local == false,
1105 where: not is_nil(u.nickname)
1109 def maybe_active_user_query(query, active) do
1110 if active, do: active_user_query(query), else: query
1113 def active_user_query(query \\ User) do
1116 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1117 where: not is_nil(u.nickname)
1121 def maybe_deactivated_user_query(query, deactivated) do
1122 if deactivated, do: deactivated_user_query(query), else: query
1125 def deactivated_user_query(query \\ User) do
1128 where: fragment("(?->'deactivated' @> 'true')", u.info),
1129 where: not is_nil(u.nickname)
1133 def active_local_user_query do
1135 u in local_user_query(),
1136 where: fragment("not (?->'deactivated' @> 'true')", u.info)
1140 def moderator_user_query do
1143 where: u.local == true,
1144 where: fragment("?->'is_moderator' @> 'true'", u.info)
1148 def deactivate(%User{} = user, status \\ true) do
1149 info_cng = User.Info.set_activation_status(user.info, status)
1153 |> put_embed(:info, info_cng)
1155 update_and_set_cache(cng)
1158 def update_notification_settings(%User{} = user, settings \\ %{}) do
1159 info_changeset = User.Info.update_notification_settings(user.info, settings)
1162 |> put_embed(:info, info_changeset)
1163 |> update_and_set_cache()
1166 @spec delete(User.t()) :: :ok
1167 def delete(%User{} = user),
1168 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:delete, user])
1170 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1171 def perform(:delete, %User{} = user) do
1172 {:ok, user} = User.deactivate(user)
1174 # Remove all relationships
1175 {:ok, followers} = User.get_followers(user)
1177 Enum.each(followers, fn follower -> User.unfollow(follower, user) end)
1179 {:ok, friends} = User.get_friends(user)
1181 Enum.each(friends, fn followed -> User.unfollow(user, followed) end)
1183 delete_user_activities(user)
1186 def delete_user_activities(%User{ap_id: ap_id} = user) do
1189 |> Activity.query_by_actor()
1190 |> Activity.with_preloaded_object()
1193 Repo.transaction(fn -> Enum.each(stream, &delete_activity(&1)) end, timeout: :infinity)
1198 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1199 Object.normalize(activity) |> ActivityPub.delete()
1202 defp delete_activity(_activity), do: "Doing nothing"
1204 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1205 Pleroma.HTML.Scrubber.TwitterText
1208 @default_scrubbers Pleroma.Config.get([:markup, :scrub_policy])
1210 def html_filter_policy(_), do: @default_scrubbers
1212 def fetch_by_ap_id(ap_id) do
1213 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1220 case OStatus.make_user(ap_id) do
1221 {:ok, user} -> {:ok, user}
1222 _ -> {:error, "Could not fetch by AP id"}
1227 def get_or_fetch_by_ap_id(ap_id) do
1228 user = get_cached_by_ap_id(ap_id)
1230 if !is_nil(user) and !User.needs_update?(user) do
1233 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1234 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1236 resp = fetch_by_ap_id(ap_id)
1238 if should_fetch_initial do
1239 with {:ok, %User{} = user} = resp do
1240 {:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])
1248 def get_or_create_instance_user do
1249 relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"
1251 if user = get_cached_by_ap_id(relay_uri) do
1255 %User{info: %User.Info{}}
1256 |> cast(%{}, [:ap_id, :nickname, :local])
1257 |> put_change(:ap_id, relay_uri)
1258 |> put_change(:nickname, nil)
1259 |> put_change(:local, true)
1260 |> put_change(:follower_address, relay_uri <> "/followers")
1262 {:ok, user} = Repo.insert(changes)
1268 def public_key_from_info(%{
1269 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1273 |> :public_key.pem_decode()
1275 |> :public_key.pem_entry_decode()
1281 def public_key_from_info(%{magic_key: magic_key}) do
1282 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1285 def get_public_key_for_ap_id(ap_id) do
1286 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1287 {:ok, public_key} <- public_key_from_info(user.info) do
1294 defp blank?(""), do: nil
1295 defp blank?(n), do: n
1297 def insert_or_update_user(data) do
1299 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1300 |> remote_user_creation()
1301 |> Repo.insert(on_conflict: :replace_all, conflict_target: :nickname)
1305 def ap_enabled?(%User{local: true}), do: true
1306 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1307 def ap_enabled?(_), do: false
1309 @doc "Gets or fetch a user by uri or nickname."
1310 @spec get_or_fetch(String.t()) :: User.t()
1311 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1312 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1314 # wait a period of time and return newest version of the User structs
1315 # this is because we have synchronous follow APIs and need to simulate them
1316 # with an async handshake
1317 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1318 with %User{} = a <- User.get_cached_by_id(a.id),
1319 %User{} = b <- User.get_cached_by_id(b.id) do
1327 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1328 with :ok <- :timer.sleep(timeout),
1329 %User{} = a <- User.get_cached_by_id(a.id),
1330 %User{} = b <- User.get_cached_by_id(b.id) do
1338 def parse_bio(bio) when is_binary(bio) and bio != "" do
1340 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1344 def parse_bio(_), do: ""
1346 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1347 # TODO: get profile URLs other than user.ap_id
1348 profile_urls = [user.ap_id]
1351 |> CommonUtils.format_input("text/plain",
1352 mentions_format: :full,
1353 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1358 def parse_bio(_, _), do: ""
1360 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1361 Repo.transaction(fn ->
1362 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1366 def tag(nickname, tags) when is_binary(nickname),
1367 do: tag(get_by_nickname(nickname), tags)
1369 def tag(%User{} = user, tags),
1370 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1372 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1373 Repo.transaction(fn ->
1374 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1378 def untag(nickname, tags) when is_binary(nickname),
1379 do: untag(get_by_nickname(nickname), tags)
1381 def untag(%User{} = user, tags),
1382 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1384 defp update_tags(%User{} = user, new_tags) do
1385 {:ok, updated_user} =
1387 |> change(%{tags: new_tags})
1388 |> update_and_set_cache()
1393 defp normalize_tags(tags) do
1396 |> Enum.map(&String.downcase(&1))
1399 defp local_nickname_regex do
1400 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1401 @extended_local_nickname_regex
1403 @strict_local_nickname_regex
1407 def local_nickname(nickname_or_mention) do
1410 |> String.split("@")
1414 def full_nickname(nickname_or_mention),
1415 do: String.trim_leading(nickname_or_mention, "@")
1417 def error_user(ap_id) do
1422 nickname: "erroruser@example.com",
1423 inserted_at: NaiveDateTime.utc_now()
1427 def all_superusers do
1430 where: u.local == true,
1431 where: fragment("?->'is_admin' @> 'true' OR ?->'is_moderator' @> 'true'", u.info, u.info)
1436 defp paginate(query, page, page_size) do
1439 offset: ^((page - 1) * page_size)
1443 def showing_reblogs?(%User{} = user, %User{} = target) do
1444 target.ap_id not in user.info.muted_reblogs
1448 The function returns a query to get users with no activity for given interval of days.
1449 Inactive users are those who didn't read any notification, or had any activity where
1450 the user is the activity's actor, during `inactivity_threshold` days.
1451 Deactivated users will not appear in this list.
1455 iex> Pleroma.User.list_inactive_users()
1458 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1459 def list_inactive_users_query(inactivity_threshold \\ 7) do
1460 negative_inactivity_threshold = -inactivity_threshold
1461 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1462 # Subqueries are not supported in `where` clauses, join gets too complicated.
1463 has_read_notifications =
1464 from(n in Pleroma.Notification,
1465 where: n.seen == true,
1467 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1470 |> Pleroma.Repo.all()
1472 from(u in Pleroma.User,
1473 left_join: a in Pleroma.Activity,
1474 on: u.ap_id == a.actor,
1475 where: not is_nil(u.nickname),
1476 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1477 where: u.id not in ^has_read_notifications,
1480 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1481 is_nil(max(a.inserted_at))
1486 Enable or disable email notifications for user
1490 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1491 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1493 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1494 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1496 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1497 {:ok, t()} | {:error, Ecto.Changeset.t()}
1498 def switch_email_notifications(user, type, status) do
1499 info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})
1502 |> put_embed(:info, info)
1503 |> update_and_set_cache()
1507 Set `last_digest_emailed_at` value for the user to current time
1509 @spec touch_last_digest_emailed_at(t()) :: t()
1510 def touch_last_digest_emailed_at(user) do
1511 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1513 {:ok, updated_user} =
1515 |> change(%{last_digest_emailed_at: now})
1516 |> update_and_set_cache()