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(:current_sign_in_at, :naive_datetime)
59 field(:last_digest_emailed_at, :naive_datetime)
60 has_many(:notifications, Notification)
61 has_many(:registrations, Registration)
62 embeds_one(:info, Pleroma.User.Info)
67 def auth_active?(%User{info: %User.Info{confirmation_pending: true}}),
68 do: !Pleroma.Config.get([:instance, :account_activation_required])
70 def auth_active?(%User{}), do: true
72 def visible_for?(user, for_user \\ nil)
74 def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
76 def visible_for?(%User{} = user, for_user) do
77 auth_active?(user) || superuser?(for_user)
80 def visible_for?(_, _), do: false
82 def superuser?(%User{local: true, info: %User.Info{is_admin: true}}), do: true
83 def superuser?(%User{local: true, info: %User.Info{is_moderator: true}}), do: true
84 def superuser?(_), do: false
86 def avatar_url(user, options \\ []) do
88 %{"url" => [%{"href" => href} | _]} -> href
89 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
93 def banner_url(user, options \\ []) do
94 case user.info.banner do
95 %{"url" => [%{"href" => href} | _]} -> href
96 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
100 def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url
101 def profile_url(%User{ap_id: ap_id}), do: ap_id
102 def profile_url(_), do: nil
104 def ap_id(%User{nickname: nickname}) do
105 "#{Web.base_url()}/users/#{nickname}"
108 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
109 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
111 def user_info(%User{} = user) do
112 oneself = if user.local, do: 1, else: 0
115 following_count: length(user.following) - oneself,
116 note_count: user.info.note_count,
117 follower_count: user.info.follower_count,
118 locked: user.info.locked,
119 confirmation_pending: user.info.confirmation_pending,
120 default_scope: user.info.default_scope
124 def remote_user_creation(params) do
127 |> Map.put(:info, params[:info] || %{})
129 info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])
133 |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
134 |> validate_required([:name, :ap_id])
135 |> unique_constraint(:nickname)
136 |> validate_format(:nickname, @email_regex)
137 |> validate_length(:bio, max: 5000)
138 |> validate_length(:name, max: 100)
139 |> put_change(:local, false)
140 |> put_embed(:info, info_cng)
143 case info_cng.changes[:source_data] do
144 %{"followers" => followers} ->
146 |> put_change(:follower_address, followers)
149 followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
152 |> put_change(:follower_address, followers)
159 def update_changeset(struct, params \\ %{}) do
161 |> cast(params, [:bio, :name, :avatar])
162 |> unique_constraint(:nickname)
163 |> validate_format(:nickname, local_nickname_regex())
164 |> validate_length(:bio, max: 5000)
165 |> validate_length(:name, min: 1, max: 100)
168 def upgrade_changeset(struct, params \\ %{}) do
171 |> Map.put(:last_refreshed_at, NaiveDateTime.utc_now())
175 |> User.Info.user_upgrade(params[:info])
178 |> cast(params, [:bio, :name, :follower_address, :avatar, :last_refreshed_at])
179 |> unique_constraint(:nickname)
180 |> validate_format(:nickname, local_nickname_regex())
181 |> validate_length(:bio, max: 5000)
182 |> validate_length(:name, max: 100)
183 |> put_embed(:info, info_cng)
186 def password_update_changeset(struct, params) do
189 |> cast(params, [:password, :password_confirmation])
190 |> validate_required([:password, :password_confirmation])
191 |> validate_confirmation(:password)
193 OAuth.Token.delete_user_tokens(struct)
194 OAuth.Authorization.delete_user_authorizations(struct)
196 if changeset.valid? do
197 hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
200 |> put_change(:password_hash, hashed)
206 def reset_password(user, data) do
207 update_and_set_cache(password_update_changeset(user, data))
210 def register_changeset(struct, params \\ %{}, opts \\ []) do
211 confirmation_status =
212 if opts[:confirmed] || !Pleroma.Config.get([:instance, :account_activation_required]) do
218 info_change = User.Info.confirmation_changeset(%User.Info{}, confirmation_status)
222 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
223 |> validate_required([:name, :nickname, :password, :password_confirmation])
224 |> validate_confirmation(:password)
225 |> unique_constraint(:email)
226 |> unique_constraint(:nickname)
227 |> validate_exclusion(:nickname, Pleroma.Config.get([Pleroma.User, :restricted_nicknames]))
228 |> validate_format(:nickname, local_nickname_regex())
229 |> validate_format(:email, @email_regex)
230 |> validate_length(:bio, max: 1000)
231 |> validate_length(:name, min: 1, max: 100)
232 |> put_change(:info, info_change)
235 if opts[:external] do
238 validate_required(changeset, [:email])
241 if changeset.valid? do
242 hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
243 ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
244 followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
247 |> put_change(:password_hash, hashed)
248 |> put_change(:ap_id, ap_id)
249 |> unique_constraint(:ap_id)
250 |> put_change(:following, [followers])
251 |> put_change(:follower_address, followers)
257 defp autofollow_users(user) do
258 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
262 where: u.local == true,
263 where: u.nickname in ^candidates
267 follow_all(user, autofollowed_users)
270 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
271 def register(%Ecto.Changeset{} = changeset) do
272 with {:ok, user} <- Repo.insert(changeset),
273 {:ok, user} <- autofollow_users(user),
274 {:ok, _} <- Pleroma.User.WelcomeMessage.post_welcome_message_to_user(user),
275 {:ok, _} <- try_send_confirmation_email(user) do
280 def try_send_confirmation_email(%User{} = user) do
281 if user.info.confirmation_pending &&
282 Pleroma.Config.get([:instance, :account_activation_required]) do
284 |> Pleroma.Emails.UserEmail.account_confirmation_email()
285 |> Pleroma.Emails.Mailer.deliver_async()
293 def needs_update?(%User{local: true}), do: false
295 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
297 def needs_update?(%User{local: false} = user) do
298 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
301 def needs_update?(_), do: true
303 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
307 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
308 follow(follower, followed)
311 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
312 if not User.ap_enabled?(followed) do
313 follow(follower, followed)
319 def maybe_follow(%User{} = follower, %User{info: _info} = followed) do
320 if not following?(follower, followed) do
321 follow(follower, followed)
327 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
328 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
329 def follow_all(follower, followeds) do
332 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
333 |> Enum.map(fn %{follower_address: fa} -> fa end)
337 where: u.id == ^follower.id,
342 "array(select distinct unnest (array_cat(?, ?)))",
351 {1, [follower]} = Repo.update_all(q, [])
353 Enum.each(followeds, fn followed ->
354 update_follower_count(followed)
360 def follow(%User{} = follower, %User{info: info} = followed) do
361 user_config = Application.get_env(:pleroma, :user)
362 deny_follow_blocked = Keyword.get(user_config, :deny_follow_blocked)
364 ap_followers = followed.follower_address
367 following?(follower, followed) or info.deactivated ->
368 {:error, "Could not follow user: #{followed.nickname} is already on your list."}
370 deny_follow_blocked and blocks?(followed, follower) ->
371 {:error, "Could not follow user: #{followed.nickname} blocked you."}
374 if !followed.local && follower.local && !ap_enabled?(followed) do
375 Websub.subscribe(follower, followed)
380 where: u.id == ^follower.id,
381 update: [push: [following: ^ap_followers]],
385 {1, [follower]} = Repo.update_all(q, [])
387 {:ok, _} = update_follower_count(followed)
393 def unfollow(%User{} = follower, %User{} = followed) do
394 ap_followers = followed.follower_address
396 if following?(follower, followed) and follower.ap_id != followed.ap_id do
399 where: u.id == ^follower.id,
400 update: [pull: [following: ^ap_followers]],
404 {1, [follower]} = Repo.update_all(q, [])
406 {:ok, followed} = update_follower_count(followed)
410 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
412 {:error, "Not subscribed!"}
416 @spec following?(User.t(), User.t()) :: boolean
417 def following?(%User{} = follower, %User{} = followed) do
418 Enum.member?(follower.following, followed.follower_address)
421 def follow_import(%User{} = follower, followed_identifiers)
422 when is_list(followed_identifiers) do
424 followed_identifiers,
425 fn followed_identifier ->
426 with %User{} = followed <- get_or_fetch(followed_identifier),
427 {:ok, follower} <- maybe_direct_follow(follower, followed),
428 {:ok, _} <- ActivityPub.follow(follower, followed) do
432 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
439 def locked?(%User{} = user) do
440 user.info.locked || false
444 Repo.get_by(User, id: id)
447 def get_by_ap_id(ap_id) do
448 Repo.get_by(User, ap_id: ap_id)
451 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
452 # of the ap_id and the domain and tries to get that user
453 def get_by_guessed_nickname(ap_id) do
454 domain = URI.parse(ap_id).host
455 name = List.last(String.split(ap_id, "/"))
456 nickname = "#{name}@#{domain}"
458 get_by_nickname(nickname)
461 def set_cache(user) do
462 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
463 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
464 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
468 def update_and_set_cache(changeset) do
469 with {:ok, user} <- Repo.update(changeset) do
476 def invalidate_cache(user) do
477 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
478 Cachex.del(:user_cache, "nickname:#{user.nickname}")
479 Cachex.del(:user_cache, "user_info:#{user.id}")
482 def get_cached_by_ap_id(ap_id) do
483 key = "ap_id:#{ap_id}"
484 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
487 def get_cached_by_id(id) do
491 Cachex.fetch!(:user_cache, key, fn _ ->
495 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
496 {:commit, user.ap_id}
502 get_cached_by_ap_id(ap_id)
505 def get_cached_by_nickname(nickname) do
506 key = "nickname:#{nickname}"
507 Cachex.fetch!(:user_cache, key, fn _ -> get_or_fetch_by_nickname(nickname) end)
510 def get_cached_by_nickname_or_id(nickname_or_id) do
511 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
514 def get_by_nickname(nickname) do
515 Repo.get_by(User, nickname: nickname) ||
516 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
517 Repo.get_by(User, nickname: local_nickname(nickname))
521 def get_by_email(email), do: Repo.get_by(User, email: email)
523 def get_by_nickname_or_email(nickname_or_email) do
524 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
527 def get_cached_user_info(user) do
528 key = "user_info:#{user.id}"
529 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
532 def fetch_by_nickname(nickname) do
533 ap_try = ActivityPub.make_user_from_nickname(nickname)
536 {:ok, user} -> {:ok, user}
537 _ -> OStatus.make_user(nickname)
541 def get_or_fetch_by_nickname(nickname) do
542 with %User{} = user <- get_by_nickname(nickname) do
546 with [_nick, _domain] <- String.split(nickname, "@"),
547 {:ok, user} <- fetch_by_nickname(nickname) do
548 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
549 {:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])
559 @doc "Fetch some posts when the user has just been federated with"
560 def fetch_initial_posts(user) do
561 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
564 # Insert all the posts in reverse order, so they're in the right order on the timeline
565 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
566 &Pleroma.Web.Federator.incoming_ap_doc/1
570 def get_followers_query(%User{id: id, follower_address: follower_address}, nil) do
573 where: fragment("? <@ ?", ^[follower_address], u.following),
578 def get_followers_query(user, page) do
579 from(u in get_followers_query(user, nil))
580 |> paginate(page, 20)
583 def get_followers_query(user), do: get_followers_query(user, nil)
585 def get_followers(user, page \\ nil) do
586 q = get_followers_query(user, page)
591 def get_followers_ids(user, page \\ nil) do
592 q = get_followers_query(user, page)
594 Repo.all(from(u in q, select: u.id))
597 def get_friends_query(%User{id: id, following: following}, nil) do
600 where: u.follower_address in ^following,
605 def get_friends_query(user, page) do
606 from(u in get_friends_query(user, nil))
607 |> paginate(page, 20)
610 def get_friends_query(user), do: get_friends_query(user, nil)
612 def get_friends(user, page \\ nil) do
613 q = get_friends_query(user, page)
618 def get_friends_ids(user, page \\ nil) do
619 q = get_friends_query(user, page)
621 Repo.all(from(u in q, select: u.id))
624 def get_follow_requests_query(%User{} = user) do
629 "? ->> 'type' = 'Follow'",
634 "? ->> 'state' = 'pending'",
639 "coalesce((?)->'object'->>'id', (?)->>'object') = ?",
647 def get_follow_requests(%User{} = user) do
650 |> User.get_follow_requests_query()
651 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
652 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
653 |> group_by([a, u], u.id)
660 def increase_note_count(%User{} = user) do
662 |> where(id: ^user.id)
667 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
674 |> Repo.update_all([])
676 {1, [user]} -> set_cache(user)
681 def decrease_note_count(%User{} = user) do
683 |> where(id: ^user.id)
688 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
695 |> Repo.update_all([])
697 {1, [user]} -> set_cache(user)
702 def update_note_count(%User{} = user) do
706 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
710 note_count = Repo.one(note_count_query)
712 info_cng = User.Info.set_note_count(user.info, note_count)
716 |> put_embed(:info, info_cng)
718 update_and_set_cache(cng)
721 def update_follower_count(%User{} = user) do
722 follower_count_query =
724 |> where([u], ^user.follower_address in u.following)
725 |> where([u], u.id != ^user.id)
726 |> select([u], %{count: count(u.id)})
729 |> where(id: ^user.id)
730 |> join(:inner, [u], s in subquery(follower_count_query))
735 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
742 |> Repo.update_all([])
744 {1, [user]} -> set_cache(user)
749 def get_users_from_set_query(ap_ids, false) do
752 where: u.ap_id in ^ap_ids
756 def get_users_from_set_query(ap_ids, true) do
757 query = get_users_from_set_query(ap_ids, false)
761 where: u.local == true
765 def get_users_from_set(ap_ids, local_only \\ true) do
766 get_users_from_set_query(ap_ids, local_only)
770 def get_recipients_from_activity(%Activity{recipients: to}) do
774 where: u.ap_id in ^to,
775 or_where: fragment("? && ?", u.following, ^to)
778 query = from(u in query, where: u.local == true)
783 def search(query, resolve \\ false, for_user \\ nil) do
784 # Strip the beginning @ off if there is a query
785 query = String.trim_leading(query, "@")
787 if resolve, do: get_or_fetch(query)
790 Repo.transaction(fn ->
791 Ecto.Adapters.SQL.query(Repo, "select set_limit(0.25)", [])
792 Repo.all(search_query(query, for_user))
798 def search_query(query, for_user) do
799 fts_subquery = fts_search_subquery(query)
800 trigram_subquery = trigram_search_subquery(query)
801 union_query = from(s in trigram_subquery, union_all: ^fts_subquery)
802 distinct_query = from(s in subquery(union_query), order_by: s.search_type, distinct: s.id)
804 from(s in subquery(boost_search_rank_query(distinct_query, for_user)),
805 order_by: [desc: s.search_rank],
810 defp boost_search_rank_query(query, nil), do: query
812 defp boost_search_rank_query(query, for_user) do
813 friends_ids = get_friends_ids(for_user)
814 followers_ids = get_followers_ids(for_user)
816 from(u in subquery(query),
821 CASE WHEN (?) THEN (?) * 1.3
822 WHEN (?) THEN (?) * 1.2
823 WHEN (?) THEN (?) * 1.1
826 u.id in ^friends_ids and u.id in ^followers_ids,
828 u.id in ^friends_ids,
830 u.id in ^followers_ids,
838 defp fts_search_subquery(term, query \\ User) do
841 |> String.replace(~r/\W+/, " ")
844 |> Enum.map(&(&1 <> ":*"))
855 setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
856 setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B'),
857 to_tsquery('simple', ?),
869 (setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
870 setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B')) @@ to_tsquery('simple', ?)
879 defp trigram_search_subquery(term) do
883 # ^1 gives 'Postgrex expected a binary, got 1' for some weird reason
884 search_type: fragment("?", 1),
887 "similarity(?, trim(? || ' ' || coalesce(?, '')))",
893 where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term)
897 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
900 fn blocked_identifier ->
901 with %User{} = blocked <- get_or_fetch(blocked_identifier),
902 {:ok, blocker} <- block(blocker, blocked),
903 {:ok, _} <- ActivityPub.block(blocker, blocked) do
907 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
914 def mute(muter, %User{ap_id: ap_id}) do
917 |> User.Info.add_to_mutes(ap_id)
921 |> put_embed(:info, info_cng)
923 update_and_set_cache(cng)
926 def unmute(muter, %{ap_id: ap_id}) do
929 |> User.Info.remove_from_mutes(ap_id)
933 |> put_embed(:info, info_cng)
935 update_and_set_cache(cng)
938 def subscribe(subscriber, %{ap_id: ap_id}) do
939 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
941 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
942 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
945 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
949 |> User.Info.add_to_subscribers(subscriber.ap_id)
952 |> put_embed(:info, info_cng)
953 |> update_and_set_cache()
958 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
959 with %User{} = user <- get_cached_by_ap_id(ap_id) do
962 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
965 |> put_embed(:info, info_cng)
966 |> update_and_set_cache()
970 def block(blocker, %User{ap_id: ap_id} = blocked) do
971 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
973 if following?(blocker, blocked) do
974 {:ok, blocker, _} = unfollow(blocker, blocked)
981 if subscribed_to?(blocked, blocker) do
982 {:ok, blocker} = unsubscribe(blocked, blocker)
988 if following?(blocked, blocker) do
989 unfollow(blocked, blocker)
992 {:ok, blocker} = update_follower_count(blocker)
996 |> User.Info.add_to_block(ap_id)
1000 |> put_embed(:info, info_cng)
1002 update_and_set_cache(cng)
1005 # helper to handle the block given only an actor's AP id
1006 def block(blocker, %{ap_id: ap_id}) do
1007 block(blocker, User.get_by_ap_id(ap_id))
1010 def unblock(blocker, %{ap_id: ap_id}) do
1013 |> User.Info.remove_from_block(ap_id)
1017 |> put_embed(:info, info_cng)
1019 update_and_set_cache(cng)
1022 def mutes?(nil, _), do: false
1023 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
1025 def blocks?(user, %{ap_id: ap_id}) do
1026 blocks = user.info.blocks
1027 domain_blocks = user.info.domain_blocks
1028 %{host: host} = URI.parse(ap_id)
1030 Enum.member?(blocks, ap_id) ||
1031 Enum.any?(domain_blocks, fn domain ->
1036 def subscribed_to?(user, %{ap_id: ap_id}) do
1037 with %User{} = target <- User.get_by_ap_id(ap_id) do
1038 Enum.member?(target.info.subscribers, user.ap_id)
1042 def muted_users(user),
1043 do: Repo.all(from(u in User, where: u.ap_id in ^user.info.mutes))
1045 def blocked_users(user),
1046 do: Repo.all(from(u in User, where: u.ap_id in ^user.info.blocks))
1048 def subscribers(user),
1049 do: Repo.all(from(u in User, where: u.ap_id in ^user.info.subscribers))
1051 def block_domain(user, domain) do
1054 |> User.Info.add_to_domain_block(domain)
1058 |> put_embed(:info, info_cng)
1060 update_and_set_cache(cng)
1063 def unblock_domain(user, domain) do
1066 |> User.Info.remove_from_domain_block(domain)
1070 |> put_embed(:info, info_cng)
1072 update_and_set_cache(cng)
1075 def maybe_local_user_query(query, local) do
1076 if local, do: local_user_query(query), else: query
1079 def local_user_query(query \\ User) do
1082 where: u.local == true,
1083 where: not is_nil(u.nickname)
1087 def maybe_external_user_query(query, external) do
1088 if external, do: external_user_query(query), else: query
1091 def external_user_query(query \\ User) do
1094 where: u.local == false,
1095 where: not is_nil(u.nickname)
1099 def maybe_active_user_query(query, active) do
1100 if active, do: active_user_query(query), else: query
1103 def active_user_query(query \\ User) do
1106 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1107 where: not is_nil(u.nickname)
1111 def maybe_deactivated_user_query(query, deactivated) do
1112 if deactivated, do: deactivated_user_query(query), else: query
1115 def deactivated_user_query(query \\ User) do
1118 where: fragment("(?->'deactivated' @> 'true')", u.info),
1119 where: not is_nil(u.nickname)
1123 def active_local_user_query do
1125 u in local_user_query(),
1126 where: fragment("not (?->'deactivated' @> 'true')", u.info)
1130 def moderator_user_query do
1133 where: u.local == true,
1134 where: fragment("?->'is_moderator' @> 'true'", u.info)
1138 def deactivate(%User{} = user, status \\ true) do
1139 info_cng = User.Info.set_activation_status(user.info, status)
1143 |> put_embed(:info, info_cng)
1145 update_and_set_cache(cng)
1148 def update_notification_settings(%User{} = user, settings \\ %{}) do
1149 info_changeset = User.Info.update_notification_settings(user.info, settings)
1152 |> put_embed(:info, info_changeset)
1153 |> update_and_set_cache()
1156 def delete(%User{} = user) do
1157 {:ok, user} = User.deactivate(user)
1159 # Remove all relationships
1160 {:ok, followers} = User.get_followers(user)
1162 Enum.each(followers, fn follower -> User.unfollow(follower, user) end)
1164 {:ok, friends} = User.get_friends(user)
1166 Enum.each(friends, fn followed -> User.unfollow(user, followed) end)
1168 delete_user_activities(user)
1171 def delete_user_activities(%User{ap_id: ap_id} = user) do
1173 |> where(actor: ^ap_id)
1174 |> Activity.with_preloaded_object()
1177 %{data: %{"type" => "Create"}} = activity ->
1178 activity |> Object.normalize() |> ActivityPub.delete()
1180 # TODO: Do something with likes, follows, repeats.
1188 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1189 Pleroma.HTML.Scrubber.TwitterText
1192 @default_scrubbers Pleroma.Config.get([:markup, :scrub_policy])
1194 def html_filter_policy(_), do: @default_scrubbers
1196 def fetch_by_ap_id(ap_id) do
1197 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1204 case OStatus.make_user(ap_id) do
1206 _ -> {:error, "Could not fetch by AP id"}
1211 def get_or_fetch_by_ap_id(ap_id) do
1212 user = get_by_ap_id(ap_id)
1214 if !is_nil(user) and !User.needs_update?(user) do
1217 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1218 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1220 user = fetch_by_ap_id(ap_id)
1222 if should_fetch_initial do
1223 with %User{} = user do
1224 {:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])
1232 def get_or_create_instance_user do
1233 relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"
1235 if user = get_by_ap_id(relay_uri) do
1239 %User{info: %User.Info{}}
1240 |> cast(%{}, [:ap_id, :nickname, :local])
1241 |> put_change(:ap_id, relay_uri)
1242 |> put_change(:nickname, nil)
1243 |> put_change(:local, true)
1244 |> put_change(:follower_address, relay_uri <> "/followers")
1246 {:ok, user} = Repo.insert(changes)
1252 def public_key_from_info(%{
1253 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1257 |> :public_key.pem_decode()
1259 |> :public_key.pem_entry_decode()
1265 def public_key_from_info(%{magic_key: magic_key}) do
1266 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1269 def get_public_key_for_ap_id(ap_id) do
1270 with %User{} = user <- get_or_fetch_by_ap_id(ap_id),
1271 {:ok, public_key} <- public_key_from_info(user.info) do
1278 defp blank?(""), do: nil
1279 defp blank?(n), do: n
1281 def insert_or_update_user(data) do
1284 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1286 cs = User.remote_user_creation(data)
1288 Repo.insert(cs, on_conflict: :replace_all, conflict_target: :nickname)
1291 def ap_enabled?(%User{local: true}), do: true
1292 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1293 def ap_enabled?(_), do: false
1295 @doc "Gets or fetch a user by uri or nickname."
1296 @spec get_or_fetch(String.t()) :: User.t()
1297 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1298 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1300 # wait a period of time and return newest version of the User structs
1301 # this is because we have synchronous follow APIs and need to simulate them
1302 # with an async handshake
1303 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1304 with %User{} = a <- User.get_by_id(a.id),
1305 %User{} = b <- User.get_by_id(b.id) do
1313 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1314 with :ok <- :timer.sleep(timeout),
1315 %User{} = a <- User.get_by_id(a.id),
1316 %User{} = b <- User.get_by_id(b.id) do
1324 def parse_bio(bio, user \\ %User{info: %{source_data: %{}}})
1325 def parse_bio(nil, _user), do: ""
1326 def parse_bio(bio, _user) when bio == "", do: bio
1328 def parse_bio(bio, user) do
1330 (user.info.source_data["tag"] || [])
1331 |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
1332 |> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
1333 {String.trim(name, ":"), url}
1336 # TODO: get profile URLs other than user.ap_id
1337 profile_urls = [user.ap_id]
1340 |> CommonUtils.format_input("text/plain",
1341 mentions_format: :full,
1342 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1345 |> Formatter.emojify(emoji)
1348 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1349 Repo.transaction(fn ->
1350 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1354 def tag(nickname, tags) when is_binary(nickname),
1355 do: tag(User.get_by_nickname(nickname), tags)
1357 def tag(%User{} = user, tags),
1358 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1360 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1361 Repo.transaction(fn ->
1362 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1366 def untag(nickname, tags) when is_binary(nickname),
1367 do: untag(User.get_by_nickname(nickname), tags)
1369 def untag(%User{} = user, tags),
1370 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1372 defp update_tags(%User{} = user, new_tags) do
1373 {:ok, updated_user} =
1375 |> change(%{tags: new_tags})
1376 |> update_and_set_cache()
1381 def bookmark(%User{} = user, status_id) do
1382 bookmarks = Enum.uniq(user.bookmarks ++ [status_id])
1383 update_bookmarks(user, bookmarks)
1386 def unbookmark(%User{} = user, status_id) do
1387 bookmarks = Enum.uniq(user.bookmarks -- [status_id])
1388 update_bookmarks(user, bookmarks)
1391 def update_bookmarks(%User{} = user, bookmarks) do
1393 |> change(%{bookmarks: bookmarks})
1394 |> update_and_set_cache
1397 defp normalize_tags(tags) do
1400 |> Enum.map(&String.downcase(&1))
1403 defp local_nickname_regex do
1404 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1405 @extended_local_nickname_regex
1407 @strict_local_nickname_regex
1411 def local_nickname(nickname_or_mention) do
1414 |> String.split("@")
1418 def full_nickname(nickname_or_mention),
1419 do: String.trim_leading(nickname_or_mention, "@")
1421 def error_user(ap_id) do
1426 nickname: "erroruser@example.com",
1427 inserted_at: NaiveDateTime.utc_now()
1431 def all_superusers do
1434 where: u.local == true,
1435 where: fragment("?->'is_admin' @> 'true' OR ?->'is_moderator' @> 'true'", u.info, u.info)
1440 defp paginate(query, page, page_size) do
1443 offset: ^((page - 1) * page_size)
1447 def showing_reblogs?(%User{} = user, %User{} = target) do
1448 target.ap_id not in user.info.muted_reblogs
1452 The function returns a query to get users with no activity for given interval of days.
1453 Inactive users are those who didn't read any notification, or had any activity where
1454 the user is the activity's actor, during `inactivity_threshold` days.
1455 Deactivated users will not appear in this list.
1459 iex> Pleroma.User.list_inactive_users()
1462 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1463 def list_inactive_users_query(inactivity_threshold \\ 7) do
1464 negative_inactivity_threshold = -inactivity_threshold
1465 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1466 # Subqueries are not supported in `where` clauses, join gets too complicated.
1467 has_read_notifications =
1468 from(n in Pleroma.Notification,
1469 where: n.seen == true,
1471 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1474 |> Pleroma.Repo.all()
1476 from(u in Pleroma.User,
1477 left_join: a in Pleroma.Activity,
1478 on: u.ap_id == a.actor,
1479 where: not is_nil(u.nickname),
1480 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1481 where: u.id not in ^has_read_notifications,
1484 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1485 is_nil(max(a.inserted_at))