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.Bookmark
14 alias Pleroma.Formatter
15 alias Pleroma.Notification
17 alias Pleroma.Registration
21 alias Pleroma.Web.ActivityPub.ActivityPub
22 alias Pleroma.Web.ActivityPub.Utils
23 alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
24 alias Pleroma.Web.OAuth
25 alias Pleroma.Web.OStatus
26 alias Pleroma.Web.RelMe
27 alias Pleroma.Web.Websub
31 @type t :: %__MODULE__{}
33 @primary_key {:id, Pleroma.FlakeId, autogenerate: true}
35 # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
36 @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])?)*$/
38 @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
39 @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
43 field(:email, :string)
45 field(:nickname, :string)
46 field(:password_hash, :string)
47 field(:password, :string, virtual: true)
48 field(:password_confirmation, :string, virtual: true)
49 field(:following, {:array, :string}, default: [])
50 field(:ap_id, :string)
52 field(:local, :boolean, default: true)
53 field(:follower_address, :string)
54 field(:search_rank, :float, virtual: true)
55 field(:search_type, :integer, virtual: true)
56 field(:tags, {:array, :string}, default: [])
57 field(:last_refreshed_at, :naive_datetime_usec)
58 field(:last_digest_emailed_at, :naive_datetime)
59 has_many(:bookmarks, Bookmark)
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, user} <- set_cache(user),
275 {:ok, _} <- Pleroma.User.WelcomeMessage.post_welcome_message_to_user(user),
276 {:ok, _} <- try_send_confirmation_email(user) do
281 def try_send_confirmation_email(%User{} = user) do
282 if user.info.confirmation_pending &&
283 Pleroma.Config.get([:instance, :account_activation_required]) do
285 |> Pleroma.Emails.UserEmail.account_confirmation_email()
286 |> Pleroma.Emails.Mailer.deliver_async()
294 def needs_update?(%User{local: true}), do: false
296 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
298 def needs_update?(%User{local: false} = user) do
299 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
302 def needs_update?(_), do: true
304 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
308 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
309 follow(follower, followed)
312 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
313 if not User.ap_enabled?(followed) do
314 follow(follower, followed)
320 def maybe_follow(%User{} = follower, %User{info: _info} = followed) do
321 if not following?(follower, followed) do
322 follow(follower, followed)
328 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
329 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
330 def follow_all(follower, followeds) do
333 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
334 |> Enum.map(fn %{follower_address: fa} -> fa end)
338 where: u.id == ^follower.id,
343 "array(select distinct unnest (array_cat(?, ?)))",
352 {1, [follower]} = Repo.update_all(q, [])
354 Enum.each(followeds, fn followed ->
355 update_follower_count(followed)
361 def follow(%User{} = follower, %User{info: info} = followed) do
362 user_config = Application.get_env(:pleroma, :user)
363 deny_follow_blocked = Keyword.get(user_config, :deny_follow_blocked)
365 ap_followers = followed.follower_address
368 following?(follower, followed) or info.deactivated ->
369 {:error, "Could not follow user: #{followed.nickname} is already on your list."}
371 deny_follow_blocked and blocks?(followed, follower) ->
372 {:error, "Could not follow user: #{followed.nickname} blocked you."}
375 if !followed.local && follower.local && !ap_enabled?(followed) do
376 Websub.subscribe(follower, followed)
381 where: u.id == ^follower.id,
382 update: [push: [following: ^ap_followers]],
386 {1, [follower]} = Repo.update_all(q, [])
388 {:ok, _} = update_follower_count(followed)
394 def unfollow(%User{} = follower, %User{} = followed) do
395 ap_followers = followed.follower_address
397 if following?(follower, followed) and follower.ap_id != followed.ap_id do
400 where: u.id == ^follower.id,
401 update: [pull: [following: ^ap_followers]],
405 {1, [follower]} = Repo.update_all(q, [])
407 {:ok, followed} = update_follower_count(followed)
411 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
413 {:error, "Not subscribed!"}
417 @spec following?(User.t(), User.t()) :: boolean
418 def following?(%User{} = follower, %User{} = followed) do
419 Enum.member?(follower.following, followed.follower_address)
422 def follow_import(%User{} = follower, followed_identifiers)
423 when is_list(followed_identifiers) do
425 followed_identifiers,
426 fn followed_identifier ->
427 with %User{} = followed <- get_or_fetch(followed_identifier),
428 {:ok, follower} <- maybe_direct_follow(follower, followed),
429 {:ok, _} <- ActivityPub.follow(follower, followed) do
433 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
440 def locked?(%User{} = user) do
441 user.info.locked || false
445 Repo.get_by(User, id: id)
448 def get_by_ap_id(ap_id) do
449 Repo.get_by(User, ap_id: ap_id)
452 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
453 # of the ap_id and the domain and tries to get that user
454 def get_by_guessed_nickname(ap_id) do
455 domain = URI.parse(ap_id).host
456 name = List.last(String.split(ap_id, "/"))
457 nickname = "#{name}@#{domain}"
459 get_cached_by_nickname(nickname)
462 def set_cache({:ok, user}), do: set_cache(user)
463 def set_cache({:error, err}), do: {:error, err}
465 def set_cache(%User{} = user) do
466 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
467 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
468 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
472 def update_and_set_cache(changeset) do
473 with {:ok, user} <- Repo.update(changeset) do
480 def invalidate_cache(user) do
481 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
482 Cachex.del(:user_cache, "nickname:#{user.nickname}")
483 Cachex.del(:user_cache, "user_info:#{user.id}")
486 def get_cached_by_ap_id(ap_id) do
487 key = "ap_id:#{ap_id}"
488 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
491 def get_cached_by_id(id) do
495 Cachex.fetch!(:user_cache, key, fn _ ->
499 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
500 {:commit, user.ap_id}
506 get_cached_by_ap_id(ap_id)
509 def get_cached_by_nickname(nickname) do
510 key = "nickname:#{nickname}"
511 Cachex.fetch!(:user_cache, key, fn _ -> get_or_fetch_by_nickname(nickname) end)
514 def get_cached_by_nickname_or_id(nickname_or_id) do
515 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
518 def get_by_nickname(nickname) do
519 Repo.get_by(User, nickname: nickname) ||
520 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
521 Repo.get_by(User, nickname: local_nickname(nickname))
525 def get_by_email(email), do: Repo.get_by(User, email: email)
527 def get_by_nickname_or_email(nickname_or_email) do
528 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
531 def get_cached_user_info(user) do
532 key = "user_info:#{user.id}"
533 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
536 def fetch_by_nickname(nickname) do
537 ap_try = ActivityPub.make_user_from_nickname(nickname)
540 {:ok, user} -> {:ok, user}
541 _ -> OStatus.make_user(nickname)
545 def get_or_fetch_by_nickname(nickname) do
546 with %User{} = user <- get_by_nickname(nickname) do
550 with [_nick, _domain] <- String.split(nickname, "@"),
551 {:ok, user} <- fetch_by_nickname(nickname) do
552 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
554 {:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])
564 @doc "Fetch some posts when the user has just been federated with"
565 def fetch_initial_posts(user) do
566 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
569 # Insert all the posts in reverse order, so they're in the right order on the timeline
570 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
571 &Pleroma.Web.Federator.incoming_ap_doc/1
575 def get_followers_query(%User{id: id, follower_address: follower_address}, nil) do
578 where: fragment("? <@ ?", ^[follower_address], u.following),
583 def get_followers_query(user, page) do
584 from(u in get_followers_query(user, nil))
585 |> paginate(page, 20)
588 def get_followers_query(user), do: get_followers_query(user, nil)
590 def get_followers(user, page \\ nil) do
591 q = get_followers_query(user, page)
596 def get_followers_ids(user, page \\ nil) do
597 q = get_followers_query(user, page)
599 Repo.all(from(u in q, select: u.id))
602 def get_friends_query(%User{id: id, following: following}, nil) do
605 where: u.follower_address in ^following,
610 def get_friends_query(user, page) do
611 from(u in get_friends_query(user, nil))
612 |> paginate(page, 20)
615 def get_friends_query(user), do: get_friends_query(user, nil)
617 def get_friends(user, page \\ nil) do
618 q = get_friends_query(user, page)
623 def get_friends_ids(user, page \\ nil) do
624 q = get_friends_query(user, page)
626 Repo.all(from(u in q, select: u.id))
629 def get_follow_requests_query(%User{} = user) do
634 "? ->> 'type' = 'Follow'",
639 "? ->> 'state' = 'pending'",
644 "coalesce((?)->'object'->>'id', (?)->>'object') = ?",
652 def get_follow_requests(%User{} = user) do
655 |> User.get_follow_requests_query()
656 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
657 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
658 |> group_by([a, u], u.id)
665 def increase_note_count(%User{} = user) do
667 |> where(id: ^user.id)
672 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
679 |> Repo.update_all([])
681 {1, [user]} -> set_cache(user)
686 def decrease_note_count(%User{} = user) do
688 |> where(id: ^user.id)
693 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
700 |> Repo.update_all([])
702 {1, [user]} -> set_cache(user)
707 def update_note_count(%User{} = user) do
711 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
715 note_count = Repo.one(note_count_query)
717 info_cng = User.Info.set_note_count(user.info, note_count)
721 |> put_embed(:info, info_cng)
723 update_and_set_cache(cng)
726 def update_follower_count(%User{} = user) do
727 follower_count_query =
729 |> where([u], ^user.follower_address in u.following)
730 |> where([u], u.id != ^user.id)
731 |> select([u], %{count: count(u.id)})
734 |> where(id: ^user.id)
735 |> join(:inner, [u], s in subquery(follower_count_query))
740 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
747 |> Repo.update_all([])
749 {1, [user]} -> set_cache(user)
754 def get_users_from_set_query(ap_ids, false) do
757 where: u.ap_id in ^ap_ids
761 def get_users_from_set_query(ap_ids, true) do
762 query = get_users_from_set_query(ap_ids, false)
766 where: u.local == true
770 def get_users_from_set(ap_ids, local_only \\ true) do
771 get_users_from_set_query(ap_ids, local_only)
775 def get_recipients_from_activity(%Activity{recipients: to}) do
779 where: u.ap_id in ^to,
780 or_where: fragment("? && ?", u.following, ^to)
783 query = from(u in query, where: u.local == true)
788 def search(query, resolve \\ false, for_user \\ nil) do
789 # Strip the beginning @ off if there is a query
790 query = String.trim_leading(query, "@")
792 if resolve, do: get_or_fetch(query)
795 Repo.transaction(fn ->
796 Ecto.Adapters.SQL.query(Repo, "select set_limit(0.25)", [])
797 Repo.all(search_query(query, for_user))
803 def search_query(query, for_user) do
804 fts_subquery = fts_search_subquery(query)
805 trigram_subquery = trigram_search_subquery(query)
806 union_query = from(s in trigram_subquery, union_all: ^fts_subquery)
807 distinct_query = from(s in subquery(union_query), order_by: s.search_type, distinct: s.id)
809 from(s in subquery(boost_search_rank_query(distinct_query, for_user)),
810 order_by: [desc: s.search_rank],
815 defp boost_search_rank_query(query, nil), do: query
817 defp boost_search_rank_query(query, for_user) do
818 friends_ids = get_friends_ids(for_user)
819 followers_ids = get_followers_ids(for_user)
821 from(u in subquery(query),
826 CASE WHEN (?) THEN (?) * 1.3
827 WHEN (?) THEN (?) * 1.2
828 WHEN (?) THEN (?) * 1.1
831 u.id in ^friends_ids and u.id in ^followers_ids,
833 u.id in ^friends_ids,
835 u.id in ^followers_ids,
843 defp fts_search_subquery(term, query \\ User) do
846 |> String.replace(~r/\W+/, " ")
849 |> Enum.map(&(&1 <> ":*"))
860 setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
861 setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B'),
862 to_tsquery('simple', ?),
874 (setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
875 setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B')) @@ to_tsquery('simple', ?)
884 defp trigram_search_subquery(term) do
888 # ^1 gives 'Postgrex expected a binary, got 1' for some weird reason
889 search_type: fragment("?", 1),
892 "similarity(?, trim(? || ' ' || coalesce(?, '')))",
898 where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term)
902 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers) do
905 fn blocked_identifier ->
906 with %User{} = blocked <- get_or_fetch(blocked_identifier),
907 {:ok, blocker} <- block(blocker, blocked),
908 {:ok, _} <- ActivityPub.block(blocker, blocked) do
912 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
919 def mute(muter, %User{ap_id: ap_id}) do
922 |> User.Info.add_to_mutes(ap_id)
926 |> put_embed(:info, info_cng)
928 update_and_set_cache(cng)
931 def unmute(muter, %{ap_id: ap_id}) do
934 |> User.Info.remove_from_mutes(ap_id)
938 |> put_embed(:info, info_cng)
940 update_and_set_cache(cng)
943 def subscribe(subscriber, %{ap_id: ap_id}) do
944 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
946 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
947 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
950 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
954 |> User.Info.add_to_subscribers(subscriber.ap_id)
957 |> put_embed(:info, info_cng)
958 |> update_and_set_cache()
963 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
964 with %User{} = user <- get_cached_by_ap_id(ap_id) do
967 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
970 |> put_embed(:info, info_cng)
971 |> update_and_set_cache()
975 def block(blocker, %User{ap_id: ap_id} = blocked) do
976 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
978 if following?(blocker, blocked) do
979 {:ok, blocker, _} = unfollow(blocker, blocked)
986 if subscribed_to?(blocked, blocker) do
987 {:ok, blocker} = unsubscribe(blocked, blocker)
993 if following?(blocked, blocker) do
994 unfollow(blocked, blocker)
997 {:ok, blocker} = update_follower_count(blocker)
1001 |> User.Info.add_to_block(ap_id)
1005 |> put_embed(:info, info_cng)
1007 update_and_set_cache(cng)
1010 # helper to handle the block given only an actor's AP id
1011 def block(blocker, %{ap_id: ap_id}) do
1012 block(blocker, get_cached_by_ap_id(ap_id))
1015 def unblock(blocker, %{ap_id: ap_id}) do
1018 |> User.Info.remove_from_block(ap_id)
1022 |> put_embed(:info, info_cng)
1024 update_and_set_cache(cng)
1027 def mutes?(nil, _), do: false
1028 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
1030 def blocks?(user, %{ap_id: ap_id}) do
1031 blocks = user.info.blocks
1032 domain_blocks = user.info.domain_blocks
1033 %{host: host} = URI.parse(ap_id)
1035 Enum.member?(blocks, ap_id) ||
1036 Enum.any?(domain_blocks, fn domain ->
1041 def subscribed_to?(user, %{ap_id: ap_id}) do
1042 with %User{} = target <- get_cached_by_ap_id(ap_id) do
1043 Enum.member?(target.info.subscribers, user.ap_id)
1047 def muted_users(user),
1048 do: Repo.all(from(u in User, where: u.ap_id in ^user.info.mutes))
1050 def blocked_users(user),
1051 do: Repo.all(from(u in User, where: u.ap_id in ^user.info.blocks))
1053 def subscribers(user),
1054 do: Repo.all(from(u in User, where: u.ap_id in ^user.info.subscribers))
1056 def block_domain(user, domain) do
1059 |> User.Info.add_to_domain_block(domain)
1063 |> put_embed(:info, info_cng)
1065 update_and_set_cache(cng)
1068 def unblock_domain(user, domain) do
1071 |> User.Info.remove_from_domain_block(domain)
1075 |> put_embed(:info, info_cng)
1077 update_and_set_cache(cng)
1080 def maybe_local_user_query(query, local) do
1081 if local, do: local_user_query(query), else: query
1084 def local_user_query(query \\ User) do
1087 where: u.local == true,
1088 where: not is_nil(u.nickname)
1092 def maybe_external_user_query(query, external) do
1093 if external, do: external_user_query(query), else: query
1096 def external_user_query(query \\ User) do
1099 where: u.local == false,
1100 where: not is_nil(u.nickname)
1104 def maybe_active_user_query(query, active) do
1105 if active, do: active_user_query(query), else: query
1108 def active_user_query(query \\ User) do
1111 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1112 where: not is_nil(u.nickname)
1116 def maybe_deactivated_user_query(query, deactivated) do
1117 if deactivated, do: deactivated_user_query(query), else: query
1120 def deactivated_user_query(query \\ User) do
1123 where: fragment("(?->'deactivated' @> 'true')", u.info),
1124 where: not is_nil(u.nickname)
1128 def active_local_user_query do
1130 u in local_user_query(),
1131 where: fragment("not (?->'deactivated' @> 'true')", u.info)
1135 def moderator_user_query do
1138 where: u.local == true,
1139 where: fragment("?->'is_moderator' @> 'true'", u.info)
1143 def deactivate(%User{} = user, status \\ true) do
1144 info_cng = User.Info.set_activation_status(user.info, status)
1148 |> put_embed(:info, info_cng)
1150 update_and_set_cache(cng)
1153 def update_notification_settings(%User{} = user, settings \\ %{}) do
1154 info_changeset = User.Info.update_notification_settings(user.info, settings)
1157 |> put_embed(:info, info_changeset)
1158 |> update_and_set_cache()
1161 def delete(%User{} = user) do
1162 {:ok, user} = User.deactivate(user)
1164 # Remove all relationships
1165 {:ok, followers} = User.get_followers(user)
1167 Enum.each(followers, fn follower -> User.unfollow(follower, user) end)
1169 {:ok, friends} = User.get_friends(user)
1171 Enum.each(friends, fn followed -> User.unfollow(user, followed) end)
1173 delete_user_activities(user)
1176 def delete_user_activities(%User{ap_id: ap_id} = user) do
1178 |> where(actor: ^ap_id)
1179 |> Activity.with_preloaded_object()
1182 %{data: %{"type" => "Create"}} = activity ->
1183 activity |> Object.normalize() |> ActivityPub.delete()
1185 # TODO: Do something with likes, follows, repeats.
1193 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1194 Pleroma.HTML.Scrubber.TwitterText
1197 @default_scrubbers Pleroma.Config.get([:markup, :scrub_policy])
1199 def html_filter_policy(_), do: @default_scrubbers
1201 def fetch_by_ap_id(ap_id) do
1202 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1209 case OStatus.make_user(ap_id) do
1211 _ -> {:error, "Could not fetch by AP id"}
1216 def get_or_fetch_by_ap_id(ap_id) do
1217 user = get_cached_by_ap_id(ap_id)
1219 if !is_nil(user) and !User.needs_update?(user) do
1222 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1223 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1225 user = fetch_by_ap_id(ap_id)
1227 if should_fetch_initial do
1228 with %User{} = user do
1229 {:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])
1237 def get_or_create_instance_user do
1238 relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"
1240 if user = get_cached_by_ap_id(relay_uri) do
1244 %User{info: %User.Info{}}
1245 |> cast(%{}, [:ap_id, :nickname, :local])
1246 |> put_change(:ap_id, relay_uri)
1247 |> put_change(:nickname, nil)
1248 |> put_change(:local, true)
1249 |> put_change(:follower_address, relay_uri <> "/followers")
1251 {:ok, user} = Repo.insert(changes)
1257 def public_key_from_info(%{
1258 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1262 |> :public_key.pem_decode()
1264 |> :public_key.pem_entry_decode()
1270 def public_key_from_info(%{magic_key: magic_key}) do
1271 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1274 def get_public_key_for_ap_id(ap_id) do
1275 with %User{} = user <- get_or_fetch_by_ap_id(ap_id),
1276 {:ok, public_key} <- public_key_from_info(user.info) do
1283 defp blank?(""), do: nil
1284 defp blank?(n), do: n
1286 def insert_or_update_user(data) do
1288 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1289 |> remote_user_creation()
1290 |> Repo.insert(on_conflict: :replace_all, conflict_target: :nickname)
1294 def ap_enabled?(%User{local: true}), do: true
1295 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1296 def ap_enabled?(_), do: false
1298 @doc "Gets or fetch a user by uri or nickname."
1299 @spec get_or_fetch(String.t()) :: User.t()
1300 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1301 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1303 # wait a period of time and return newest version of the User structs
1304 # this is because we have synchronous follow APIs and need to simulate them
1305 # with an async handshake
1306 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1307 with %User{} = a <- User.get_cached_by_id(a.id),
1308 %User{} = b <- User.get_cached_by_id(b.id) do
1316 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1317 with :ok <- :timer.sleep(timeout),
1318 %User{} = a <- User.get_cached_by_id(a.id),
1319 %User{} = b <- User.get_cached_by_id(b.id) do
1327 def parse_bio(bio, user \\ %User{info: %{source_data: %{}}})
1328 def parse_bio(nil, _user), do: ""
1329 def parse_bio(bio, _user) when bio == "", do: bio
1331 def parse_bio(bio, user) do
1333 (user.info.source_data["tag"] || [])
1334 |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
1335 |> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
1336 {String.trim(name, ":"), url}
1339 # TODO: get profile URLs other than user.ap_id
1340 profile_urls = [user.ap_id]
1343 |> CommonUtils.format_input("text/plain",
1344 mentions_format: :full,
1345 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1348 |> Formatter.emojify(emoji)
1351 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1352 Repo.transaction(fn ->
1353 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1357 def tag(nickname, tags) when is_binary(nickname),
1358 do: tag(get_by_nickname(nickname), tags)
1360 def tag(%User{} = user, tags),
1361 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1363 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1364 Repo.transaction(fn ->
1365 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1369 def untag(nickname, tags) when is_binary(nickname),
1370 do: untag(get_by_nickname(nickname), tags)
1372 def untag(%User{} = user, tags),
1373 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1375 defp update_tags(%User{} = user, new_tags) do
1376 {:ok, updated_user} =
1378 |> change(%{tags: new_tags})
1379 |> update_and_set_cache()
1384 defp normalize_tags(tags) do
1387 |> Enum.map(&String.downcase(&1))
1390 defp local_nickname_regex do
1391 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1392 @extended_local_nickname_regex
1394 @strict_local_nickname_regex
1398 def local_nickname(nickname_or_mention) do
1401 |> String.split("@")
1405 def full_nickname(nickname_or_mention),
1406 do: String.trim_leading(nickname_or_mention, "@")
1408 def error_user(ap_id) do
1413 nickname: "erroruser@example.com",
1414 inserted_at: NaiveDateTime.utc_now()
1418 def all_superusers do
1421 where: u.local == true,
1422 where: fragment("?->'is_admin' @> 'true' OR ?->'is_moderator' @> 'true'", u.info, u.info)
1427 defp paginate(query, page, page_size) do
1430 offset: ^((page - 1) * page_size)
1434 def showing_reblogs?(%User{} = user, %User{} = target) do
1435 target.ap_id not in user.info.muted_reblogs
1439 The function returns a query to get users with no activity for given interval of days.
1440 Inactive users are those who didn't read any notification, or had any activity where
1441 the user is the activity's actor, during `inactivity_threshold` days.
1442 Deactivated users will not appear in this list.
1446 iex> Pleroma.User.list_inactive_users()
1449 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1450 def list_inactive_users_query(inactivity_threshold \\ 7) do
1451 negative_inactivity_threshold = -inactivity_threshold
1452 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1453 # Subqueries are not supported in `where` clauses, join gets too complicated.
1454 has_read_notifications =
1455 from(n in Pleroma.Notification,
1456 where: n.seen == true,
1458 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1461 |> Pleroma.Repo.all()
1463 from(u in Pleroma.User,
1464 left_join: a in Pleroma.Activity,
1465 on: u.ap_id == a.actor,
1466 where: not is_nil(u.nickname),
1467 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1468 where: u.id not in ^has_read_notifications,
1471 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1472 is_nil(max(a.inserted_at))
1477 Enable or disable email notifications for user
1481 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1482 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1484 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1485 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1487 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1488 {:ok, t()} | {:error, Ecto.Changeset.t()}
1489 def switch_email_notifications(user, type, status) do
1490 info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})
1493 |> put_embed(:info, info)
1494 |> update_and_set_cache()
1498 Set `last_digest_emailed_at` value for the user to current time
1500 @spec touch_last_digest_emailed_at(t()) :: t()
1501 def touch_last_digest_emailed_at(user) do
1502 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1504 {:ok, updated_user} =
1506 |> change(%{last_digest_emailed_at: now})
1507 |> update_and_set_cache()