commit: c1a1150888f48448d353897327d4d3f050bec9b4
parent 0111659a1f8e637b046c905551d7d5251d6c2efa
Author: marcin mikołajczak <me@mkljczk.pl>
Date: Sat, 14 Sep 2024 17:46:33 +0000
Merge branch 'profile-image-descriptions' into 'develop'
Allow providing avatar/header descriptions
See merge request pleroma/pleroma!4227
Diffstat:
9 files changed, 156 insertions(+), 9 deletions(-)
diff --git a/changelog.d/profile-image-descriptions.add b/changelog.d/profile-image-descriptions.add
@@ -0,0 +1 @@
+Allow providing avatar/header descriptions
+\ No newline at end of file
diff --git a/docs/development/API/differences_in_mastoapi_responses.md b/docs/development/API/differences_in_mastoapi_responses.md
@@ -104,7 +104,7 @@ Has these additional fields under the `pleroma` object:
- `background_image`: nullable URL string, background image of the user
- `tags`: Lists an array of tags for the user
- `relationship` (object): Includes fields as documented for Mastodon API https://docs.joinmastodon.org/entities/relationship/
-- `is_moderator`: boolean, nullable, true if user is a moderator
+- `is_moderator`: boolean, nullable, true if user is a moderator
- `is_admin`: boolean, nullable, true if user is an admin
- `confirmation_pending`: boolean, true if a new user account is waiting on email confirmation to be activated
- `hide_favorites`: boolean, true when the user has hiding favorites enabled
@@ -121,6 +121,8 @@ Has these additional fields under the `pleroma` object:
- `notification_settings`: object, can be absent. See `/api/v1/pleroma/notification_settings` for the parameters/keys returned.
- `accepts_chat_messages`: boolean, but can be null if we don't have that information about a user
- `favicon`: nullable URL string, Favicon image of the user's instance
+- `avatar_description`: string, image description for user avatar, defaults to empty string
+- `header_description`: string, image description for user banner, defaults to empty string
### Source
@@ -256,6 +258,8 @@ Additional parameters can be added to the JSON body/Form data:
- `actor_type` - the type of this account.
- `accepts_chat_messages` - if false, this account will reject all chat messages.
- `language` - user's preferred language for receiving emails (digest, confirmation, etc.)
+- `avatar_description` - image description for user avatar
+- `header_description` - image description for user banner
All images (avatar, banner and background) can be reset to the default by sending an empty string ("") instead of a file.
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
@@ -586,16 +586,26 @@ defmodule Pleroma.User do
|> validate_length(:bio, max: bio_limit)
|> validate_length(:name, min: 1, max: name_limit)
|> validate_inclusion(:actor_type, Pleroma.Constants.allowed_user_actor_types())
+ |> validate_image_description(:avatar_description, params)
+ |> validate_image_description(:header_description, params)
|> put_fields()
|> put_emoji()
|> put_change_if_present(:bio, &{:ok, parse_bio(&1, struct)})
- |> put_change_if_present(:avatar, &put_upload(&1, :avatar))
- |> put_change_if_present(:banner, &put_upload(&1, :banner))
+ |> put_change_if_present(
+ :avatar,
+ &put_upload(&1, :avatar, Map.get(params, :avatar_description))
+ )
+ |> put_change_if_present(
+ :banner,
+ &put_upload(&1, :banner, Map.get(params, :header_description))
+ )
|> put_change_if_present(:background, &put_upload(&1, :background))
|> put_change_if_present(
:pleroma_settings_store,
&{:ok, Map.merge(struct.pleroma_settings_store, &1)}
)
+ |> maybe_update_image_description(:avatar, Map.get(params, :avatar_description))
+ |> maybe_update_image_description(:banner, Map.get(params, :header_description))
|> validate_fields(false)
end
@@ -674,13 +684,41 @@ defmodule Pleroma.User do
end
end
- defp put_upload(value, type) do
+ defp put_upload(value, type, description \\ nil) do
with %Plug.Upload{} <- value,
- {:ok, object} <- ActivityPub.upload(value, type: type) do
+ {:ok, object} <- ActivityPub.upload(value, type: type, description: description) do
{:ok, object.data}
end
end
+ defp validate_image_description(changeset, key, params) do
+ description_limit = Config.get([:instance, :description_limit], 5_000)
+ description = Map.get(params, key)
+
+ if is_binary(description) and String.length(description) > description_limit do
+ changeset
+ |> add_error(key, "#{key} is too long")
+ else
+ changeset
+ end
+ end
+
+ defp maybe_update_image_description(changeset, image_field, description)
+ when is_binary(description) do
+ with {:image_missing, true} <- {:image_missing, not changed?(changeset, image_field)},
+ {:existing_image, %{"id" => id}} <-
+ {:existing_image, Map.get(changeset.data, image_field)},
+ {:object, %Object{} = object} <- {:object, Object.get_by_ap_id(id)},
+ {:ok, object} <- Object.update_data(object, %{"name" => description}) do
+ put_change(changeset, image_field, object.data)
+ else
+ {:description_too_long, true} -> {:error}
+ _ -> changeset
+ end
+ end
+
+ defp maybe_update_image_description(changeset, _, _), do: changeset
+
def update_as_admin_changeset(struct, params) do
struct
|> update_changeset(params)
diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex
@@ -813,6 +813,16 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
allOf: [BooleanLike],
nullable: true,
description: "User's birthday will be visible"
+ },
+ avatar_description: %Schema{
+ type: :string,
+ nullable: true,
+ description: "Avatar image description."
+ },
+ header_description: %Schema{
+ type: :string,
+ nullable: true,
+ description: "Header image description."
}
},
example: %{
diff --git a/lib/pleroma/web/api_spec/schemas/account.ex b/lib/pleroma/web/api_spec/schemas/account.ex
@@ -111,7 +111,9 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
format: :uri,
nullable: true,
description: "Favicon image of the user's instance"
- }
+ },
+ avatar_description: %Schema{type: :string},
+ header_description: %Schema{type: :string}
}
},
source: %Schema{
@@ -152,6 +154,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
example: %{
"acct" => "foobar",
"avatar" => "https://mypleroma.com/images/avi.png",
+ "avatar_description" => "",
"avatar_static" => "https://mypleroma.com/images/avi.png",
"bot" => false,
"created_at" => "2020-03-24T13:05:58.000Z",
@@ -162,6 +165,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
"followers_count" => 0,
"following_count" => 1,
"header" => "https://mypleroma.com/images/banner.png",
+ "header_description" => "",
"header_static" => "https://mypleroma.com/images/banner.png",
"id" => "9tKi3esbG7OQgZ2920",
"locked" => false,
diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
@@ -232,6 +232,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|> Maps.put_if_present(:is_discoverable, params[:discoverable])
|> Maps.put_if_present(:birthday, params[:birthday])
|> Maps.put_if_present(:language, Pleroma.Web.Gettext.normalize_locale(params[:language]))
+ |> Maps.put_if_present(:avatar_description, params[:avatar_description])
+ |> Maps.put_if_present(:header_description, params[:header_description])
# What happens here:
#
@@ -277,6 +279,12 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
{:error, %Ecto.Changeset{errors: [{:name, {_, _}} | _]}} ->
render_error(conn, :request_entity_too_large, "Name is too long")
+ {:error, %Ecto.Changeset{errors: [{:avatar_description, {_, _}} | _]}} ->
+ render_error(conn, :request_entity_too_large, "Avatar description is too long")
+
+ {:error, %Ecto.Changeset{errors: [{:header_description, {_, _}} | _]}} ->
+ render_error(conn, :request_entity_too_large, "Banner description is too long")
+
{:error, %Ecto.Changeset{errors: [{:fields, {"invalid", _}} | _]}} ->
render_error(conn, :request_entity_too_large, "One or more field entries are too long")
diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex
@@ -219,8 +219,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
avatar = User.avatar_url(user) |> MediaProxy.url()
avatar_static = User.avatar_url(user) |> MediaProxy.preview_url(static: true)
+ avatar_description = image_description(user.avatar)
header = User.banner_url(user) |> MediaProxy.url()
header_static = User.banner_url(user) |> MediaProxy.preview_url(static: true)
+ header_description = image_description(user.banner)
following_count =
if !user.hide_follows_count or !user.hide_follows or self,
@@ -321,7 +323,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
skip_thread_containment: user.skip_thread_containment,
background_image: image_url(user.background) |> MediaProxy.url(),
accepts_chat_messages: user.accepts_chat_messages,
- favicon: favicon
+ favicon: favicon,
+ avatar_description: avatar_description,
+ header_description: header_description
}
}
|> maybe_put_role(user, opts[:for])
@@ -345,6 +349,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
defp username_from_nickname(_), do: nil
+ defp image_description(%{"name" => name}), do: name
+
+ defp image_description(_), do: ""
+
defp maybe_put_follow_requests_count(
data,
%User{id: user_id} = user,
diff --git a/test/pleroma/web/mastodon_api/update_credentials_test.exs b/test/pleroma/web/mastodon_api/update_credentials_test.exs
@@ -430,6 +430,75 @@ defmodule Pleroma.Web.MastodonAPI.UpdateCredentialsTest do
assert :ok == File.rm(Path.absname("test/tmp/large_binary.data"))
end
+ test "adds avatar description with a new avatar", %{user: user, conn: conn} do
+ new_avatar = %Plug.Upload{
+ content_type: "image/jpeg",
+ path: Path.absname("test/fixtures/image.jpg"),
+ filename: "an_image.jpg"
+ }
+
+ res =
+ patch(conn, "/api/v1/accounts/update_credentials", %{
+ "avatar" => new_avatar,
+ "avatar_description" => "me and pleroma tan"
+ })
+
+ assert json_response_and_validate_schema(res, 200)
+
+ user = User.get_by_id(user.id)
+ assert user.avatar["name"] == "me and pleroma tan"
+ end
+
+ test "adds avatar description to existing avatar", %{user: user, conn: conn} do
+ new_avatar = %Plug.Upload{
+ content_type: "image/jpeg",
+ path: Path.absname("test/fixtures/image.jpg"),
+ filename: "an_image.jpg"
+ }
+
+ assert user.avatar == %{}
+
+ conn
+ |> patch("/api/v1/accounts/update_credentials", %{"avatar" => new_avatar})
+
+ assert conn
+ |> assign(:user, User.get_by_id(user.id))
+ |> patch("/api/v1/accounts/update_credentials", %{
+ "avatar_description" => "me and pleroma tan"
+ })
+ |> json_response_and_validate_schema(200)
+
+ user = User.get_by_id(user.id)
+ assert user.avatar["name"] == "me and pleroma tan"
+ end
+
+ test "limit", %{user: user, conn: conn} do
+ new_header = %Plug.Upload{
+ content_type: "image/jpeg",
+ path: Path.absname("test/fixtures/image.jpg"),
+ filename: "an_image.jpg"
+ }
+
+ assert user.banner == %{}
+
+ conn
+ |> patch("/api/v1/accounts/update_credentials", %{"header" => new_header})
+
+ description_limit = Config.get([:instance, :description_limit], 100)
+
+ description = String.duplicate(".", description_limit + 1)
+
+ conn =
+ conn
+ |> assign(:user, User.get_by_id(user.id))
+ |> patch("/api/v1/accounts/update_credentials", %{
+ "header_description" => description
+ })
+
+ assert %{"error" => "Banner description is too long"} =
+ json_response_and_validate_schema(conn, 413)
+ end
+
test "Strip / from upload files", %{user: user, conn: conn} do
new_image = %Plug.Upload{
content_type: "image/jpeg",
diff --git a/test/pleroma/web/mastodon_api/views/account_view_test.exs b/test/pleroma/web/mastodon_api/views/account_view_test.exs
@@ -96,7 +96,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
hide_follows_count: false,
relationship: %{},
skip_thread_containment: false,
- accepts_chat_messages: nil
+ accepts_chat_messages: nil,
+ avatar_description: "",
+ header_description: ""
}
}
@@ -340,7 +342,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
hide_follows_count: false,
relationship: %{},
skip_thread_containment: false,
- accepts_chat_messages: nil
+ accepts_chat_messages: nil,
+ avatar_description: "",
+ header_description: ""
}
}