# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.User do
+ @moduledoc """
+ A user, local or remote
+ """
+
use Ecto.Schema
import Ecto.Changeset
defdelegate following(user), to: FollowingRelationship
defdelegate following?(follower, followed), to: FollowingRelationship
defdelegate following_ap_ids(user), to: FollowingRelationship
- defdelegate get_follow_requests(user), to: FollowingRelationship
+ defdelegate get_follow_requests_query(user), to: FollowingRelationship
+
+ def get_follow_requests(user) do
+ get_follow_requests_query(user)
+ |> Repo.all()
+ end
+
defdelegate search(query, opts \\ []), to: User.Search
@doc """
|> validate_format(:nickname, @email_regex)
|> validate_length(:bio, max: bio_limit)
|> validate_length(:name, max: name_limit)
- |> validate_fields(true)
+ |> validate_fields(true, struct)
|> validate_non_local()
end
:pleroma_settings_store,
&{:ok, Map.merge(struct.pleroma_settings_store, &1)}
)
- |> validate_fields(false)
+ |> validate_fields(false, struct)
end
defp put_fields(changeset) do
+ # These fields are inconsistent in tests when it comes to binary/atom keys
if raw_fields = get_change(changeset, :raw_fields) do
raw_fields =
raw_fields
+ |> Enum.map(fn
+ %{name: name, value: value} ->
+ %{"name" => name, "value" => value}
+
+ %{"name" => _} = field ->
+ field
+ end)
|> Enum.filter(fn %{"name" => n} -> n != "" end)
fields =
{:ok, new_value} <- value_function.(value) do
put_change(changeset, map_field, new_value)
else
- _ -> changeset
+ {:error, :file_too_large} ->
+ Ecto.Changeset.validate_change(changeset, map_field, fn map_field, _value ->
+ [{map_field, "file is too large"}]
+ end)
+
+ _ ->
+ changeset
end
end
|> put_private_key()
end
- def register_changeset(struct, params \\ %{}, opts \\ []) do
+ @spec register_changeset(User.t(), map(), keyword()) :: Changeset.t()
+ def register_changeset(%User{} = struct, params \\ %{}, opts \\ []) do
bio_limit = Config.get([:instance, :user_bio_length], 5000)
name_limit = Config.get([:instance, :user_name_length], 100)
reason_limit = Config.get([:instance, :registration_reason_length], 500)
end
@doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
+ @spec register(Changeset.t()) :: {:ok, User.t()} | {:error, any} | nil
def register(%Ecto.Changeset{} = changeset) do
with {:ok, user} <- Repo.insert(changeset) do
post_register_action(user)
end
end
+ @spec post_register_action(User.t()) :: {:error, any} | {:ok, User.t()}
def post_register_action(%User{is_confirmed: false} = user) do
with {:ok, _} <- maybe_send_confirmation_email(user) do
{:ok, user}
def needs_update?(_), do: true
- @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
+ @spec maybe_direct_follow(User.t(), User.t()) ::
+ {:ok, User.t(), User.t()} | {:error, String.t()}
# "Locked" (self-locked) users demand explicit authorization of follow requests
def maybe_direct_follow(%User{} = follower, %User{local: true, is_locked: true} = followed) do
get_cached_by_nickname(nickname)
end
+ @spec set_cache(
+ {:error, any}
+ | {:ok, User.t()}
+ | User.t()
+ ) :: {:ok, User.t()} | {:error, any}
def set_cache({:ok, user}), do: set_cache(user)
def set_cache({:error, err}), do: {:error, err}
{:ok, user}
end
+ @spec update_and_set_cache(User.t(), map()) :: {:ok, User.t()} | {:error, any}
def update_and_set_cache(struct, params) do
struct
|> update_changeset(params)
|> update_and_set_cache()
end
+ @spec update_and_set_cache(Changeset.t()) :: {:ok, User.t()} | {:error, any}
def update_and_set_cache(%{data: %Pleroma.User{} = user} = changeset) do
was_superuser_before_update = User.superuser?(user)
end
end
+ @spec get_cached_by_id(String.t()) :: nil | Pleroma.User.t()
def get_cached_by_id(id) do
key = "id:#{id}"
%User{
invisible: true,
local: true,
+ actor_type: "Application",
ap_id: uri,
nickname: nickname,
follower_address: uri <> "/followers"
# TODO: get profile URLs other than user.ap_id
profile_urls = [user.ap_id]
- bio
- |> CommonUtils.format_input("text/plain",
+ CommonUtils.format_input(bio, "text/plain",
mentions_format: :full,
- rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
+ rel: fn link ->
+ case RelMe.maybe_put_rel_me(link, profile_urls) do
+ "me" -> "me"
+ _ -> nil
+ end
+ end
)
|> elem(0)
end
defp put_password_hash(
%Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
) do
- change(changeset, password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt(password))
+ change(changeset, password_hash: Pleroma.Password.hash_pwd_salt(password))
end
defp put_password_hash(changeset), do: changeset
end
end
+ @spec delete_alias(User.t(), User.t()) :: {:error, :no_such_alias}
def delete_alias(user, alias_user) do
current_aliases = user.also_known_as || []
alias_ap_id = alias_user.ap_id
|> update_and_set_cache()
end
- def validate_fields(changeset, remote? \\ false) do
+ @spec validate_fields(Ecto.Changeset.t(), Boolean.t(), User.t()) :: Ecto.Changeset.t()
+ def validate_fields(changeset, remote? \\ false, struct) do
limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
limit = Config.get([:instance, limit_name], 0)
[fields: "invalid"]
end
end)
+ |> maybe_validate_rel_me_field(struct)
end
defp valid_field?(%{"name" => name, "value" => value}) do
defp valid_field?(_), do: false
+ defp is_url(nil), do: nil
+
+ defp is_url(uri) do
+ case URI.parse(uri) do
+ %URI{host: nil} -> false
+ %URI{scheme: nil} -> false
+ _ -> true
+ end
+ end
+
+ @spec maybe_validate_rel_me_field(Changeset.t(), User.t()) :: Changeset.t()
+ defp maybe_validate_rel_me_field(changeset, %User{ap_id: _ap_id} = struct) do
+ fields = get_change(changeset, :fields)
+ raw_fields = get_change(changeset, :raw_fields)
+
+ if is_nil(fields) do
+ changeset
+ else
+ validate_rel_me_field(changeset, fields, raw_fields, struct)
+ end
+ end
+
+ defp maybe_validate_rel_me_field(changeset, _), do: changeset
+
+ @spec validate_rel_me_field(Changeset.t(), [Map.t()], [Map.t()], User.t()) :: Changeset.t()
+ defp validate_rel_me_field(changeset, fields, raw_fields, %User{
+ nickname: nickname,
+ ap_id: ap_id
+ }) do
+ fields =
+ fields
+ |> Enum.with_index()
+ |> Enum.map(fn {%{"name" => name, "value" => value}, index} ->
+ raw_value =
+ if is_nil(raw_fields) do
+ nil
+ else
+ Enum.at(raw_fields, index)["value"]
+ end
+
+ if is_url(raw_value) do
+ frontend_url =
+ Pleroma.Web.Router.Helpers.redirect_url(
+ Pleroma.Web.Endpoint,
+ :redirector_with_meta,
+ nickname
+ )
+
+ possible_urls = [ap_id, frontend_url]
+
+ with "me" <- RelMe.maybe_put_rel_me(raw_value, possible_urls) do
+ %{
+ "name" => name,
+ "value" => value,
+ "verified_at" => DateTime.to_iso8601(DateTime.utc_now())
+ }
+ else
+ e ->
+ Logger.error("Could not check for rel=me, #{inspect(e)}")
+ %{"name" => name, "value" => value}
+ end
+ else
+ %{"name" => name, "value" => value}
+ end
+ end)
+
+ put_change(changeset, :fields, fields)
+ end
+
defp truncate_field(%{"name" => name, "value" => value}) do
{name, _chopped} =
String.split_at(name, Config.get([:instance, :account_field_name_length], 255))
cast(user, params, [:is_confirmed, :confirmation_token])
end
- @spec approval_changeset(User.t(), keyword()) :: Changeset.t()
+ @spec approval_changeset(Changeset.t(), keyword()) :: Changeset.t()
def approval_changeset(user, set_approval: approved?) do
cast(user, %{is_approved: approved?}, [:is_approved])
end
with {:ok, relationship} <- UserRelationship.create_block(user, blocked) do
@cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
{:ok, relationship}
+ else
+ err -> err
end
end
- @spec add_to_block(User.t(), User.t()) ::
+ @spec remove_from_block(User.t(), User.t()) ::
{:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()}
defp remove_from_block(%User{} = user, %User{} = blocked) do
with {:ok, relationship} <- UserRelationship.delete_block(user, blocked) do
@cachex.del(:user_cache, "blocked_users_ap_ids:#{user.ap_id}")
{:ok, relationship}
+ else
+ err -> err
end
end
# - display name
def sanitize_html(%User{} = user, filter) do
fields =
- Enum.map(user.fields, fn %{"name" => name, "value" => value} ->
- %{
- "name" => name,
- "value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
- }
+ Enum.map(user.fields, fn %{"value" => value} = field ->
+ Map.put(field, "value", HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly))
end)
user