1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.User do
13 alias Pleroma.Activity
15 alias Pleroma.Notification
17 alias Pleroma.Registration
19 alias Pleroma.RepoStreamer
22 alias Pleroma.Web.ActivityPub.ActivityPub
23 alias Pleroma.Web.ActivityPub.Utils
24 alias Pleroma.Web.CommonAPI
25 alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
26 alias Pleroma.Web.OAuth
27 alias Pleroma.Web.OStatus
28 alias Pleroma.Web.RelMe
29 alias Pleroma.Web.Websub
33 @type t :: %__MODULE__{}
35 @primary_key {:id, Pleroma.FlakeId, autogenerate: true}
37 # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
38 @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])?)*$/
40 @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
41 @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/
45 field(:email, :string)
47 field(:nickname, :string)
48 field(:password_hash, :string)
49 field(:password, :string, virtual: true)
50 field(:password_confirmation, :string, virtual: true)
51 field(:following, {:array, :string}, default: [])
52 field(:ap_id, :string)
54 field(:local, :boolean, default: true)
55 field(:follower_address, :string)
56 field(:following_address, :string)
57 field(:search_rank, :float, virtual: true)
58 field(:search_type, :integer, virtual: true)
59 field(:tags, {:array, :string}, default: [])
60 field(:last_refreshed_at, :naive_datetime_usec)
61 field(:last_digest_emailed_at, :naive_datetime)
62 has_many(:notifications, Notification)
63 has_many(:registrations, Registration)
64 embeds_one(:info, User.Info)
69 def auth_active?(%User{info: %User.Info{confirmation_pending: true}}),
70 do: !Pleroma.Config.get([:instance, :account_activation_required])
72 def auth_active?(%User{}), do: true
74 def visible_for?(user, for_user \\ nil)
76 def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
78 def visible_for?(%User{} = user, for_user) do
79 auth_active?(user) || superuser?(for_user)
82 def visible_for?(_, _), do: false
84 def superuser?(%User{local: true, info: %User.Info{is_admin: true}}), do: true
85 def superuser?(%User{local: true, info: %User.Info{is_moderator: true}}), do: true
86 def superuser?(_), do: false
88 def avatar_url(user, options \\ []) do
90 %{"url" => [%{"href" => href} | _]} -> href
91 _ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
95 def banner_url(user, options \\ []) do
96 case user.info.banner do
97 %{"url" => [%{"href" => href} | _]} -> href
98 _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
102 def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url
103 def profile_url(%User{ap_id: ap_id}), do: ap_id
104 def profile_url(_), do: nil
106 def ap_id(%User{nickname: nickname}) do
107 "#{Web.base_url()}/users/#{nickname}"
110 def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
111 def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
113 @spec ap_following(User.t()) :: Sring.t()
114 def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
115 def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
117 def user_info(%User{} = user, args \\ %{}) do
119 if args[:following_count],
120 do: args[:following_count],
121 else: user.info.following_count || following_count(user)
124 if args[:follower_count], do: args[:follower_count], else: user.info.follower_count
127 note_count: user.info.note_count,
128 locked: user.info.locked,
129 confirmation_pending: user.info.confirmation_pending,
130 default_scope: user.info.default_scope
132 |> Map.put(:following_count, following_count)
133 |> Map.put(:follower_count, follower_count)
136 def follow_state(%User{} = user, %User{} = target) do
137 follow_activity = Utils.fetch_latest_follow(user, target)
140 do: follow_activity.data["state"],
141 # Ideally this would be nil, but then Cachex does not commit the value
145 def get_cached_follow_state(user, target) do
146 key = "follow_state:#{user.ap_id}|#{target.ap_id}"
147 Cachex.fetch!(:user_cache, key, fn _ -> {:commit, follow_state(user, target)} end)
150 def set_follow_state_cache(user_ap_id, target_ap_id, state) do
153 "follow_state:#{user_ap_id}|#{target_ap_id}",
158 def set_info_cache(user, args) do
159 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user, args))
162 @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
163 def restrict_deactivated(query) do
165 where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info)
169 def following_count(%User{following: []}), do: 0
171 def following_count(%User{} = user) do
173 |> get_friends_query()
174 |> Repo.aggregate(:count, :id)
177 def remote_user_creation(params) do
178 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
179 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
181 params = Map.put(params, :info, params[:info] || %{})
182 info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])
186 |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
187 |> validate_required([:name, :ap_id])
188 |> unique_constraint(:nickname)
189 |> validate_format(:nickname, @email_regex)
190 |> validate_length(:bio, max: bio_limit)
191 |> validate_length(:name, max: name_limit)
192 |> put_change(:local, false)
193 |> put_embed(:info, info_cng)
196 case info_cng.changes[:source_data] do
197 %{"followers" => followers, "following" => following} ->
199 |> put_change(:follower_address, followers)
200 |> put_change(:following_address, following)
203 followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
206 |> put_change(:follower_address, followers)
213 def update_changeset(struct, params \\ %{}) do
214 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
215 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
218 |> cast(params, [:bio, :name, :avatar, :following])
219 |> unique_constraint(:nickname)
220 |> validate_format(:nickname, local_nickname_regex())
221 |> validate_length(:bio, max: bio_limit)
222 |> validate_length(:name, min: 1, max: name_limit)
225 def upgrade_changeset(struct, params \\ %{}) do
226 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
227 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
229 params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
230 info_cng = User.Info.user_upgrade(struct.info, params[:info])
241 |> unique_constraint(:nickname)
242 |> validate_format(:nickname, local_nickname_regex())
243 |> validate_length(:bio, max: bio_limit)
244 |> validate_length(:name, max: name_limit)
245 |> put_embed(:info, info_cng)
248 def password_update_changeset(struct, params) do
250 |> cast(params, [:password, :password_confirmation])
251 |> validate_required([:password, :password_confirmation])
252 |> validate_confirmation(:password)
256 @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
257 def reset_password(%User{id: user_id} = user, data) do
260 |> Multi.update(:user, password_update_changeset(user, data))
261 |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
262 |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
264 case Repo.transaction(multi) do
265 {:ok, %{user: user} = _} -> set_cache(user)
266 {:error, _, changeset, _} -> {:error, changeset}
270 def register_changeset(struct, params \\ %{}, opts \\ []) do
271 bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
272 name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
275 if is_nil(opts[:need_confirmation]) do
276 Pleroma.Config.get([:instance, :account_activation_required])
278 opts[:need_confirmation]
282 User.Info.confirmation_changeset(%User.Info{}, need_confirmation: need_confirmation?)
286 |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
287 |> validate_required([:name, :nickname, :password, :password_confirmation])
288 |> validate_confirmation(:password)
289 |> unique_constraint(:email)
290 |> unique_constraint(:nickname)
291 |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
292 |> validate_format(:nickname, local_nickname_regex())
293 |> validate_format(:email, @email_regex)
294 |> validate_length(:bio, max: bio_limit)
295 |> validate_length(:name, min: 1, max: name_limit)
296 |> put_change(:info, info_change)
299 if opts[:external] do
302 validate_required(changeset, [:email])
305 if changeset.valid? do
306 ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
307 followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
311 |> put_change(:ap_id, ap_id)
312 |> unique_constraint(:ap_id)
313 |> put_change(:following, [followers])
314 |> put_change(:follower_address, followers)
320 defp autofollow_users(user) do
321 candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames])
324 User.Query.build(%{nickname: candidates, local: true, deactivated: false})
327 follow_all(user, autofollowed_users)
330 @doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
331 def register(%Ecto.Changeset{} = changeset) do
332 with {:ok, user} <- Repo.insert(changeset),
333 {:ok, user} <- autofollow_users(user),
334 {:ok, user} <- set_cache(user),
335 {:ok, _} <- User.WelcomeMessage.post_welcome_message_to_user(user),
336 {:ok, _} <- try_send_confirmation_email(user) do
341 def try_send_confirmation_email(%User{} = user) do
342 if user.info.confirmation_pending &&
343 Pleroma.Config.get([:instance, :account_activation_required]) do
345 |> Pleroma.Emails.UserEmail.account_confirmation_email()
346 |> Pleroma.Emails.Mailer.deliver_async()
354 def needs_update?(%User{local: true}), do: false
356 def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
358 def needs_update?(%User{local: false} = user) do
359 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
362 def needs_update?(_), do: true
364 @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
365 def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
369 def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
370 follow(follower, followed)
373 def maybe_direct_follow(%User{} = follower, %User{} = followed) do
374 if not User.ap_enabled?(followed) do
375 follow(follower, followed)
381 @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
382 @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
383 def follow_all(follower, followeds) do
386 |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
387 |> Enum.map(fn %{follower_address: fa} -> fa end)
391 where: u.id == ^follower.id,
396 "array(select distinct unnest (array_cat(?, ?)))",
405 {1, [follower]} = Repo.update_all(q, [])
407 Enum.each(followeds, fn followed ->
408 update_follower_count(followed)
414 def follow(%User{} = follower, %User{info: info} = followed) do
415 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
416 ap_followers = followed.follower_address
420 {:error, "Could not follow user: You are deactivated."}
422 deny_follow_blocked and blocks?(followed, follower) ->
423 {:error, "Could not follow user: #{followed.nickname} blocked you."}
426 if !followed.local && follower.local && !ap_enabled?(followed) do
427 Websub.subscribe(follower, followed)
432 where: u.id == ^follower.id,
433 update: [push: [following: ^ap_followers]],
437 {1, [follower]} = Repo.update_all(q, [])
439 follower = maybe_update_following_count(follower)
441 {:ok, _} = update_follower_count(followed)
447 def unfollow(%User{} = follower, %User{} = followed) do
448 ap_followers = followed.follower_address
450 if following?(follower, followed) and follower.ap_id != followed.ap_id do
453 where: u.id == ^follower.id,
454 update: [pull: [following: ^ap_followers]],
458 {1, [follower]} = Repo.update_all(q, [])
460 follower = maybe_update_following_count(follower)
462 {:ok, followed} = update_follower_count(followed)
466 {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
468 {:error, "Not subscribed!"}
472 @spec following?(User.t(), User.t()) :: boolean
473 def following?(%User{} = follower, %User{} = followed) do
474 Enum.member?(follower.following, followed.follower_address)
477 def locked?(%User{} = user) do
478 user.info.locked || false
482 Repo.get_by(User, id: id)
485 def get_by_ap_id(ap_id) do
486 Repo.get_by(User, ap_id: ap_id)
489 def get_all_by_ap_id(ap_ids) do
490 from(u in __MODULE__,
491 where: u.ap_id in ^ap_ids
496 # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
497 # of the ap_id and the domain and tries to get that user
498 def get_by_guessed_nickname(ap_id) do
499 domain = URI.parse(ap_id).host
500 name = List.last(String.split(ap_id, "/"))
501 nickname = "#{name}@#{domain}"
503 get_cached_by_nickname(nickname)
506 def set_cache({:ok, user}), do: set_cache(user)
507 def set_cache({:error, err}), do: {:error, err}
509 def set_cache(%User{} = user) do
510 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
511 Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
512 Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
516 def update_and_set_cache(changeset) do
517 with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
524 def invalidate_cache(user) do
525 Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
526 Cachex.del(:user_cache, "nickname:#{user.nickname}")
527 Cachex.del(:user_cache, "user_info:#{user.id}")
530 def get_cached_by_ap_id(ap_id) do
531 key = "ap_id:#{ap_id}"
532 Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
535 def get_cached_by_id(id) do
539 Cachex.fetch!(:user_cache, key, fn _ ->
543 Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
544 {:commit, user.ap_id}
550 get_cached_by_ap_id(ap_id)
553 def get_cached_by_nickname(nickname) do
554 key = "nickname:#{nickname}"
556 Cachex.fetch!(:user_cache, key, fn ->
557 user_result = get_or_fetch_by_nickname(nickname)
560 {:ok, user} -> {:commit, user}
561 {:error, _error} -> {:ignore, nil}
566 def get_cached_by_nickname_or_id(nickname_or_id) do
567 get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
570 def get_by_nickname(nickname) do
571 Repo.get_by(User, nickname: nickname) ||
572 if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
573 Repo.get_by(User, nickname: local_nickname(nickname))
577 def get_by_email(email), do: Repo.get_by(User, email: email)
579 def get_by_nickname_or_email(nickname_or_email) do
580 get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
583 def get_cached_user_info(user) do
584 key = "user_info:#{user.id}"
585 Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
588 def fetch_by_nickname(nickname) do
589 ap_try = ActivityPub.make_user_from_nickname(nickname)
592 {:ok, user} -> {:ok, user}
593 _ -> OStatus.make_user(nickname)
597 def get_or_fetch_by_nickname(nickname) do
598 with %User{} = user <- get_by_nickname(nickname) do
602 with [_nick, _domain] <- String.split(nickname, "@"),
603 {:ok, user} <- fetch_by_nickname(nickname) do
604 if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
605 fetch_initial_posts(user)
610 _e -> {:error, "not found " <> nickname}
615 @doc "Fetch some posts when the user has just been federated with"
616 def fetch_initial_posts(user),
617 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:fetch_initial_posts, user])
619 @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
620 def get_followers_query(%User{} = user, nil) do
621 User.Query.build(%{followers: user, deactivated: false})
624 def get_followers_query(user, page) do
625 from(u in get_followers_query(user, nil))
626 |> User.Query.paginate(page, 20)
629 @spec get_followers_query(User.t()) :: Ecto.Query.t()
630 def get_followers_query(user), do: get_followers_query(user, nil)
632 @spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
633 def get_followers(user, page \\ nil) do
634 q = get_followers_query(user, page)
639 @spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
640 def get_external_followers(user, page \\ nil) do
643 |> get_followers_query(page)
644 |> User.Query.build(%{external: true})
649 def get_followers_ids(user, page \\ nil) do
650 q = get_followers_query(user, page)
652 Repo.all(from(u in q, select: u.id))
655 @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
656 def get_friends_query(%User{} = user, nil) do
657 User.Query.build(%{friends: user, deactivated: false})
660 def get_friends_query(user, page) do
661 from(u in get_friends_query(user, nil))
662 |> User.Query.paginate(page, 20)
665 @spec get_friends_query(User.t()) :: Ecto.Query.t()
666 def get_friends_query(user), do: get_friends_query(user, nil)
668 def get_friends(user, page \\ nil) do
669 q = get_friends_query(user, page)
674 def get_friends_ids(user, page \\ nil) do
675 q = get_friends_query(user, page)
677 Repo.all(from(u in q, select: u.id))
680 @spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
681 def get_follow_requests(%User{} = user) do
683 Activity.follow_requests_for_actor(user)
684 |> join(:inner, [a], u in User, on: a.actor == u.ap_id)
685 |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
686 |> group_by([a, u], u.id)
693 def increase_note_count(%User{} = user) do
695 |> where(id: ^user.id)
700 "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
707 |> Repo.update_all([])
709 {1, [user]} -> set_cache(user)
714 def decrease_note_count(%User{} = user) do
716 |> where(id: ^user.id)
721 "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
728 |> Repo.update_all([])
730 {1, [user]} -> set_cache(user)
735 def update_note_count(%User{} = user) do
739 where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
743 note_count = Repo.one(note_count_query)
745 info_cng = User.Info.set_note_count(user.info, note_count)
749 |> put_embed(:info, info_cng)
750 |> update_and_set_cache()
753 def maybe_fetch_follow_information(user) do
754 with {:ok, user} <- fetch_follow_information(user) do
758 Logger.error("Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}")
764 def fetch_follow_information(user) do
765 with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
766 info_cng = User.Info.follow_information_update(user.info, info)
771 |> put_embed(:info, info_cng)
773 update_and_set_cache(changeset)
780 def update_follower_count(%User{} = user) do
781 if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do
782 follower_count_query =
783 User.Query.build(%{followers: user, deactivated: false})
784 |> select([u], %{count: count(u.id)})
787 |> where(id: ^user.id)
788 |> join(:inner, [u], s in subquery(follower_count_query))
793 "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
800 |> Repo.update_all([])
802 {1, [user]} -> set_cache(user)
806 {:ok, maybe_fetch_follow_information(user)}
810 def maybe_update_following_count(%User{local: false} = user) do
811 if Pleroma.Config.get([:instance, :external_user_synchronization]) do
812 {:ok, maybe_fetch_follow_information(user)}
818 def maybe_update_following_count(user), do: user
820 def remove_duplicated_following(%User{following: following} = user) do
821 uniq_following = Enum.uniq(following)
823 if length(following) == length(uniq_following) do
827 |> update_changeset(%{following: uniq_following})
828 |> update_and_set_cache()
832 @spec get_users_from_set([String.t()], boolean()) :: [User.t()]
833 def get_users_from_set(ap_ids, local_only \\ true) do
834 criteria = %{ap_id: ap_ids, deactivated: false}
835 criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria
837 User.Query.build(criteria)
841 @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
842 def get_recipients_from_activity(%Activity{recipients: to}) do
843 User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
847 @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
848 def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
852 User.Info.add_to_mutes(info, ap_id)
853 |> User.Info.add_to_muted_notifications(info, ap_id, notifications?)
857 |> put_embed(:info, info_cng)
859 update_and_set_cache(cng)
862 def unmute(muter, %{ap_id: ap_id}) do
866 User.Info.remove_from_mutes(info, ap_id)
867 |> User.Info.remove_from_muted_notifications(info, ap_id)
871 |> put_embed(:info, info_cng)
873 update_and_set_cache(cng)
876 def subscribe(subscriber, %{ap_id: ap_id}) do
877 deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
879 with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
880 blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
883 {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
887 |> User.Info.add_to_subscribers(subscriber.ap_id)
890 |> put_embed(:info, info_cng)
891 |> update_and_set_cache()
896 def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
897 with %User{} = user <- get_cached_by_ap_id(ap_id) do
900 |> User.Info.remove_from_subscribers(unsubscriber.ap_id)
903 |> put_embed(:info, info_cng)
904 |> update_and_set_cache()
908 def block(blocker, %User{ap_id: ap_id} = blocked) do
909 # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
911 if following?(blocker, blocked) do
912 {:ok, blocker, _} = unfollow(blocker, blocked)
918 # clear any requested follows as well
920 case CommonAPI.reject_follow_request(blocked, blocker) do
921 {:ok, %User{} = updated_blocked} -> updated_blocked
926 if subscribed_to?(blocked, blocker) do
927 {:ok, blocker} = unsubscribe(blocked, blocker)
933 if following?(blocked, blocker) do
934 unfollow(blocked, blocker)
937 {:ok, blocker} = update_follower_count(blocker)
941 |> User.Info.add_to_block(ap_id)
945 |> put_embed(:info, info_cng)
947 update_and_set_cache(cng)
950 # helper to handle the block given only an actor's AP id
951 def block(blocker, %{ap_id: ap_id}) do
952 block(blocker, get_cached_by_ap_id(ap_id))
955 def unblock(blocker, %{ap_id: ap_id}) do
958 |> User.Info.remove_from_block(ap_id)
962 |> put_embed(:info, info_cng)
964 update_and_set_cache(cng)
967 def mutes?(nil, _), do: false
968 def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
970 @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean()
971 def muted_notifications?(nil, _), do: false
973 def muted_notifications?(user, %{ap_id: ap_id}),
974 do: Enum.member?(user.info.muted_notifications, ap_id)
976 def blocks?(%User{} = user, %User{} = target) do
977 blocks_ap_id?(user, target) || blocks_domain?(user, target)
980 def blocks?(nil, _), do: false
982 def blocks_ap_id?(%User{} = user, %User{} = target) do
983 Enum.member?(user.info.blocks, target.ap_id)
986 def blocks_ap_id?(_, _), do: false
988 def blocks_domain?(%User{} = user, %User{} = target) do
989 domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks)
990 %{host: host} = URI.parse(target.ap_id)
991 Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host)
994 def blocks_domain?(_, _), do: false
996 def subscribed_to?(user, %{ap_id: ap_id}) do
997 with %User{} = target <- get_cached_by_ap_id(ap_id) do
998 Enum.member?(target.info.subscribers, user.ap_id)
1002 @spec muted_users(User.t()) :: [User.t()]
1003 def muted_users(user) do
1004 User.Query.build(%{ap_id: user.info.mutes, deactivated: false})
1008 @spec blocked_users(User.t()) :: [User.t()]
1009 def blocked_users(user) do
1010 User.Query.build(%{ap_id: user.info.blocks, deactivated: false})
1014 @spec subscribers(User.t()) :: [User.t()]
1015 def subscribers(user) do
1016 User.Query.build(%{ap_id: user.info.subscribers, deactivated: false})
1020 def block_domain(user, domain) do
1023 |> User.Info.add_to_domain_block(domain)
1027 |> put_embed(:info, info_cng)
1029 update_and_set_cache(cng)
1032 def unblock_domain(user, domain) do
1035 |> User.Info.remove_from_domain_block(domain)
1039 |> put_embed(:info, info_cng)
1041 update_and_set_cache(cng)
1044 def deactivate_async(user, status \\ true) do
1045 PleromaJobQueue.enqueue(:background, __MODULE__, [:deactivate_async, user, status])
1048 def deactivate(%User{} = user, status \\ true) do
1049 info_cng = User.Info.set_activation_status(user.info, status)
1051 with {:ok, friends} <- User.get_friends(user),
1052 {:ok, followers} <- User.get_followers(user),
1056 |> put_embed(:info, info_cng)
1057 |> update_and_set_cache() do
1058 Enum.each(followers, &invalidate_cache(&1))
1059 Enum.each(friends, &update_follower_count(&1))
1065 def update_notification_settings(%User{} = user, settings \\ %{}) do
1066 info_changeset = User.Info.update_notification_settings(user.info, settings)
1069 |> put_embed(:info, info_changeset)
1070 |> update_and_set_cache()
1073 @spec delete(User.t()) :: :ok
1074 def delete(%User{} = user),
1075 do: PleromaJobQueue.enqueue(:background, __MODULE__, [:delete, user])
1077 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1078 def perform(:delete, %User{} = user) do
1079 {:ok, _user} = ActivityPub.delete(user)
1081 # Remove all relationships
1082 {:ok, followers} = User.get_followers(user)
1084 Enum.each(followers, fn follower ->
1085 ActivityPub.unfollow(follower, user)
1086 User.unfollow(follower, user)
1089 {:ok, friends} = User.get_friends(user)
1091 Enum.each(friends, fn followed ->
1092 ActivityPub.unfollow(user, followed)
1093 User.unfollow(user, followed)
1096 delete_user_activities(user)
1097 invalidate_cache(user)
1101 @spec perform(atom(), User.t()) :: {:ok, User.t()}
1102 def perform(:fetch_initial_posts, %User{} = user) do
1103 pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
1106 # Insert all the posts in reverse order, so they're in the right order on the timeline
1107 Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
1108 &Pleroma.Web.Federator.incoming_ap_doc/1
1114 def perform(:deactivate_async, user, status), do: deactivate(user, status)
1116 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1117 def perform(:blocks_import, %User{} = blocker, blocked_identifiers)
1118 when is_list(blocked_identifiers) do
1120 blocked_identifiers,
1121 fn blocked_identifier ->
1122 with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier),
1123 {:ok, blocker} <- block(blocker, blocked),
1124 {:ok, _} <- ActivityPub.block(blocker, blocked) do
1128 Logger.debug("blocks_import failed for #{blocked_identifier} with: #{inspect(err)}")
1135 @spec perform(atom(), User.t(), list()) :: list() | {:error, any()}
1136 def perform(:follow_import, %User{} = follower, followed_identifiers)
1137 when is_list(followed_identifiers) do
1139 followed_identifiers,
1140 fn followed_identifier ->
1141 with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier),
1142 {:ok, follower} <- maybe_direct_follow(follower, followed),
1143 {:ok, _} <- ActivityPub.follow(follower, followed) do
1147 Logger.debug("follow_import failed for #{followed_identifier} with: #{inspect(err)}")
1154 @spec external_users_query() :: Ecto.Query.t()
1155 def external_users_query do
1163 @spec external_users(keyword()) :: [User.t()]
1164 def external_users(opts \\ []) do
1166 external_users_query()
1167 |> select([u], struct(u, [:id, :ap_id, :info]))
1171 do: where(query, [u], u.id > ^opts[:max_id]),
1176 do: limit(query, ^opts[:limit]),
1182 def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_identifiers),
1184 PleromaJobQueue.enqueue(:background, __MODULE__, [
1190 def follow_import(%User{} = follower, followed_identifiers) when is_list(followed_identifiers),
1192 PleromaJobQueue.enqueue(:background, __MODULE__, [
1195 followed_identifiers
1198 def delete_user_activities(%User{ap_id: ap_id} = user) do
1200 |> Activity.query_by_actor()
1201 |> RepoStreamer.chunk_stream(50)
1202 |> Stream.each(fn activities ->
1203 Enum.each(activities, &delete_activity(&1))
1210 defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
1212 |> Object.normalize()
1213 |> ActivityPub.delete()
1216 defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
1217 user = get_cached_by_ap_id(activity.actor)
1218 object = Object.normalize(activity)
1220 ActivityPub.unlike(user, object)
1223 defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
1224 user = get_cached_by_ap_id(activity.actor)
1225 object = Object.normalize(activity)
1227 ActivityPub.unannounce(user, object)
1230 defp delete_activity(_activity), do: "Doing nothing"
1232 def html_filter_policy(%User{info: %{no_rich_text: true}}) do
1233 Pleroma.HTML.Scrubber.TwitterText
1236 def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])
1238 def fetch_by_ap_id(ap_id) do
1239 ap_try = ActivityPub.make_user_from_ap_id(ap_id)
1246 case OStatus.make_user(ap_id) do
1247 {:ok, user} -> {:ok, user}
1248 _ -> {:error, "Could not fetch by AP id"}
1253 def get_or_fetch_by_ap_id(ap_id) do
1254 user = get_cached_by_ap_id(ap_id)
1256 if !is_nil(user) and !User.needs_update?(user) do
1259 # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
1260 should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled])
1262 resp = fetch_by_ap_id(ap_id)
1264 if should_fetch_initial do
1265 with {:ok, %User{} = user} <- resp do
1266 fetch_initial_posts(user)
1274 @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
1275 def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
1276 if user = get_cached_by_ap_id(uri) do
1280 %User{info: %User.Info{}}
1281 |> cast(%{}, [:ap_id, :nickname, :local])
1282 |> put_change(:ap_id, uri)
1283 |> put_change(:nickname, nickname)
1284 |> put_change(:local, true)
1285 |> put_change(:follower_address, uri <> "/followers")
1287 {:ok, user} = Repo.insert(changes)
1293 def public_key_from_info(%{
1294 source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
1298 |> :public_key.pem_decode()
1300 |> :public_key.pem_entry_decode()
1306 def public_key_from_info(%{magic_key: magic_key}) when not is_nil(magic_key) do
1307 {:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
1310 def public_key_from_info(_), do: {:error, "not found key"}
1312 def get_public_key_for_ap_id(ap_id) do
1313 with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
1314 {:ok, public_key} <- public_key_from_info(user.info) do
1321 defp blank?(""), do: nil
1322 defp blank?(n), do: n
1324 def insert_or_update_user(data) do
1326 |> Map.put(:name, blank?(data[:name]) || data[:nickname])
1327 |> remote_user_creation()
1328 |> Repo.insert(on_conflict: :replace_all_except_primary_key, conflict_target: :nickname)
1332 def ap_enabled?(%User{local: true}), do: true
1333 def ap_enabled?(%User{info: info}), do: info.ap_enabled
1334 def ap_enabled?(_), do: false
1336 @doc "Gets or fetch a user by uri or nickname."
1337 @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
1338 def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
1339 def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
1341 # wait a period of time and return newest version of the User structs
1342 # this is because we have synchronous follow APIs and need to simulate them
1343 # with an async handshake
1344 def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
1345 with %User{} = a <- User.get_cached_by_id(a.id),
1346 %User{} = b <- User.get_cached_by_id(b.id) do
1354 def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
1355 with :ok <- :timer.sleep(timeout),
1356 %User{} = a <- User.get_cached_by_id(a.id),
1357 %User{} = b <- User.get_cached_by_id(b.id) do
1365 def parse_bio(bio) when is_binary(bio) and bio != "" do
1367 |> CommonUtils.format_input("text/plain", mentions_format: :full)
1371 def parse_bio(_), do: ""
1373 def parse_bio(bio, user) when is_binary(bio) and bio != "" do
1374 # TODO: get profile URLs other than user.ap_id
1375 profile_urls = [user.ap_id]
1378 |> CommonUtils.format_input("text/plain",
1379 mentions_format: :full,
1380 rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
1385 def parse_bio(_, _), do: ""
1387 def tag(user_identifiers, tags) when is_list(user_identifiers) do
1388 Repo.transaction(fn ->
1389 for user_identifier <- user_identifiers, do: tag(user_identifier, tags)
1393 def tag(nickname, tags) when is_binary(nickname),
1394 do: tag(get_by_nickname(nickname), tags)
1396 def tag(%User{} = user, tags),
1397 do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
1399 def untag(user_identifiers, tags) when is_list(user_identifiers) do
1400 Repo.transaction(fn ->
1401 for user_identifier <- user_identifiers, do: untag(user_identifier, tags)
1405 def untag(nickname, tags) when is_binary(nickname),
1406 do: untag(get_by_nickname(nickname), tags)
1408 def untag(%User{} = user, tags),
1409 do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
1411 defp update_tags(%User{} = user, new_tags) do
1412 {:ok, updated_user} =
1414 |> change(%{tags: new_tags})
1415 |> update_and_set_cache()
1420 defp normalize_tags(tags) do
1423 |> Enum.map(&String.downcase(&1))
1426 defp local_nickname_regex do
1427 if Pleroma.Config.get([:instance, :extended_nickname_format]) do
1428 @extended_local_nickname_regex
1430 @strict_local_nickname_regex
1434 def local_nickname(nickname_or_mention) do
1437 |> String.split("@")
1441 def full_nickname(nickname_or_mention),
1442 do: String.trim_leading(nickname_or_mention, "@")
1444 def error_user(ap_id) do
1449 nickname: "erroruser@example.com",
1450 inserted_at: NaiveDateTime.utc_now()
1454 @spec all_superusers() :: [User.t()]
1455 def all_superusers do
1456 User.Query.build(%{super_users: true, local: true, deactivated: false})
1460 def showing_reblogs?(%User{} = user, %User{} = target) do
1461 target.ap_id not in user.info.muted_reblogs
1465 The function returns a query to get users with no activity for given interval of days.
1466 Inactive users are those who didn't read any notification, or had any activity where
1467 the user is the activity's actor, during `inactivity_threshold` days.
1468 Deactivated users will not appear in this list.
1472 iex> Pleroma.User.list_inactive_users()
1475 @spec list_inactive_users_query(integer()) :: Ecto.Query.t()
1476 def list_inactive_users_query(inactivity_threshold \\ 7) do
1477 negative_inactivity_threshold = -inactivity_threshold
1478 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1479 # Subqueries are not supported in `where` clauses, join gets too complicated.
1480 has_read_notifications =
1481 from(n in Pleroma.Notification,
1482 where: n.seen == true,
1484 having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"),
1487 |> Pleroma.Repo.all()
1489 from(u in Pleroma.User,
1490 left_join: a in Pleroma.Activity,
1491 on: u.ap_id == a.actor,
1492 where: not is_nil(u.nickname),
1493 where: fragment("not (?->'deactivated' @> 'true')", u.info),
1494 where: u.id not in ^has_read_notifications,
1497 max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or
1498 is_nil(max(a.inserted_at))
1503 Enable or disable email notifications for user
1507 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true)
1508 Pleroma.User{info: %{email_notifications: %{"digest" => true}}}
1510 iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false)
1511 Pleroma.User{info: %{email_notifications: %{"digest" => false}}}
1513 @spec switch_email_notifications(t(), String.t(), boolean()) ::
1514 {:ok, t()} | {:error, Ecto.Changeset.t()}
1515 def switch_email_notifications(user, type, status) do
1516 info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})
1519 |> put_embed(:info, info)
1520 |> update_and_set_cache()
1524 Set `last_digest_emailed_at` value for the user to current time
1526 @spec touch_last_digest_emailed_at(t()) :: t()
1527 def touch_last_digest_emailed_at(user) do
1528 now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second)
1530 {:ok, updated_user} =
1532 |> change(%{last_digest_emailed_at: now})
1533 |> update_and_set_cache()
1538 @spec toggle_confirmation(User.t()) :: {:ok, User.t()} | {:error, Changeset.t()}
1539 def toggle_confirmation(%User{} = user) do
1540 need_confirmation? = !user.info.confirmation_pending
1543 User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)
1547 |> put_embed(:info, info_changeset)
1548 |> update_and_set_cache()
1551 def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
1555 def get_mascot(%{info: %{mascot: mascot}}) when is_nil(mascot) do
1556 # use instance-default
1557 config = Pleroma.Config.get([:assets, :mascots])
1558 default_mascot = Pleroma.Config.get([:assets, :default_mascot])
1559 mascot = Keyword.get(config, default_mascot)
1562 "id" => "default-mascot",
1563 "url" => mascot[:url],
1564 "preview_url" => mascot[:url],
1566 "mime_type" => mascot[:mime_type]
1571 def ensure_keys_present(%User{info: info} = user) do
1575 {:ok, pem} = Keys.generate_rsa_pem()
1578 |> Ecto.Changeset.change()
1579 |> Ecto.Changeset.put_embed(:info, User.Info.set_keys(info, pem))
1580 |> update_and_set_cache()
1584 def get_ap_ids_by_nicknames(nicknames) do
1586 where: u.nickname in ^nicknames,
1592 defdelegate search(query, opts \\ []), to: User.Search
1594 defp put_password_hash(
1595 %Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
1597 change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
1600 defp put_password_hash(changeset), do: changeset
1602 def is_internal_user?(%User{nickname: nil}), do: true
1603 def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true
1604 def is_internal_user?(_), do: false