# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.User do
+ @moduledoc """
+ A user, local or remote
+ """
+
use Ecto.Schema
import Ecto.Changeset
alias Pleroma.Emoji
alias Pleroma.FollowingRelationship
alias Pleroma.Formatter
+ alias Pleroma.Hashtag
+ alias Pleroma.User.HashtagFollow
alias Pleroma.HTML
alias Pleroma.Keys
alias Pleroma.MFA
field(:is_suggested, :boolean, default: false)
field(:last_status_at, :naive_datetime)
field(:language, :string)
+ field(:status_ttl_days, :integer, default: nil)
embeds_one(
:notification_settings,
has_many(:outgoing_relationships, UserRelationship, foreign_key: :source_id)
has_many(:incoming_relationships, UserRelationship, foreign_key: :target_id)
+ has_many(:frontend_profiles, Pleroma.Akkoma.FrontendSettingsProfile)
+
+ many_to_many(:followed_hashtags, Hashtag,
+ on_replace: :delete,
+ on_delete: :delete_all,
+ join_through: HashtagFollow
+ )
+
for {relationship_type,
[
{outgoing_relation, outgoing_relation_target},
:pleroma_settings_store,
:is_discoverable,
:actor_type,
- :disclose_client
+ :disclose_client,
+ :status_ttl_days
]
)
|> unique_constraint(:nickname)
|> validate_length(:bio, max: bio_limit)
|> validate_length(:name, min: 1, max: name_limit)
|> validate_inclusion(:actor_type, ["Person", "Service"])
+ |> validate_number(:status_ttl_days, greater_than: 0)
|> put_fields()
|> put_emoji()
|> put_change_if_present(:bio, &{:ok, parse_bio(&1, 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_ap_id()
|> unique_constraint(:ap_id)
|> put_following_and_follower_and_featured_address()
+ |> 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)
|> put_ap_id()
|> unique_constraint(:ap_id)
|> put_following_and_follower_and_featured_address()
+ |> put_private_key()
end
def maybe_validate_required_email(changeset, true), do: changeset
|> put_change(:featured_address, featured)
end
+ defp put_private_key(changeset) do
+ {:ok, pem} = Keys.generate_rsa_pem()
+ put_change(changeset, :keys, pem)
+ end
+
defp autofollow_users(user) do
candidates = Config.get([:instance, :autofollowed_nicknames])
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{} = user, _} ->
{:ok, user}
- _ ->
+ e ->
+ Logger.error("Could not fetch user #{ap_id}, #{inspect(e)}")
{:error, :not_found}
end
end
follower_address: uri <> "/followers"
}
|> change
+ |> put_private_key()
|> unique_constraint(:nickname)
|> Repo.insert()
|> set_cache()
@doc "Gets or fetch a user by uri or nickname."
@spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
- def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
+ def get_or_fetch("http://" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
+ def get_or_fetch("https://" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
# wait a period of time and return newest version of the User structs
}
end
- def ensure_keys_present(%{keys: keys} = user) when not is_nil(keys), do: {:ok, user}
-
- def ensure_keys_present(%User{} = user) do
- with {:ok, pem} <- Keys.generate_rsa_pem() do
- user
- |> cast(%{keys: pem}, [:keys])
- |> validate_required([:keys])
- |> update_and_set_cache()
- end
- end
-
def get_ap_ids_by_nicknames(nicknames) do
from(u in User,
where: u.nickname in ^nicknames,
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
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
_ -> {:error, user}
end
end
+
+ defp maybe_load_followed_hashtags(%User{followed_hashtags: follows} = user)
+ when is_list(follows),
+ do: user
+
+ defp maybe_load_followed_hashtags(%User{} = user) do
+ followed_hashtags = HashtagFollow.get_by_user(user)
+ %{user | followed_hashtags: followed_hashtags}
+ end
+
+ def followed_hashtags(%User{followed_hashtags: follows})
+ when is_list(follows),
+ do: follows
+
+ def followed_hashtags(%User{} = user) do
+ {:ok, user} =
+ user
+ |> maybe_load_followed_hashtags()
+ |> set_cache()
+
+ user.followed_hashtags
+ end
+
+ def follow_hashtag(%User{} = user, %Hashtag{} = hashtag) do
+ Logger.debug("Follow hashtag #{hashtag.name} for user #{user.nickname}")
+ user = maybe_load_followed_hashtags(user)
+
+ with {:ok, _} <- HashtagFollow.new(user, hashtag),
+ follows <- HashtagFollow.get_by_user(user),
+ %User{} = user <- user |> Map.put(:followed_hashtags, follows) do
+ user
+ |> set_cache()
+ end
+ end
+
+ def unfollow_hashtag(%User{} = user, %Hashtag{} = hashtag) do
+ Logger.debug("Unfollow hashtag #{hashtag.name} for user #{user.nickname}")
+ user = maybe_load_followed_hashtags(user)
+
+ with {:ok, _} <- HashtagFollow.delete(user, hashtag),
+ follows <- HashtagFollow.get_by_user(user),
+ %User{} = user <- user |> Map.put(:followed_hashtags, follows) do
+ user
+ |> set_cache()
+ end
+ end
+
+ def following_hashtag?(%User{} = user, %Hashtag{} = hashtag) do
+ not is_nil(HashtagFollow.get(user, hashtag))
+ end
end