alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.UserRelationship
- alias Pleroma.Web
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
+ alias Pleroma.Web.Endpoint
alias Pleroma.Web.OAuth
alias Pleroma.Web.RelMe
alias Pleroma.Workers.BackgroundWorker
field(:local, :boolean, default: true)
field(:follower_address, :string)
field(:following_address, :string)
+ field(:featured_address, :string)
field(:search_rank, :float, virtual: true)
field(:search_type, :integer, virtual: true)
field(:tags, {:array, :string}, default: [])
field(:hide_followers, :boolean, default: false)
field(:hide_follows, :boolean, default: false)
field(:hide_favorites, :boolean, default: true)
- field(:pinned_activities, {:array, :string}, default: [])
field(:email_notifications, :map, default: %{"digest" => false})
field(:mascot, :map, default: nil)
field(:emoji, :map, default: %{})
field(:also_known_as, {:array, ObjectValidators.ObjectID}, default: [])
field(:inbox, :string)
field(:shared_inbox, :string)
- field(:accepts_chat_messages, :boolean, default: nil)
field(:last_active_at, :naive_datetime)
field(:disclose_client, :boolean, default: true)
+ field(:pinned_objects, :map, default: %{})
+ 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)
+
for {relationship_type,
[
{outgoing_relation, outgoing_relation_target},
has_many(incoming_relation_source, through: [incoming_relation, :source])
end
- # `:blocks` is deprecated (replaced with `blocked_users` relation)
- field(:blocks, {:array, :string}, default: [])
- # `:mutes` is deprecated (replaced with `muted_users` relation)
- field(:mutes, {:array, :string}, default: [])
- # `:muted_reblogs` is deprecated (replaced with `reblog_muted_users` relation)
- field(:muted_reblogs, {:array, :string}, default: [])
- # `:muted_notifications` is deprecated (replaced with `notification_muted_users` relation)
- field(:muted_notifications, {:array, :string}, default: [])
- # `:subscribers` is deprecated (replaced with `subscriber_users` relation)
- field(:subscribers, {:array, :string}, default: [])
-
embeds_one(
:multi_factor_authentication_settings,
MFA.Settings,
_ ->
unless options[:no_default] do
- Config.get([:assets, :default_user_avatar], "#{Web.base_url()}/images/avi.png")
+ Config.get([:assets, :default_user_avatar], "#{Endpoint.url()}/images/avi.png")
end
end
end
def banner_url(user, options \\ []) do
case user.banner do
%{"url" => [%{"href" => href} | _]} -> href
- _ -> !options[:no_default] && "#{Web.base_url()}/images/banner.png"
+ _ -> !options[:no_default] && "#{Endpoint.url()}/images/banner.png"
end
end
# Should probably be renamed or removed
- def ap_id(%User{nickname: nickname}), do: "#{Web.base_url()}/users/#{nickname}"
+ @spec ap_id(User.t()) :: String.t()
+ def ap_id(%User{nickname: nickname}), do: "#{Endpoint.url()}/users/#{nickname}"
+ @spec ap_followers(User.t()) :: String.t()
def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
+ @spec ap_featured_collection(User.t()) :: String.t()
+ def ap_featured_collection(%User{featured_address: fa}) when is_binary(fa), do: fa
+
+ def ap_featured_collection(%User{} = user), do: "#{ap_id(user)}/collections/featured"
+
defp truncate_fields_param(params) do
if Map.has_key?(params, :fields) do
Map.put(params, :fields, Enum.map(params[:fields], &truncate_field/1))
:uri,
:follower_address,
:following_address,
+ :featured_address,
:hide_followers,
:hide_follows,
:hide_followers_count,
:invisible,
:actor_type,
:also_known_as,
- :accepts_chat_messages
+ :pinned_objects
]
)
|> cast(params, [:name], empty_values: [])
:pleroma_settings_store,
:is_discoverable,
:actor_type,
- :accepts_chat_messages,
- :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)})
# Used to auto-register LDAP accounts which won't have a password hash stored locally
def register_changeset_ldap(struct, params = %{password: password})
when is_nil(password) do
- params = Map.put_new(params, :accepts_chat_messages, true)
-
params =
if Map.has_key?(params, :email) do
Map.put_new(params, :email, params[:email])
|> cast(params, [
:name,
:nickname,
- :email,
- :accepts_chat_messages
+ :email
])
|> validate_required([:name, :nickname])
|> unique_constraint(:nickname)
|> validate_format(:nickname, local_nickname_regex())
|> put_ap_id()
|> unique_constraint(:ap_id)
- |> put_following_and_follower_address()
+ |> put_following_and_follower_and_featured_address()
+ |> put_private_key()
end
def register_changeset(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)
- params = Map.put_new(params, :accepts_chat_messages, true)
confirmed? =
if is_nil(opts[:confirmed]) do
:password,
:password_confirmation,
:emoji,
- :accepts_chat_messages,
- :registration_reason
+ :registration_reason,
+ :language
])
|> validate_required([:name, :nickname, :password, :password_confirmation])
|> validate_confirmation(:password)
|> put_password_hash
|> put_ap_id()
|> unique_constraint(:ap_id)
- |> put_following_and_follower_address()
+ |> put_following_and_follower_and_featured_address()
+ |> put_private_key()
end
def maybe_validate_required_email(changeset, true), do: changeset
end
end
- defp put_ap_id(changeset) do
+ def put_ap_id(changeset) do
ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
put_change(changeset, :ap_id, ap_id)
end
- defp put_following_and_follower_address(changeset) do
- followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
+ def put_following_and_follower_and_featured_address(changeset) do
+ user = %User{nickname: get_field(changeset, :nickname)}
+ followers = ap_followers(user)
+ following = ap_following(user)
+ featured = ap_featured_collection(user)
changeset
|> put_change(:follower_address, followers)
+ |> put_change(:following_address, following)
+ |> 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
{:ok, user} <- set_cache(user),
{:ok, _} <- maybe_send_registration_email(user),
{:ok, _} <- maybe_send_welcome_email(user),
- {:ok, _} <- maybe_send_welcome_message(user),
- {:ok, _} <- maybe_send_welcome_chat_message(user) do
+ {:ok, _} <- maybe_send_welcome_message(user) do
{:ok, user}
end
end
end
end
- defp maybe_send_welcome_chat_message(user) do
- if User.WelcomeChatMessage.enabled?() do
- User.WelcomeChatMessage.post_message(user)
- {:ok, :enqueued}
- else
- {:ok, :noop}
- end
- end
-
defp maybe_send_welcome_email(%User{email: email} = user) when is_binary(email) do
if User.WelcomeEmail.enabled?() do
User.WelcomeEmail.send_email(user)
|> update_and_set_cache()
end
- def update_and_set_cache(changeset) do
+ def update_and_set_cache(%{data: %Pleroma.User{} = user} = changeset) do
+ was_superuser_before_update = User.superuser?(user)
+
with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
set_cache(user)
end
+ |> maybe_remove_report_notifications(was_superuser_before_update)
+ end
+
+ defp maybe_remove_report_notifications({:ok, %Pleroma.User{} = user} = result, true) do
+ if not User.superuser?(user),
+ do: user |> Notification.destroy_multiple_from_types(["pleroma:report"])
+
+ result
+ end
+
+ defp maybe_remove_report_notifications(result, _) do
+ result
end
def get_user_friends_ap_ids(user) do
blocker
end
- # clear any requested follows as well
+ # clear any requested follows from both sides as well
blocked =
case CommonAPI.reject_follow_request(blocked, blocker) do
{:ok, %User{} = updated_blocked} -> updated_blocked
nil -> blocked
end
+ blocker =
+ case CommonAPI.reject_follow_request(blocker, blocked) do
+ {:ok, %User{} = updated_blocker} -> updated_blocker
+ nil -> blocker
+ end
+
unsubscribe(blocked, blocker)
unfollowing_blocked = Config.get([:activitypub, :unfollow_blocked], true)
def confirm(%User{} = user), do: {:ok, user}
+ def set_suggestion(users, is_suggested) when is_list(users) do
+ Repo.transaction(fn ->
+ Enum.map(users, fn user ->
+ with {:ok, user} <- set_suggestion(user, is_suggested), do: user
+ end)
+ end)
+ end
+
+ def set_suggestion(%User{is_suggested: is_suggested} = user, is_suggested), do: {:ok, user}
+
+ def set_suggestion(%User{} = user, is_suggested) when is_boolean(is_suggested) do
+ user
+ |> change(is_suggested: is_suggested)
+ |> update_and_set_cache()
+ end
+
def update_notification_settings(%User{} = user, settings) do
user
|> cast(%{notification_settings: settings}, [])
email: nil,
name: nil,
password_hash: nil,
- keys: nil,
- public_key: nil,
avatar: %{},
tags: [],
last_refreshed_at: nil,
follower_count: 0,
following_count: 0,
is_locked: false,
- is_confirmed: true,
password_reset_pending: false,
- is_approved: true,
registration_reason: nil,
confirmation_token: nil,
domain_blocks: [],
raw_fields: [],
is_discoverable: false,
also_known_as: []
+ # id: preserved
+ # ap_id: preserved
+ # nickname: preserved
})
end
+ # Purge doesn't delete the user from the database.
+ # It just nulls all its fields and deactivates it.
+ # See `User.purge_user_changeset/1` above.
+ defp purge(%User{} = user) do
+ user
+ |> purge_user_changeset()
+ |> update_and_set_cache()
+ end
+
def delete(users) when is_list(users) do
for user <- users, do: delete(user)
end
def delete(%User{} = user) do
+ # Purge the user immediately
+ purge(user)
BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
end
- defp delete_and_invalidate_cache(%User{} = user) do
+ # *Actually* delete the user from the DB
+ defp delete_from_db(%User{} = user) do
invalidate_cache(user)
Repo.delete(user)
end
- defp delete_or_deactivate(%User{local: false} = user), do: delete_and_invalidate_cache(user)
+ # If the user never finalized their account, it's safe to delete them.
+ defp maybe_delete_from_db(%User{local: true, is_confirmed: false} = user),
+ do: delete_from_db(user)
- defp delete_or_deactivate(%User{local: true} = user) do
- status = account_status(user)
-
- case status do
- :confirmation_pending ->
- delete_and_invalidate_cache(user)
-
- :approval_pending ->
- delete_and_invalidate_cache(user)
+ defp maybe_delete_from_db(%User{local: true, is_approved: false} = user),
+ do: delete_from_db(user)
- _ ->
- user
- |> purge_user_changeset()
- |> update_and_set_cache()
- end
- end
+ defp maybe_delete_from_db(user), do: {:ok, user}
def perform(:force_password_reset, user), do: force_password_reset(user)
@spec perform(atom(), User.t()) :: {:ok, User.t()}
def perform(:delete, %User{} = user) do
+ # Purge the user again, in case perform/2 is called directly
+ purge(user)
+
# Remove all relationships
user
|> get_followers()
delete_user_activities(user)
delete_notifications_from_user_activities(user)
-
delete_outgoing_pending_follow_requests(user)
- delete_or_deactivate(user)
+ maybe_delete_from_db(user)
end
def perform(:set_activation_async, user, status), do: set_activation(user, status)
{%User{} = user, _} ->
{:ok, user}
- _ ->
+ e ->
+ Logger.error("Could not fetch user, #{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
|> Enum.map(&String.downcase/1)
end
- defp local_nickname_regex do
+ def local_nickname_regex do
if Config.get([:instance, :extended_nickname_format]) do
@extended_local_nickname_regex
else
}
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,
def change_email(user, email) do
user
|> cast(%{email: email}, [:email])
- |> validate_required([:email])
+ |> maybe_validate_required_email(false)
|> unique_constraint(:email)
|> validate_format(:email, @email_regex)
|> update_and_set_cache()
end
+ def alias_users(user) do
+ user.also_known_as
+ |> Enum.map(&User.get_cached_by_ap_id/1)
+ |> Enum.filter(fn user -> user != nil end)
+ end
+
+ def add_alias(user, new_alias_user) do
+ current_aliases = user.also_known_as || []
+ new_alias_ap_id = new_alias_user.ap_id
+
+ if new_alias_ap_id in current_aliases do
+ {:ok, user}
+ else
+ user
+ |> cast(%{also_known_as: current_aliases ++ [new_alias_ap_id]}, [:also_known_as])
+ |> update_and_set_cache()
+ end
+ end
+
+ def delete_alias(user, alias_user) do
+ current_aliases = user.also_known_as || []
+ alias_ap_id = alias_user.ap_id
+
+ if alias_ap_id in current_aliases do
+ user
+ |> cast(%{also_known_as: current_aliases -- [alias_ap_id]}, [:also_known_as])
+ |> update_and_set_cache()
+ else
+ {:error, :no_such_alias}
+ end
+ end
+
# Internal function; public one is `deactivate/2`
defp set_activation_status(user, status) do
user
|> update_and_set_cache()
end
- def roles(%{is_moderator: is_moderator, is_admin: is_admin}) do
- %{
- admin: is_admin,
- moderator: is_moderator
- }
- end
-
def validate_fields(changeset, remote? \\ false) do
limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
limit = Config.get([:instance, limit_name], 0)
cast(user, %{is_approved: approved?}, [:is_approved])
end
- def add_pinnned_activity(user, %Pleroma.Activity{id: id}) do
- if id not in user.pinned_activities do
- max_pinned_statuses = Config.get([:instance, :max_pinned_statuses], 0)
- params = %{pinned_activities: user.pinned_activities ++ [id]}
-
- # if pinned activity was scheduled for deletion, we remove job
- if expiration = Pleroma.Workers.PurgeExpiredActivity.get_expiration(id) do
- Oban.cancel_job(expiration.id)
- end
+ @spec add_pinned_object_id(User.t(), String.t()) :: {:ok, User.t()} | {:error, term()}
+ def add_pinned_object_id(%User{} = user, object_id) do
+ if !user.pinned_objects[object_id] do
+ params = %{pinned_objects: Map.put(user.pinned_objects, object_id, NaiveDateTime.utc_now())}
user
- |> cast(params, [:pinned_activities])
- |> validate_length(:pinned_activities,
- max: max_pinned_statuses,
- message: "You have already pinned the maximum number of statuses"
- )
+ |> cast(params, [:pinned_objects])
+ |> validate_change(:pinned_objects, fn :pinned_objects, pinned_objects ->
+ max_pinned_statuses = Config.get([:instance, :max_pinned_statuses], 0)
+
+ if Enum.count(pinned_objects) <= max_pinned_statuses do
+ []
+ else
+ [pinned_objects: "You have already pinned the maximum number of statuses"]
+ end
+ end)
else
change(user)
end
|> update_and_set_cache()
end
- def remove_pinnned_activity(user, %Pleroma.Activity{id: id, data: data}) do
- params = %{pinned_activities: List.delete(user.pinned_activities, id)}
-
- # if pinned activity was scheduled for deletion, we reschedule it for deletion
- if data["expires_at"] do
- # MRF.ActivityExpirationPolicy used UTC timestamps for expires_at in original implementation
- {:ok, expires_at} =
- data["expires_at"] |> Pleroma.EctoType.ActivityPub.ObjectValidators.DateTime.cast()
-
- Pleroma.Workers.PurgeExpiredActivity.enqueue(%{
- activity_id: id,
- expires_at: expires_at
- })
- end
-
+ @spec remove_pinned_object_id(User.t(), String.t()) :: {:ok, t()} | {:error, term()}
+ def remove_pinned_object_id(%User{} = user, object_id) do
user
- |> cast(params, [:pinned_activities])
+ |> cast(
+ %{pinned_objects: Map.delete(user.pinned_objects, object_id)},
+ [:pinned_objects]
+ )
|> update_and_set_cache()
end
|> update_and_set_cache()
end
- def active_user_count(weeks \\ 4) do
- active_after = Timex.shift(NaiveDateTime.utc_now(), weeks: -weeks)
+ def active_user_count(days \\ 30) do
+ active_after = Timex.shift(NaiveDateTime.utc_now(), days: -days)
__MODULE__
|> where([u], u.last_active_at >= ^active_after)
|> where([u], u.local == true)
|> Repo.aggregate(:count)
end
+
+ def update_last_status_at(user) do
+ User
+ |> where(id: ^user.id)
+ |> update([u], set: [last_status_at: fragment("NOW()")])
+ |> select([u], u)
+ |> Repo.update_all([])
+ |> case do
+ {1, [user]} -> set_cache(user)
+ _ -> {:error, user}
+ end
+ end
end