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.Formatter
14 alias Pleroma.Notification
16 alias Pleroma.Registration
20 alias Pleroma.Web.ActivityPub.ActivityPub
21 alias Pleroma.Web.ActivityPub.Utils
22 alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
23 alias Pleroma.Web.OAuth
24 alias Pleroma.Web.OStatus
25 alias Pleroma.Web.RelMe
26 alias Pleroma.Web.Websub
30 @type t :: %__MODULE__{}
32 @primary_key {:id, Pleroma.FlakeId, autogenerate: true}
34 # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
35 @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])?)*$/
37 @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
38 @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
42 field(:email, :string)
44 field(:nickname, :string)
45 field(:password_hash, :string)
46 field(:password, :string, virtual: true)
47 field(:password_confirmation, :string, virtual: true)
48 field(:following, {:array, :string}, default: [])
49 field(:ap_id, :string)
51 field(:local, :boolean, default: true)
52 field(:follower_address, :string)
53 field(:search_rank, :float, virtual: true)
54 field(:search_type, :integer, virtual: true)
55 field(:tags, {:array, :string}, default: [])
56 field(:bookmarks, {:array, :string}, default: [])
57 field(:last_refreshed_at, :naive_datetime_usec)
58 field(:last_digest_emailed_at, :naive_datetime)
59 has_many(:notifications, Notification)
60 has_many(:registrations, Registration)
61 embeds_one(:info, Pleroma.User.Info)
66 def auth_active?(%User{info: %User.Info{confirmation_pending: true}}),
67 do: !Pleroma.Config.get([:instance, :account_activation_required])
69 def auth_active?(%User{}), do: true
71 def visible_for?(user, for_user \\ nil)
73 def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
75 def visible_for?(%User{} = user, for_user) do
76 auth_active?(user) || superuser?(for_user)
79 def visible_for?(_, _), do: false
81 def superuser?(%User{local: true, info: %User.Info{is_admin: true}}), do: true
82 def superuser?(%User{local: true, info: %User.Info{is_moderator: true}}), do: true
83 def superuser?(_), do: false
85 def avatar_url(user, options \\ []) do
87 %{"url" => [%{"href" => href} | _]} -> href
88 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
92 def banner_url(user, options \\ []) do
93 case user.info.banner do
94 %{"url" => [%{"href" => href} | _]} -> href
95 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
99 def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url
100 def profile_url(%User{ap_id: ap_id}), do: ap_id
101 def profile_url(_), do: nil
103 def ap_id(%User{nickname: nickname}) do
104 "#{Web.base_url()}/users/#{nickname}"
107 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
108 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
110 def user_info(%User{} = user) do
111 oneself = if user.local, do: 1, else: 0
114 following_count: length(user.following) - oneself,
115 note_count: user.info.note_count,
116 follower_count: user.info.follower_count,
117 locked: user.info.locked,
118 confirmation_pending: user.info.confirmation_pending,
119 default_scope: user.info.default_scope
123 def remote_user_creation(params) do
126 |> Map.put(:info, params[:info] || %{})
128 info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])
132 |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
133 |> validate_required([:name, :ap_id])
134 |> unique_constraint(:nickname)
135 |> validate_format(:nickname, @email_regex)
136 |> validate_length(:bio, max: 5000)
137 |> validate_length(:name, max: 100)
138 |> put_change(:local, false)
139 |> put_embed(:info, info_cng)
142 case info_cng.changes[:source_data] do
143 %{"followers" => followers} ->
145 |> put_change(:follower_address, followers)
148 followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
151 |> put_change(:follower_address, followers)
158 def update_changeset(struct, params \\ %{}) do
160 |> cast(params, [:bio, :name, :avatar])
161 |> unique_constraint(:nickname)
162 |> validate_format(:nickname, local_nickname_regex())
163 |> validate_length(:bio, max: 5000)
164 |> validate_length(:name, min: 1, max: 100)
167 def upgrade_changeset(struct, params \\ %{}) do
170 |> Map.put(:last_refreshed_at, NaiveDateTime.utc_now())
174 |> User.Info.user_upgrade(params[:info])
177 |> cast(params, [:bio, :name, :follower_address, :avatar, :last_refreshed_at])
178 |> unique_constraint(:nickname)
179 |> validate_format(:nickname, local_nickname_regex())
180 |> validate_length(:bio, max: 5000)
181 |> validate_length(:name, max: 100)
182 |> put_embed(:info, info_cng)
185 def password_update_changeset(struct, params) do
188 |> cast(params, [:password, :password_confirmation])
189 |> validate_required([:password, :password_confirmation])
190 |> validate_confirmation(:password)
192 OAuth.Token.delete_user_tokens(struct)
193 OAuth.Authorization.delete_user_authorizations(struct)
195 if changeset.valid? do
196 hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
199 |> put_change(:password_hash, hashed)
205 def reset_password(user, data) do
206 update_and_set_cache(password_update_changeset(user, data))
209 def register_changeset(struct, params \\ %{}, opts \\ []) do
210 confirmation_status =
211 if opts[:confirmed] || !Pleroma.Config.get([:instance, :account_activation_required]) do
217 info_change = User.Info.confirmation_changeset(%User.Info{}, confirmation_status)
221 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
222 |> validate_required([:name, :nickname, :password, :password_confirmation])
223 |> validate_confirmation(:password)
224 |> unique_constraint(:email)
225 |> unique_constraint(:nickname)
226 |> validate_exclusion(:nickname, Pleroma.Config.get([Pleroma.User, :restricted_nicknames]))
227 |> validate_format(:nickname, local_nickname_regex())
228 |> validate_format(:email, @email_regex)
229 |> validate_length(:bio, max: 1000)
230 |> validate_length(:name, min: 1, max: 100)
231 |> put_change(:info, info_change)
234 if opts[:external] do
237 validate_required(changeset, [:email])
240 if changeset.valid? do
241 hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
242 ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
243 followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
246 |> put_change(:password_hash, hashed)
247 |> put_change(:ap_id, ap_id)
248 |> unique_constraint(:ap_id)
249 |> put_change(:following, [followers])
250 |> put_change(:follower_address, followers)
256 defp autofollow_users(user) do
257 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
261 where: u.local == true,
262 where: u.nickname in ^candidates
266 follow_all(user, autofollowed_users)
269 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
270 def register(%Ecto.Changeset{} = changeset) do
271 with {:ok, user} <- Repo.insert(changeset),
272 {:ok, user} <- autofollow_users(user),
273 {:ok, _} <- Pleroma.User.WelcomeMessage.post_welcome_message_to_user(user),
274 {:ok, _} <- try_send_confirmation_email(user) do
279 def try_send_confirmation_email(%User{} = user) do
280 if user.info.confirmation_pending &&
281 Pleroma.Config.get([:instance, :account_activation_required]) do
283 |> Pleroma.Emails.UserEmail.account_confirmation_email()
284 |> Pleroma.Emails.Mailer.deliver_async()
292 def needs_update?(%User{local: true}), do: false
294 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
296 def needs_update?(%User{local: false} = user) do
297 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
300 def needs_update?(_), do: true
302 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
306 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
307 follow(follower, followed)
310 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
311 if not User.ap_enabled?(followed) do
312 follow(follower, followed)
318 def maybe_follow(%User{} = follower, %User{info: _info} = followed) do
319 if not following?(follower, followed) do
320 follow(follower, followed)
326 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
327 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
328 def follow_all(follower, followeds) do
331 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
332 |> Enum.map(fn %{follower_address: fa} -> fa end)
336 where: u.id == ^follower.id,
341 "array(select distinct unnest (array_cat(?, ?)))",
350 {1, [follower]} = Repo.update_all(q, [])
352 Enum.each(followeds, fn followed ->
353 update_follower_count(followed)
359 def follow(%User{} = follower, %User{info: info} = followed) do
360 user_config = Application.get_env(:pleroma, :user)
361 deny_follow_blocked = Keyword.get(user_config, :deny_follow_blocked)
363 ap_followers = followed.follower_address
366 following?(follower, followed) or info.deactivated ->
367 {:error, "Could not follow user: #{followed.nickname} is already on your list."}
369 deny_follow_blocked and blocks?(followed, follower) ->
370 {:error, "Could not follow user: #{followed.nickname} blocked you."}
373 if !followed.local && follower.local && !ap_enabled?(followed) do
374 Websub.subscribe(follower, followed)
379 where: u.id == ^follower.id,
380 update: [push: [following: ^ap_followers]],
384 {1, [follower]} = Repo.update_all(q, [])
386 {:ok, _} = update_follower_count(followed)
392 def unfollow(%User{} = follower, %User{} = followed) do
393 ap_followers = followed.follower_address
395 if following?(follower, followed) and follower.ap_id != followed.ap_id do
398 where: u.id == ^follower.id,
399 update: [pull: [following: ^ap_followers]],
403 {1, [follower]} = Repo.update_all(q, [])
405 {:ok, followed} = update_follower_count(followed)
409 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
411 {:error, "Not subscribed!"}
415 @spec following?(User.t(), User.t()) :: boolean
416 def following?(%User{} = follower, %User{} = followed) do
417 Enum.member?(follower.following, followed.follower_address)
420 def follow_import(%User{} = follower, followed_identifiers)
421 when is_list(followed_identifiers) do
423 followed_identifiers,
424 fn followed_identifier ->
425 with %User{} = followed <- get_or_fetch(followed_identifier),
426 {:ok, follower} <- maybe_direct_follow(follower, followed),
427 {:ok, _} <- ActivityPub.follow(follower, followed) do
431 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
438 def locked?(%User{} = user) do
439 user.info.locked || false
443 Repo.get_by(User, id: id)
446 def get_by_ap_id(ap_id) do
447 Repo.get_by(User, ap_id: ap_id)
450 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
451 # of the ap_id and the domain and tries to get that user
452 def get_by_guessed_nickname(ap_id) do
453 domain = URI.parse(ap_id).host
454 name = List.last(String.split(ap_id, "/"))
455 nickname = "#{name}@#{domain}"
457 get_by_nickname(nickname)
460 def set_cache(user) do
461 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
462 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
463 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
467 def update_and_set_cache(changeset) do
468 with {:ok, user} <- Repo.update(changeset) do
475 def invalidate_cache(user) do
476 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
477 Cachex.del(:user_cache, "nickname:#{user.nickname}")
478 Cachex.del(:user_cache, "user_info:#{user.id}")
481 def get_cached_by_ap_id(ap_id) do
482 key = "ap_id:#{ap_id}"
483 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
486 def get_cached_by_id(id) do
490 Cachex.fetch!(:user_cache, key, fn _ ->
494 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
495 {:commit, user.ap_id}
501 get_cached_by_ap_id(ap_id)
504 def get_cached_by_nickname(nickname) do
505 key = "nickname:#{nickname}"
506 Cachex.fetch!(:user_cache, key, fn _ -> get_or_fetch_by_nickname(nickname) end)
509 def get_cached_by_nickname_or_id(nickname_or_id) do
510 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
513 def get_by_nickname(nickname) do
514 Repo.get_by(User, nickname: nickname) ||
515 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
516 Repo.get_by(User, nickname: local_nickname(nickname))
520 def get_by_email(email), do: Repo.get_by(User, email: email)
522 def get_by_nickname_or_email(nickname_or_email) do
523 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
526 def get_cached_user_info(user) do
527 key = "user_info:#{user.id}"
528 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
531 def fetch_by_nickname(nickname) do
532 ap_try = ActivityPub.make_user_from_nickname(nickname)
535 {:ok, user} -> {:ok, user}
536 _ -> OStatus.make_user(nickname)
540 def get_or_fetch_by_nickname(nickname) do
541 with %User{} = user <- get_by_nickname(nickname) do
545 with [_nick, _domain] <- String.split(nickname, "@"),
546 {:ok, user} <- fetch_by_nickname(nickname) do
547 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
548 {:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])
558 @doc "Fetch some posts when the user has just been federated with"
559 def fetch_initial_posts(user) do
560 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
563 # Insert all the posts in reverse order, so they're in the right order on the timeline
564 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
565 &Pleroma.Web.Federator.incoming_ap_doc/1
569 def get_followers_query(%User{id: id, follower_address: follower_address}, nil) do
572 where: fragment("? <@ ?", ^[follower_address], u.following),
577 def get_followers_query(user, page) do
578 from(u in get_followers_query(user, nil))
579 |> paginate(page, 20)
582 def get_followers_query(user), do: get_followers_query(user, nil)
584 def get_followers(user, page \\ nil) do
585 q = get_followers_query(user, page)
590 def get_followers_ids(user, page \\ nil) do
591 q = get_followers_query(user, page)
593 Repo.all(from(u in q, select: u.id))
596 def get_friends_query(%User{id: id, following: following}, nil) do
599 where: u.follower_address in ^following,
604 def get_friends_query(user, page) do
605 from(u in get_friends_query(user, nil))
606 |> paginate(page, 20)
609 def get_friends_query(user), do: get_friends_query(user, nil)
611 def get_friends(user, page \\ nil) do
612 q = get_friends_query(user, page)
617 def get_friends_ids(user, page \\ nil) do
618 q = get_friends_query(user, page)
620 Repo.all(from(u in q, select: u.id))
623 def get_follow_requests_query(%User{} = user) do
628 "? ->> 'type' = 'Follow'",
633 "? ->> 'state' = 'pending'",
638 "coalesce((?)->'object'->>'id', (?)->>'object') = ?",
646 def get_follow_requests(%User{} = user) do
649 |> User.get_follow_requests_query()
650 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
651 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
652 |> group_by([a, u], u.id)
659 def increase_note_count(%User{} = user) do
661 |> where(id: ^user.id)
666 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
673 |> Repo.update_all([])
675 {1, [user]} -> set_cache(user)
680 def decrease_note_count(%User{} = user) do
682 |> where(id: ^user.id)
687 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
694 |> Repo.update_all([])
696 {1, [user]} -> set_cache(user)
701 def update_note_count(%User{} = user) do
705 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
709 note_count = Repo.one(note_count_query)
711 info_cng = User.Info.set_note_count(user.info, note_count)
715 |> put_embed(:info, info_cng)
717 update_and_set_cache(cng)
720 def update_follower_count(%User{} = user) do
721 follower_count_query =
723 |> where([u], ^user.follower_address in u.following)
724 |> where([u], u.id != ^user.id)
725 |> select([u], %{count: count(u.id)})
728 |> where(id: ^user.id)
729 |> join(:inner, [u], s in subquery(follower_count_query))
734 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
741 |> Repo.update_all([])
743 {1, [user]} -> set_cache(user)
748 def get_users_from_set_query(ap_ids, false) do
751 where: u.ap_id in ^ap_ids
755 def get_users_from_set_query(ap_ids, true) do
756 query = get_users_from_set_query(ap_ids, false)
760 where: u.local == true
764 def get_users_from_set(ap_ids, local_only \\ true) do
765 get_users_from_set_query(ap_ids, local_only)
769 def get_recipients_from_activity(%Activity{recipients: to}) do
773 where: u.ap_id in ^to,
774 or_where: fragment("? && ?", u.following, ^to)
777 query = from(u in query, where: u.local == true)
782 def search(query, resolve \\ false, for_user \\ nil) do
783 # Strip the beginning @ off if there is a query
784 query = String.trim_leading(query, "@")
786 if resolve, do: get_or_fetch(query)
789 Repo.transaction(fn ->
790 Ecto.Adapters.SQL.query(Repo, "select set_limit(0.25)", [])
791 Repo.all(search_query(query, for_user))
797 def search_query(query, for_user) do
798 fts_subquery = fts_search_subquery(query)
799 trigram_subquery = trigram_search_subquery(query)
800 union_query = from(s in trigram_subquery, union_all: ^fts_subquery)
801 distinct_query = from(s in subquery(union_query), order_by: s.search_type, distinct: s.id)
803 from(s in subquery(boost_search_rank_query(distinct_query, for_user)),
804 order_by: [desc: s.search_rank],
809 defp boost_search_rank_query(query, nil), do: query
811 defp boost_search_rank_query(query, for_user) do
812 friends_ids = get_friends_ids(for_user)
813 followers_ids = get_followers_ids(for_user)
815 from(u in subquery(query),
820 CASE WHEN (?) THEN (?) * 1.3
821 WHEN (?) THEN (?) * 1.2
822 WHEN (?) THEN (?) * 1.1
825 u.id in ^friends_ids and u.id in ^followers_ids,
827 u.id in ^friends_ids,
829 u.id in ^followers_ids,
837 defp fts_search_subquery(term, query \\ User) do
840 |> String.replace(~r/\W+/, " ")
843 |> Enum.map(&(&1 <> ":*"))
854 setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
855 setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B'),
856 to_tsquery('simple', ?),
868 (setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
869 setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B')) @@ to_tsquery('simple', ?)
878 defp trigram_search_subquery(term) do
882 # ^1 gives 'Postgrex expected a binary, got 1' for some weird reason
883 search_type: fragment("?", 1),
886 "similarity(?, trim(? || ' ' || coalesce(?, '')))",
892 where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term)
896 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
899 fn blocked_identifier ->
900 with %User{} = blocked <- get_or_fetch(blocked_identifier),
901 {:ok, blocker} <- block(blocker, blocked),
902 {:ok, _} <- ActivityPub.block(blocker, blocked) do
906 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
913 def mute(muter, %User{ap_id: ap_id}) do
916 |> User.Info.add_to_mutes(ap_id)
920 |> put_embed(:info, info_cng)
922 update_and_set_cache(cng)
925 def unmute(muter, %{ap_id: ap_id}) do
928 |> User.Info.remove_from_mutes(ap_id)
932 |> put_embed(:info, info_cng)
934 update_and_set_cache(cng)
937 def subscribe(subscriber, %{ap_id: ap_id}) do
938 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
940 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
941 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
944 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
948 |> User.Info.add_to_subscribers(subscriber.ap_id)
951 |> put_embed(:info, info_cng)
952 |> update_and_set_cache()
957 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
958 with %User{} = user <- get_cached_by_ap_id(ap_id) do
961 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
964 |> put_embed(:info, info_cng)
965 |> update_and_set_cache()
969 def block(blocker, %User{ap_id: ap_id} = blocked) do
970 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
972 if following?(blocker, blocked) do
973 {:ok, blocker, _} = unfollow(blocker, blocked)
980 if subscribed_to?(blocked, blocker) do
981 {:ok, blocker} = unsubscribe(blocked, blocker)
987 if following?(blocked, blocker) do
988 unfollow(blocked, blocker)
991 {:ok, blocker} = update_follower_count(blocker)
995 |> User.Info.add_to_block(ap_id)
999 |> put_embed(:info, info_cng)
1001 update_and_set_cache(cng)
1004 # helper to handle the block given only an actor's AP id
1005 def block(blocker, %{ap_id: ap_id}) do
1006 block(blocker, User.get_by_ap_id(ap_id))
1009 def unblock(blocker, %{ap_id: ap_id}) do
1012 |> User.Info.remove_from_block(ap_id)
1016 |> put_embed(:info, info_cng)
1018 update_and_set_cache(cng)
1021 def mutes?(nil, _), do: false
1022 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
1024 def blocks?(user, %{ap_id: ap_id}) do
1025 blocks = user.info.blocks
1026 domain_blocks = user.info.domain_blocks
1027 %{host: host} = URI.parse(ap_id)
1029 Enum.member?(blocks, ap_id) ||
1030 Enum.any?(domain_blocks, fn domain ->
1035 def subscribed_to?(user, %{ap_id: ap_id}) do
1036 with %User{} = target <- User.get_by_ap_id(ap_id) do
1037 Enum.member?(target.info.subscribers, user.ap_id)
1041 def muted_users(user),
1042 do: Repo.all(from(u in User, where: u.ap_id in ^user.info.mutes))
1044 def blocked_users(user),
1045 do: Repo.all(from(u in User, where: u.ap_id in ^user.info.blocks))
1047 def subscribers(user),
1048 do: Repo.all(from(u in User, where: u.ap_id in ^user.info.subscribers))
1050 def block_domain(user, domain) do
1053 |> User.Info.add_to_domain_block(domain)
1057 |> put_embed(:info, info_cng)
1059 update_and_set_cache(cng)
1062 def unblock_domain(user, domain) do
1065 |> User.Info.remove_from_domain_block(domain)
1069 |> put_embed(:info, info_cng)
1071 update_and_set_cache(cng)
1074 def maybe_local_user_query(query, local) do
1075 if local, do: local_user_query(query), else: query
1078 def local_user_query(query \\ User) do
1081 where: u.local == true,
1082 where: not is_nil(u.nickname)
1086 def maybe_external_user_query(query, external) do
1087 if external, do: external_user_query(query), else: query
1090 def external_user_query(query \\ User) do
1093 where: u.local == false,
1094 where: not is_nil(u.nickname)
1098 def maybe_active_user_query(query, active) do
1099 if active, do: active_user_query(query), else: query
1102 def active_user_query(query \\ User) do
1105 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1106 where: not is_nil(u.nickname)
1110 def maybe_deactivated_user_query(query, deactivated) do
1111 if deactivated, do: deactivated_user_query(query), else: query
1114 def deactivated_user_query(query \\ User) do
1117 where: fragment("(?->'deactivated' @> 'true')", u.info),
1118 where: not is_nil(u.nickname)
1122 def active_local_user_query do
1124 u in local_user_query(),
1125 where: fragment("not (?->'deactivated' @> 'true')", u.info)
1129 def moderator_user_query do
1132 where: u.local == true,
1133 where: fragment("?->'is_moderator' @> 'true'", u.info)
1137 def deactivate(%User{} = user, status \\ true) do
1138 info_cng = User.Info.set_activation_status(user.info, status)
1142 |> put_embed(:info, info_cng)
1144 update_and_set_cache(cng)
1147 def update_notification_settings(%User{} = user, settings \\ %{}) do
1148 info_changeset = User.Info.update_notification_settings(user.info, settings)
1151 |> put_embed(:info, info_changeset)
1152 |> update_and_set_cache()
1155 def delete(%User{} = user) do
1156 {:ok, user} = User.deactivate(user)
1158 # Remove all relationships
1159 {:ok, followers} = User.get_followers(user)
1161 Enum.each(followers, fn follower -> User.unfollow(follower, user) end)
1163 {:ok, friends} = User.get_friends(user)
1165 Enum.each(friends, fn followed -> User.unfollow(user, followed) end)
1167 delete_user_activities(user)
1170 def delete_user_activities(%User{ap_id: ap_id} = user) do
1172 |> where(actor: ^ap_id)
1173 |> Activity.with_preloaded_object()
1176 %{data: %{"type" => "Create"}} = activity ->
1177 activity |> Object.normalize() |> ActivityPub.delete()
1179 # TODO: Do something with likes, follows, repeats.
1187 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1188 Pleroma.HTML.Scrubber.TwitterText
1191 @default_scrubbers Pleroma.Config.get([:markup, :scrub_policy])
1193 def html_filter_policy(_), do: @default_scrubbers
1195 def fetch_by_ap_id(ap_id) do
1196 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1203 case OStatus.make_user(ap_id) do
1205 _ -> {:error, "Could not fetch by AP id"}
1210 def get_or_fetch_by_ap_id(ap_id) do
1211 user = get_by_ap_id(ap_id)
1213 if !is_nil(user) and !User.needs_update?(user) do
1216 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1217 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1219 user = fetch_by_ap_id(ap_id)
1221 if should_fetch_initial do
1222 with %User{} = user do
1223 {:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])
1231 def get_or_create_instance_user do
1232 relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"
1234 if user = get_by_ap_id(relay_uri) do
1238 %User{info: %User.Info{}}
1239 |> cast(%{}, [:ap_id, :nickname, :local])
1240 |> put_change(:ap_id, relay_uri)
1241 |> put_change(:nickname, nil)
1242 |> put_change(:local, true)
1243 |> put_change(:follower_address, relay_uri <> "/followers")
1245 {:ok, user} = Repo.insert(changes)
1251 def public_key_from_info(%{
1252 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1256 |> :public_key.pem_decode()
1258 |> :public_key.pem_entry_decode()
1264 def public_key_from_info(%{magic_key: magic_key}) do
1265 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1268 def get_public_key_for_ap_id(ap_id) do
1269 with %User{} = user <- get_or_fetch_by_ap_id(ap_id),
1270 {:ok, public_key} <- public_key_from_info(user.info) do
1277 defp blank?(""), do: nil
1278 defp blank?(n), do: n
1280 def insert_or_update_user(data) do
1283 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1285 cs = User.remote_user_creation(data)
1287 Repo.insert(cs, on_conflict: :replace_all, conflict_target: :nickname)
1290 def ap_enabled?(%User{local: true}), do: true
1291 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1292 def ap_enabled?(_), do: false
1294 @doc "Gets or fetch a user by uri or nickname."
1295 @spec get_or_fetch(String.t()) :: User.t()
1296 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1297 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1299 # wait a period of time and return newest version of the User structs
1300 # this is because we have synchronous follow APIs and need to simulate them
1301 # with an async handshake
1302 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1303 with %User{} = a <- User.get_by_id(a.id),
1304 %User{} = b <- User.get_by_id(b.id) do
1312 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1313 with :ok <- :timer.sleep(timeout),
1314 %User{} = a <- User.get_by_id(a.id),
1315 %User{} = b <- User.get_by_id(b.id) do
1323 def parse_bio(bio, user \\ %User{info: %{source_data: %{}}})
1324 def parse_bio(nil, _user), do: ""
1325 def parse_bio(bio, _user) when bio == "", do: bio
1327 def parse_bio(bio, user) do
1329 (user.info.source_data["tag"] || [])
1330 |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
1331 |> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
1332 {String.trim(name, ":"), url}
1335 # TODO: get profile URLs other than user.ap_id
1336 profile_urls = [user.ap_id]
1339 |> CommonUtils.format_input("text/plain",
1340 mentions_format: :full,
1341 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1344 |> Formatter.emojify(emoji)
1347 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1348 Repo.transaction(fn ->
1349 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1353 def tag(nickname, tags) when is_binary(nickname),
1354 do: tag(User.get_by_nickname(nickname), tags)
1356 def tag(%User{} = user, tags),
1357 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1359 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1360 Repo.transaction(fn ->
1361 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1365 def untag(nickname, tags) when is_binary(nickname),
1366 do: untag(User.get_by_nickname(nickname), tags)
1368 def untag(%User{} = user, tags),
1369 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1371 defp update_tags(%User{} = user, new_tags) do
1372 {:ok, updated_user} =
1374 |> change(%{tags: new_tags})
1375 |> update_and_set_cache()
1380 def bookmark(%User{} = user, status_id) do
1381 bookmarks = Enum.uniq(user.bookmarks ++ [status_id])
1382 update_bookmarks(user, bookmarks)
1385 def unbookmark(%User{} = user, status_id) do
1386 bookmarks = Enum.uniq(user.bookmarks -- [status_id])
1387 update_bookmarks(user, bookmarks)
1390 def update_bookmarks(%User{} = user, bookmarks) do
1392 |> change(%{bookmarks: bookmarks})
1393 |> update_and_set_cache
1396 defp normalize_tags(tags) do
1399 |> Enum.map(&String.downcase(&1))
1402 defp local_nickname_regex do
1403 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1404 @extended_local_nickname_regex
1406 @strict_local_nickname_regex
1410 def local_nickname(nickname_or_mention) do
1413 |> String.split("@")
1417 def full_nickname(nickname_or_mention),
1418 do: String.trim_leading(nickname_or_mention, "@")
1420 def error_user(ap_id) do
1425 nickname: "erroruser@example.com",
1426 inserted_at: NaiveDateTime.utc_now()
1430 def all_superusers do
1433 where: u.local == true,
1434 where: fragment("?->'is_admin' @> 'true' OR ?->'is_moderator' @> 'true'", u.info, u.info)
1439 defp paginate(query, page, page_size) do
1442 offset: ^((page - 1) * page_size)
1446 def showing_reblogs?(%User{} = user, %User{} = target) do
1447 target.ap_id not in user.info.muted_reblogs
1451 The function returns a query to get users with no activity for given interval of days.
1452 Inactive users are those who didn't read any notification, or had any activity where
1453 the user is the activity's actor, during `inactivity_threshold` days.
1454 Deactivated users will not appear in this list.
1458 iex> Pleroma.User.list_inactive_users()
1461 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1462 def list_inactive_users_query(inactivity_threshold \\ 7) do
1463 negative_inactivity_threshold = -inactivity_threshold
1464 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1465 # Subqueries are not supported in `where` clauses, join gets too complicated.
1466 has_read_notifications =
1467 from(n in Pleroma.Notification,
1468 where: n.seen == true,
1470 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1473 |> Pleroma.Repo.all()
1475 from(u in Pleroma.User,
1476 left_join: a in Pleroma.Activity,
1477 on: u.ap_id == a.actor,
1478 where: not is_nil(u.nickname),
1479 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1480 where: u.id not in ^has_read_notifications,
1483 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1484 is_nil(max(a.inserted_at))
1489 Enable or disable email notifications for user
1493 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1494 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1496 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1497 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1499 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1500 {:ok, t()} | {:error, Ecto.Changeset.t()}
1501 def switch_email_notifications(user, type, status) do
1502 info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})
1505 |> put_embed(:info, info)
1506 |> update_and_set_cache()
1510 Set `last_digest_emailed_at` value for the user to current time
1512 @spec touch_last_digest_emailed_at(t()) :: t()
1513 def touch_last_digest_emailed_at(user) do
1514 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1516 {:ok, updated_user} =
1518 |> change(%{last_digest_emailed_at: now})
1519 |> update_and_set_cache()