Skip to content

Commit

Permalink
[feature] PATCH /api/v1/admin/custom_emojis/{id} endpoint (#1061)
Browse files Browse the repository at this point in the history
* start adding admin emoji PATCH stuff

* updating works OK, now how about copying

* allow emojis to be copied

* update swagger docs

* update admin processer to use non-interface storage driver

* remove shortcode updating for local emojis

* go fmt

Co-authored-by: f0x52 <f0x@cthu.lu>
  • Loading branch information
tsmethurst and f0x52 committed Nov 24, 2022
1 parent 3e82196 commit b6dbe21
Show file tree
Hide file tree
Showing 10 changed files with 1,139 additions and 4 deletions.
92 changes: 91 additions & 1 deletion docs/api/swagger.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
basePath: /
definitions:
EmojiUpdateType:
title: EmojiUpdateType models an admin update action to take on a custom emoji.
type: string
x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
InstanceConfigurationAccounts:
properties:
allow_custom_css:
Expand Down Expand Up @@ -256,7 +260,6 @@ definitions:
description: |-
Role of the account on this instance.
Omitted for remote accounts.
type: accountRole
example: user
type: string
x-go-name: Role
Expand Down Expand Up @@ -898,6 +901,25 @@ definitions:
type: object
x-go-name: EmojiCreateRequest
x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
emojiUpdateRequest:
properties:
CategoryName:
description: Category in which to place the emoji.
type: string
Image:
description: |-
Image file to use for the emoji.
Must be png or gif and no larger than 50kb.
Shortcode:
description: Desired shortcode for the emoji, without surrounding colons. This must be unique for the domain.
example: blobcat_uwu
type: string
type:
$ref: '#/definitions/EmojiUpdateType'
title: EmojiUpdateRequest represents a request to update a custom emoji, made through the admin API.
type: object
x-go-name: EmojiUpdateRequest
x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
field:
properties:
name:
Expand Down Expand Up @@ -2883,6 +2905,74 @@ paths:
summary: Get the admin view of a single emoji.
tags:
- admin
patch:
consumes:
- multipart/form-data
description: |-
Action performed depends upon the action `type` provided.
`disable`: disable a REMOTE emoji from being used/displayed on this instance. Does not work for local emojis.
`copy`: copy a REMOTE emoji to this instance. When doing this action, a shortcode MUST be provided, and it must
be unique among emojis already present on this instance. A category MAY be provided, and the copied emoji will then
be put into the provided category.
`modify`: modify a LOCAL emoji. You can provide a new image for the emoji and/or update the category.
Local emojis cannot be deleted using this endpoint. To delete a local emoji, check DELETE /api/v1/admin/custom_emojis/{id} instead.
operationId: emojiUpdate
parameters:
- description: The id of the emoji.
in: path
name: id
required: true
type: string
- description: |-
Type of action to be taken. One of: (`disable`, `copy`, `modify`).
For REMOTE emojis, `copy` or `disable` are supported.
For LOCAL emojis, only `modify` is supported.
in: formData
name: type
required: true
type: string
- description: The code to use for the emoji, which will be used by instance denizens to select it. This must be unique on the instance. Works for the `copy` action type only.
in: formData
name: shortcode
pattern: \w{2,30}
type: string
- description: A new png or gif image to use for the emoji. Animated pngs work too! To ensure compatibility with other fedi implementations, emoji size limit is 50kb by default. Works for LOCAL emojis only.
in: formData
name: image
type: file
- description: Category in which to place the emoji. 64 characters or less. If a category with the given name doesn't exist yet, it will be created.
in: formData
name: category
type: string
produces:
- application/json
responses:
"200":
description: The updated emoji.
schema:
$ref: '#/definitions/adminEmoji'
"400":
description: bad request
"401":
description: unauthorized
"403":
description: forbidden
"404":
description: not found
"406":
description: not acceptable
"500":
description: internal server error
security:
- OAuth2 Bearer:
- admin
summary: Perform admin action on a local or remote emoji known to this instance.
tags:
- admin
/api/v1/admin/custom_emojis/categories:
get:
operationId: emojiCategoriesGet
Expand Down
1 change: 1 addition & 0 deletions internal/api/client/admin/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ func (m *Module) Route(r router.Router) error {
r.AttachHandler(http.MethodGet, EmojiPath, m.EmojisGETHandler)
r.AttachHandler(http.MethodDelete, EmojiPathWithID, m.EmojiDELETEHandler)
r.AttachHandler(http.MethodGet, EmojiPathWithID, m.EmojiGETHandler)
r.AttachHandler(http.MethodPatch, EmojiPathWithID, m.EmojiPATCHHandler)
r.AttachHandler(http.MethodPost, DomainBlocksPath, m.DomainBlocksPOSTHandler)
r.AttachHandler(http.MethodGet, DomainBlocksPath, m.DomainBlocksGETHandler)
r.AttachHandler(http.MethodGet, DomainBlocksPathWithID, m.DomainBlockGETHandler)
Expand Down
221 changes: 221 additions & 0 deletions internal/api/client/admin/emojiupdate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
/*
GoToSocial
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package admin

import (
"errors"
"fmt"
"net/http"
"strings"

"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api"
"github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/validate"
)

// EmojiPATCHHandler swagger:operation PATCH /api/v1/admin/custom_emojis/{id} emojiUpdate
//
// Perform admin action on a local or remote emoji known to this instance.
//
// Action performed depends upon the action `type` provided.
//
// `disable`: disable a REMOTE emoji from being used/displayed on this instance. Does not work for local emojis.
//
// `copy`: copy a REMOTE emoji to this instance. When doing this action, a shortcode MUST be provided, and it must
// be unique among emojis already present on this instance. A category MAY be provided, and the copied emoji will then
// be put into the provided category.
//
// `modify`: modify a LOCAL emoji. You can provide a new image for the emoji and/or update the category.
//
// Local emojis cannot be deleted using this endpoint. To delete a local emoji, check DELETE /api/v1/admin/custom_emojis/{id} instead.
//
// ---
// tags:
// - admin
//
// consumes:
// - multipart/form-data
//
// produces:
// - application/json
//
// parameters:
// -
// name: id
// type: string
// description: The id of the emoji.
// in: path
// required: true
// -
// name: type
// in: formData
// description: |-
// Type of action to be taken. One of: (`disable`, `copy`, `modify`).
// For REMOTE emojis, `copy` or `disable` are supported.
// For LOCAL emojis, only `modify` is supported.
// type: string
// required: true
// -
// name: shortcode
// in: formData
// description: >-
// The code to use for the emoji, which will be used by instance denizens to select it.
// This must be unique on the instance. Works for the `copy` action type only.
// type: string
// pattern: \w{2,30}
// -
// name: image
// in: formData
// description: >-
// A new png or gif image to use for the emoji. Animated pngs work too!
// To ensure compatibility with other fedi implementations, emoji size limit is 50kb by default.
// Works for LOCAL emojis only.
// type: file
// -
// name: category
// in: formData
// description: >-
// Category in which to place the emoji. 64 characters or less.
// If a category with the given name doesn't exist yet, it will be created.
// type: string
//
// security:
// - OAuth2 Bearer:
// - admin
//
// responses:
// '200':
// description: The updated emoji.
// schema:
// "$ref": "#/definitions/adminEmoji"
// '400':
// description: bad request
// '401':
// description: unauthorized
// '403':
// description: forbidden
// '404':
// description: not found
// '406':
// description: not acceptable
// '500':
// description: internal server error
func (m *Module) EmojiPATCHHandler(c *gin.Context) {
authed, err := oauth.Authed(c, true, true, true, true)
if err != nil {
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
return
}

if !*authed.User.Admin {
err := fmt.Errorf("user %s not an admin", authed.User.ID)
api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet)
return
}

if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
return
}

emojiID := c.Param(IDKey)
if emojiID == "" {
err := errors.New("no emoji id specified")
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}

form := &model.EmojiUpdateRequest{}
if err := c.ShouldBind(form); err != nil {
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}

if err := validateUpdateEmoji(form); err != nil {
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
return
}

emoji, errWithCode := m.processor.AdminEmojiUpdate(c.Request.Context(), emojiID, form)
if errWithCode != nil {
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
return
}

c.JSON(http.StatusOK, emoji)
}

// do a first pass on the form here
func validateUpdateEmoji(form *model.EmojiUpdateRequest) error {
// check + normalize update type so we don't need
// to do this trimming + lowercasing again later
switch strings.TrimSpace(strings.ToLower(string(form.Type))) {
case string(model.EmojiUpdateDisable):
// no params required for this one, so don't bother checking
form.Type = model.EmojiUpdateDisable
case string(model.EmojiUpdateCopy):
// need at least a valid shortcode when doing a copy
if form.Shortcode == nil {
return errors.New("emoji action type was 'copy' but no shortcode was provided")
}

if err := validate.EmojiShortcode(*form.Shortcode); err != nil {
return err
}

// category optional during copy
if form.CategoryName != nil {
if err := validate.EmojiCategory(*form.CategoryName); err != nil {
return err
}
}

form.Type = model.EmojiUpdateCopy
case string(model.EmojiUpdateModify):
// need either image or category name for modify
hasImage := form.Image != nil && form.Image.Size != 0
hasCategoryName := form.CategoryName != nil
if !hasImage && !hasCategoryName {
return errors.New("emoji action type was 'modify' but no image or category name was provided")
}

if hasImage {
maxSize := config.GetMediaEmojiLocalMaxSize()
if form.Image.Size > int64(maxSize) {
return fmt.Errorf("emoji image too large: image is %dKB but size limit for custom emojis is %dKB", form.Image.Size/1024, maxSize/1024)
}
}

if hasCategoryName {
if err := validate.EmojiCategory(*form.CategoryName); err != nil {
return err
}
}

form.Type = model.EmojiUpdateModify
default:
return errors.New("emoji action type must be one of 'disable', 'copy', 'modify'")
}

return nil
}

0 comments on commit b6dbe21

Please sign in to comment.