Merge branch '1918-avatar-background-header-reset-2' into 'develop'

Resolve "Better support for resetting avatar/header/background"

Closes #1918

See merge request pleroma/pleroma!2729
This commit is contained in:
Haelwenn 2020-07-08 03:25:30 +00:00
commit a8447c3803
9 changed files with 51 additions and 275 deletions

View file

@ -16,8 +16,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
<details> <details>
<summary>API Changes</summary> <summary>API Changes</summary>
- **Breaking:** Pleroma API: The routes to update avatar, banner and background have been removed.
- **Breaking:** Image description length is limited now. - **Breaking:** Image description length is limited now.
- **Breaking:** Emoji API: changed methods and renamed routes. - **Breaking:** Emoji API: changed methods and renamed routes.
- MastodonAPI: Allow removal of avatar, banner and background.
- Streaming: Repeats of a user's posts will no longer be pushed to the user's stream. - Streaming: Repeats of a user's posts will no longer be pushed to the user's stream.
- Mastodon API: Added `pleroma.metadata.fields_limits` to /api/v1/instance - Mastodon API: Added `pleroma.metadata.fields_limits` to /api/v1/instance
- Mastodon API: On deletion, returns the original post text. - Mastodon API: On deletion, returns the original post text.

View file

@ -182,10 +182,12 @@ Additional parameters can be added to the JSON body/Form data:
- `pleroma_settings_store` - Opaque user settings to be saved on the backend. - `pleroma_settings_store` - Opaque user settings to be saved on the backend.
- `skip_thread_containment` - if true, skip filtering out broken threads - `skip_thread_containment` - if true, skip filtering out broken threads
- `allow_following_move` - if true, allows automatically follow moved following accounts - `allow_following_move` - if true, allows automatically follow moved following accounts
- `pleroma_background_image` - sets the background image of the user. - `pleroma_background_image` - sets the background image of the user. Can be set to "" (an empty string) to reset.
- `discoverable` - if true, discovery of this account in search results and other services is allowed. - `discoverable` - if true, discovery of this account in search results and other services is allowed.
- `actor_type` - the type of this account. - `actor_type` - the type of this account.
All images (avatar, banner and background) can be reset to the default by sending an empty string ("") instead of a file.
### Pleroma Settings Store ### Pleroma Settings Store
Pleroma has mechanism that allows frontends to save blobs of json for each user on the backend. This can be used to save frontend-specific settings for a user that the backend does not need to know about. Pleroma has mechanism that allows frontends to save blobs of json for each user on the backend. This can be used to save frontend-specific settings for a user that the backend does not need to know about.

View file

@ -89,7 +89,7 @@ defmodule Pleroma.User do
field(:keys, :string) field(:keys, :string)
field(:public_key, :string) field(:public_key, :string)
field(:ap_id, :string) field(:ap_id, :string)
field(:avatar, :map) field(:avatar, :map, default: %{})
field(:local, :boolean, default: true) field(:local, :boolean, default: true)
field(:follower_address, :string) field(:follower_address, :string)
field(:following_address, :string) field(:following_address, :string)
@ -539,14 +539,11 @@ defp put_emoji(changeset) do
end end
defp put_change_if_present(changeset, map_field, value_function) do defp put_change_if_present(changeset, map_field, value_function) do
if value = get_change(changeset, map_field) do with {:ok, value} <- fetch_change(changeset, map_field),
with {:ok, new_value} <- value_function.(value) do {:ok, new_value} <- value_function.(value) do
put_change(changeset, map_field, new_value) put_change(changeset, map_field, new_value)
else
_ -> changeset
end
else else
changeset _ -> changeset
end end
end end

View file

@ -4,7 +4,6 @@
defmodule Pleroma.Web.ApiSpec.PleromaAccountOperation do defmodule Pleroma.Web.ApiSpec.PleromaAccountOperation do
alias OpenApiSpex.Operation alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.AccountRelationship alias Pleroma.Web.ApiSpec.Schemas.AccountRelationship
alias Pleroma.Web.ApiSpec.Schemas.ApiError alias Pleroma.Web.ApiSpec.Schemas.ApiError
alias Pleroma.Web.ApiSpec.Schemas.FlakeID alias Pleroma.Web.ApiSpec.Schemas.FlakeID
@ -40,48 +39,6 @@ def confirmation_resend_operation do
} }
end end
def update_avatar_operation do
%Operation{
tags: ["Accounts"],
summary: "Set/clear user avatar image",
operationId: "PleromaAPI.AccountController.update_avatar",
requestBody:
request_body("Parameters", update_avatar_or_background_request(), required: true),
security: [%{"oAuth" => ["write:accounts"]}],
responses: %{
200 => update_response(),
403 => Operation.response("Forbidden", "application/json", ApiError)
}
}
end
def update_banner_operation do
%Operation{
tags: ["Accounts"],
summary: "Set/clear user banner image",
operationId: "PleromaAPI.AccountController.update_banner",
requestBody: request_body("Parameters", update_banner_request(), required: true),
security: [%{"oAuth" => ["write:accounts"]}],
responses: %{
200 => update_response()
}
}
end
def update_background_operation do
%Operation{
tags: ["Accounts"],
summary: "Set/clear user background image",
operationId: "PleromaAPI.AccountController.update_background",
security: [%{"oAuth" => ["write:accounts"]}],
requestBody:
request_body("Parameters", update_avatar_or_background_request(), required: true),
responses: %{
200 => update_response()
}
}
end
def favourites_operation do def favourites_operation do
%Operation{ %Operation{
tags: ["Accounts"], tags: ["Accounts"],
@ -136,52 +93,4 @@ defp id_param do
required: true required: true
) )
end end
defp update_avatar_or_background_request do
%Schema{
title: "PleromaAccountUpdateAvatarOrBackgroundRequest",
type: :object,
properties: %{
img: %Schema{
nullable: true,
type: :string,
format: :binary,
description: "Image encoded using `multipart/form-data` or an empty string to clear"
}
}
}
end
defp update_banner_request do
%Schema{
title: "PleromaAccountUpdateBannerRequest",
type: :object,
properties: %{
banner: %Schema{
type: :string,
nullable: true,
format: :binary,
description: "Image encoded using `multipart/form-data` or an empty string to clear"
}
}
}
end
defp update_response do
Operation.response("PleromaAccountUpdateResponse", "application/json", %Schema{
type: :object,
properties: %{
url: %Schema{
type: :string,
format: :uri,
nullable: true,
description: "Image URL"
}
},
example: %{
"url" =>
"https://cofe.party/media/9d0add56-bcb6-4c0f-8225-cbbd0b6dd773/13eadb6972c9ccd3f4ffa3b8196f0e0d38b4d2f27594457c52e52946c054cd9a.gif"
}
})
end
end end

View file

@ -148,6 +148,13 @@ def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _p
|> Enum.filter(fn {_, value} -> not is_nil(value) end) |> Enum.filter(fn {_, value} -> not is_nil(value) end)
|> Enum.into(%{}) |> Enum.into(%{})
# We use an empty string as a special value to reset
# avatars, banners, backgrounds
user_image_value = fn
"" -> {:ok, nil}
value -> {:ok, value}
end
user_params = user_params =
[ [
:no_rich_text, :no_rich_text,
@ -168,9 +175,9 @@ def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _p
|> Maps.put_if_present(:name, params[:display_name]) |> Maps.put_if_present(:name, params[:display_name])
|> Maps.put_if_present(:bio, params[:note]) |> Maps.put_if_present(:bio, params[:note])
|> Maps.put_if_present(:raw_bio, params[:note]) |> Maps.put_if_present(:raw_bio, params[:note])
|> Maps.put_if_present(:avatar, params[:avatar]) |> Maps.put_if_present(:avatar, params[:avatar], user_image_value)
|> Maps.put_if_present(:banner, params[:header]) |> Maps.put_if_present(:banner, params[:header], user_image_value)
|> Maps.put_if_present(:background, params[:pleroma_background_image]) |> Maps.put_if_present(:background, params[:pleroma_background_image], user_image_value)
|> Maps.put_if_present( |> Maps.put_if_present(
:raw_fields, :raw_fields,
params[:fields_attributes], params[:fields_attributes],

View file

@ -8,7 +8,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
import Pleroma.Web.ControllerHelper, import Pleroma.Web.ControllerHelper,
only: [json_response: 3, add_link_headers: 2, assign_account_by_id: 2] only: [json_response: 3, add_link_headers: 2, assign_account_by_id: 2]
alias Ecto.Changeset
alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Plugs.RateLimiter alias Pleroma.Plugs.RateLimiter
@ -35,17 +34,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
%{scopes: ["follow", "write:follows"]} when action in [:subscribe, :unsubscribe] %{scopes: ["follow", "write:follows"]} when action in [:subscribe, :unsubscribe]
) )
plug(
OAuthScopesPlug,
%{scopes: ["write:accounts"]}
# Note: the following actions are not permission-secured in Mastodon:
when action in [
:update_avatar,
:update_banner,
:update_background
]
)
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["read:favourites"], fallback: :proceed_unauthenticated} when action == :favourites %{scopes: ["read:favourites"], fallback: :proceed_unauthenticated} when action == :favourites
@ -68,56 +56,6 @@ def confirmation_resend(conn, params) do
end end
end end
@doc "PATCH /api/v1/pleroma/accounts/update_avatar"
def update_avatar(%{assigns: %{user: user}, body_params: %{img: ""}} = conn, _) do
{:ok, _user} =
user
|> Changeset.change(%{avatar: nil})
|> User.update_and_set_cache()
json(conn, %{url: nil})
end
def update_avatar(%{assigns: %{user: user}, body_params: params} = conn, _params) do
{:ok, %{data: data}} = ActivityPub.upload(params, type: :avatar)
{:ok, _user} = user |> Changeset.change(%{avatar: data}) |> User.update_and_set_cache()
%{"url" => [%{"href" => href} | _]} = data
json(conn, %{url: href})
end
@doc "PATCH /api/v1/pleroma/accounts/update_banner"
def update_banner(%{assigns: %{user: user}, body_params: %{banner: ""}} = conn, _) do
with {:ok, _user} <- User.update_banner(user, %{}) do
json(conn, %{url: nil})
end
end
def update_banner(%{assigns: %{user: user}, body_params: params} = conn, _) do
with {:ok, object} <- ActivityPub.upload(%{img: params[:banner]}, type: :banner),
{:ok, _user} <- User.update_banner(user, object.data) do
%{"url" => [%{"href" => href} | _]} = object.data
json(conn, %{url: href})
end
end
@doc "PATCH /api/v1/pleroma/accounts/update_background"
def update_background(%{assigns: %{user: user}, body_params: %{img: ""}} = conn, _) do
with {:ok, _user} <- User.update_background(user, %{}) do
json(conn, %{url: nil})
end
end
def update_background(%{assigns: %{user: user}, body_params: params} = conn, _) do
with {:ok, object} <- ActivityPub.upload(params, type: :background),
{:ok, _user} <- User.update_background(user, object.data) do
%{"url" => [%{"href" => href} | _]} = object.data
json(conn, %{url: href})
end
end
@doc "GET /api/v1/pleroma/accounts/:id/favourites" @doc "GET /api/v1/pleroma/accounts/:id/favourites"
def favourites(%{assigns: %{account: %{hide_favorites: true}}} = conn, _params) do def favourites(%{assigns: %{account: %{hide_favorites: true}}} = conn, _params) do
render_error(conn, :forbidden, "Can't get favorites") render_error(conn, :forbidden, "Can't get favorites")

View file

@ -328,10 +328,6 @@ defmodule Pleroma.Web.Router do
delete("/statuses/:id/reactions/:emoji", EmojiReactionController, :delete) delete("/statuses/:id/reactions/:emoji", EmojiReactionController, :delete)
post("/notifications/read", NotificationController, :mark_as_read) post("/notifications/read", NotificationController, :mark_as_read)
patch("/accounts/update_avatar", AccountController, :update_avatar)
patch("/accounts/update_banner", AccountController, :update_banner)
patch("/accounts/update_background", AccountController, :update_background)
get("/mascot", MascotController, :show) get("/mascot", MascotController, :show)
put("/mascot", MascotController, :update) put("/mascot", MascotController, :update)

View file

@ -216,10 +216,21 @@ test "updates the user's avatar", %{user: user, conn: conn} do
filename: "an_image.jpg" filename: "an_image.jpg"
} }
conn = patch(conn, "/api/v1/accounts/update_credentials", %{"avatar" => new_avatar}) assert user.avatar == %{}
assert user_response = json_response_and_validate_schema(conn, 200) res = patch(conn, "/api/v1/accounts/update_credentials", %{"avatar" => new_avatar})
assert user_response = json_response_and_validate_schema(res, 200)
assert user_response["avatar"] != User.avatar_url(user) assert user_response["avatar"] != User.avatar_url(user)
user = User.get_by_id(user.id)
refute user.avatar == %{}
# Also resets it
_res = patch(conn, "/api/v1/accounts/update_credentials", %{"avatar" => ""})
user = User.get_by_id(user.id)
assert user.avatar == nil
end end
test "updates the user's banner", %{user: user, conn: conn} do test "updates the user's banner", %{user: user, conn: conn} do
@ -229,26 +240,39 @@ test "updates the user's banner", %{user: user, conn: conn} do
filename: "an_image.jpg" filename: "an_image.jpg"
} }
conn = patch(conn, "/api/v1/accounts/update_credentials", %{"header" => new_header}) res = patch(conn, "/api/v1/accounts/update_credentials", %{"header" => new_header})
assert user_response = json_response_and_validate_schema(conn, 200) assert user_response = json_response_and_validate_schema(res, 200)
assert user_response["header"] != User.banner_url(user) assert user_response["header"] != User.banner_url(user)
# Also resets it
_res = patch(conn, "/api/v1/accounts/update_credentials", %{"header" => ""})
user = User.get_by_id(user.id)
assert user.banner == nil
end end
test "updates the user's background", %{conn: conn} do test "updates the user's background", %{conn: conn, user: user} do
new_header = %Plug.Upload{ new_header = %Plug.Upload{
content_type: "image/jpg", content_type: "image/jpg",
path: Path.absname("test/fixtures/image.jpg"), path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg" filename: "an_image.jpg"
} }
conn = res =
patch(conn, "/api/v1/accounts/update_credentials", %{ patch(conn, "/api/v1/accounts/update_credentials", %{
"pleroma_background_image" => new_header "pleroma_background_image" => new_header
}) })
assert user_response = json_response_and_validate_schema(conn, 200) assert user_response = json_response_and_validate_schema(res, 200)
assert user_response["pleroma"]["background_image"] assert user_response["pleroma"]["background_image"]
#
# Also resets it
_res =
patch(conn, "/api/v1/accounts/update_credentials", %{"pleroma_background_image" => ""})
user = User.get_by_id(user.id)
assert user.background == nil
end end
test "requires 'write:accounts' permission" do test "requires 'write:accounts' permission" do

View file

@ -13,8 +13,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do
import Pleroma.Factory import Pleroma.Factory
import Swoosh.TestAssertions import Swoosh.TestAssertions
@image "data:image/gif;base64,R0lGODlhEAAQAMQAAORHHOVSKudfOulrSOp3WOyDZu6QdvCchPGolfO0o/XBs/fNwfjZ0frl3/zy7////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKUE1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7"
describe "POST /api/v1/pleroma/accounts/confirmation_resend" do describe "POST /api/v1/pleroma/accounts/confirmation_resend" do
setup do setup do
{:ok, user} = {:ok, user} =
@ -68,103 +66,6 @@ test "resend account confirmation email (with nickname)", %{conn: conn, user: us
end end
end end
describe "PATCH /api/v1/pleroma/accounts/update_avatar" do
setup do: oauth_access(["write:accounts"])
test "user avatar can be set", %{user: user, conn: conn} do
avatar_image = File.read!("test/fixtures/avatar_data_uri")
conn =
conn
|> put_req_header("content-type", "multipart/form-data")
|> patch("/api/v1/pleroma/accounts/update_avatar", %{img: avatar_image})
user = refresh_record(user)
assert %{
"name" => _,
"type" => _,
"url" => [
%{
"href" => _,
"mediaType" => _,
"type" => _
}
]
} = user.avatar
assert %{"url" => _} = json_response_and_validate_schema(conn, 200)
end
test "user avatar can be reset", %{user: user, conn: conn} do
conn =
conn
|> put_req_header("content-type", "multipart/form-data")
|> patch("/api/v1/pleroma/accounts/update_avatar", %{img: ""})
user = User.get_cached_by_id(user.id)
assert user.avatar == nil
assert %{"url" => nil} = json_response_and_validate_schema(conn, 200)
end
end
describe "PATCH /api/v1/pleroma/accounts/update_banner" do
setup do: oauth_access(["write:accounts"])
test "can set profile banner", %{user: user, conn: conn} do
conn =
conn
|> put_req_header("content-type", "multipart/form-data")
|> patch("/api/v1/pleroma/accounts/update_banner", %{"banner" => @image})
user = refresh_record(user)
assert user.banner["type"] == "Image"
assert %{"url" => _} = json_response_and_validate_schema(conn, 200)
end
test "can reset profile banner", %{user: user, conn: conn} do
conn =
conn
|> put_req_header("content-type", "multipart/form-data")
|> patch("/api/v1/pleroma/accounts/update_banner", %{"banner" => ""})
user = refresh_record(user)
assert user.banner == %{}
assert %{"url" => nil} = json_response_and_validate_schema(conn, 200)
end
end
describe "PATCH /api/v1/pleroma/accounts/update_background" do
setup do: oauth_access(["write:accounts"])
test "background image can be set", %{user: user, conn: conn} do
conn =
conn
|> put_req_header("content-type", "multipart/form-data")
|> patch("/api/v1/pleroma/accounts/update_background", %{"img" => @image})
user = refresh_record(user)
assert user.background["type"] == "Image"
# assert %{"url" => _} = json_response(conn, 200)
assert %{"url" => _} = json_response_and_validate_schema(conn, 200)
end
test "background image can be reset", %{user: user, conn: conn} do
conn =
conn
|> put_req_header("content-type", "multipart/form-data")
|> patch("/api/v1/pleroma/accounts/update_background", %{"img" => ""})
user = refresh_record(user)
assert user.background == %{}
assert %{"url" => nil} = json_response_and_validate_schema(conn, 200)
end
end
describe "getting favorites timeline of specified user" do describe "getting favorites timeline of specified user" do
setup do setup do
[current_user, user] = insert_pair(:user, hide_favorites: false) [current_user, user] = insert_pair(:user, hide_favorites: false)