Skip to content

Commit

Permalink
[feature] Add emoji DELETE handler at /api/v1/admin/custom_emojis (#…
Browse files Browse the repository at this point in the history
…913)

* add emoji DELETE handler

* no need to process error (thanks kim)

* don't double check if user is admin

* add missing security annotation
  • Loading branch information
tsmethurst committed Oct 14, 2022
1 parent 6a95f5f commit f7416d6
Show file tree
Hide file tree
Showing 11 changed files with 369 additions and 0 deletions.
39 changes: 39 additions & 0 deletions docs/api/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2862,6 +2862,45 @@ paths:
tags:
- admin
/api/v1/admin/custom_emojis/{id}:
delete:
description: |-
Emoji with the given ID will no longer be available to use on the instance.
If you just want to update the emoji image instead, use the `/api/v1/admin/custom_emojis/{id}` PATCH route.
To disable emojis from **remote** instances, use the `/api/v1/admin/custom_emojis/{id}` PATCH route.
operationId: emojiDelete
parameters:
- description: The id of the emoji.
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: The deleted emoji will be returned to the caller in case further processing is necessary.
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: Delete a **local** emoji with the given ID from the instance.
tags:
- admin
get:
operationId: emojiGet
parameters:
Expand Down
110 changes: 110 additions & 0 deletions internal/api/client/admin/emojidelete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
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"

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

// EmojiDELETEHandler swagger:operation DELETE /api/v1/admin/custom_emojis/{id} emojiDelete
//
// Delete a **local** emoji with the given ID from the instance.
//
// Emoji with the given ID will no longer be available to use on the instance.
//
// If you just want to update the emoji image instead, use the `/api/v1/admin/custom_emojis/{id}` PATCH route.
//
// To disable emojis from **remote** instances, use the `/api/v1/admin/custom_emojis/{id}` PATCH route.
//
// ---
// tags:
// - admin
//
// produces:
// - application/json
//
// parameters:
// -
// name: id
// type: string
// description: The id of the emoji.
// in: path
// required: true
//
// security:
// - OAuth2 Bearer:
// - admin
//
// responses:
// '200':
// description: The deleted emoji will be returned to the caller in case further processing is necessary.
// 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) EmojiDELETEHandler(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
}

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

c.JSON(http.StatusOK, emoji)
}
101 changes: 101 additions & 0 deletions internal/api/client/admin/emojidelete_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
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_test

import (
"context"
"io"
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/api/client/admin"
"github.com/superseriousbusiness/gotosocial/internal/db"
)

type EmojiDeleteTestSuite struct {
AdminStandardTestSuite
}

func (suite *EmojiDeleteTestSuite) TestEmojiDelete1() {
recorder := httptest.NewRecorder()
testEmoji := suite.testEmojis["rainbow"]

path := admin.EmojiPathWithID
ctx := suite.newContext(recorder, http.MethodDelete, nil, path, "application/json")
ctx.AddParam(admin.IDKey, testEmoji.ID)

suite.adminModule.EmojiDELETEHandler(ctx)
suite.Equal(http.StatusOK, recorder.Code)

b, err := io.ReadAll(recorder.Body)
suite.NoError(err)
suite.NotNil(b)

suite.Equal(`{"shortcode":"rainbow","url":"http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png","static_url":"http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/emoji/static/01F8MH9H8E4VG3KDYJR9EGPXCQ.png","visible_in_picker":true,"id":"01F8MH9H8E4VG3KDYJR9EGPXCQ","disabled":false,"updated_at":"2021-09-20T10:40:37.000Z","total_file_size":47115,"content_type":"image/png","uri":"http://localhost:8080/emoji/01F8MH9H8E4VG3KDYJR9EGPXCQ"}`, string(b))

// emoji should no longer be in the db
dbEmoji, err := suite.db.GetEmojiByID(context.Background(), testEmoji.ID)
suite.Nil(dbEmoji)
suite.ErrorIs(err, db.ErrNoEntries)
}

func (suite *EmojiDeleteTestSuite) TestEmojiDelete2() {
recorder := httptest.NewRecorder()
testEmoji := suite.testEmojis["yell"]

path := admin.EmojiPathWithID
ctx := suite.newContext(recorder, http.MethodDelete, nil, path, "application/json")
ctx.AddParam(admin.IDKey, testEmoji.ID)

suite.adminModule.EmojiDELETEHandler(ctx)
suite.Equal(http.StatusBadRequest, recorder.Code)

b, err := io.ReadAll(recorder.Body)
suite.NoError(err)
suite.NotNil(b)

suite.Equal(`{"error":"Bad Request: EmojiDelete: emoji with id 01GD5KP5CQEE1R3X43Y1EHS2CW was not a local emoji, will not delete"}`, string(b))

// emoji should still be in the db
dbEmoji, err := suite.db.GetEmojiByID(context.Background(), testEmoji.ID)
suite.NoError(err)
suite.NotNil(dbEmoji)
}

func (suite *EmojiDeleteTestSuite) TestEmojiDeleteNotFound() {
recorder := httptest.NewRecorder()

path := admin.EmojiPathWithID
ctx := suite.newContext(recorder, http.MethodDelete, nil, path, "application/json")
ctx.AddParam(admin.IDKey, "01GF8VRXX1R00X7XH8973Z29R1")

suite.adminModule.EmojiDELETEHandler(ctx)
suite.Equal(http.StatusNotFound, recorder.Code)

b, err := io.ReadAll(recorder.Body)
suite.NoError(err)
suite.NotNil(b)
suite.Equal(`{"error":"Not Found"}`, string(b))
}

func TestEmojiDeleteTestSuite(t *testing.T) {
suite.Run(t, &EmojiDeleteTestSuite{})
}
2 changes: 2 additions & 0 deletions internal/db/bundb/bundb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ type BunDBStandardTestSuite struct {
testTags map[string]*gtsmodel.Tag
testMentions map[string]*gtsmodel.Mention
testFollows map[string]*gtsmodel.Follow
testEmojis map[string]*gtsmodel.Emoji
}

func (suite *BunDBStandardTestSuite) SetupSuite() {
Expand All @@ -54,6 +55,7 @@ func (suite *BunDBStandardTestSuite) SetupSuite() {
suite.testTags = testrig.NewTestTags()
suite.testMentions = testrig.NewTestMentions()
suite.testFollows = testrig.NewTestFollows()
suite.testEmojis = testrig.NewTestEmojis()
}

func (suite *BunDBStandardTestSuite) SetupTest() {
Expand Down
37 changes: 37 additions & 0 deletions internal/db/bundb/emoji.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,43 @@ func (e *emojiDB) UpdateEmoji(ctx context.Context, emoji *gtsmodel.Emoji, column
return emoji, nil
}

func (e *emojiDB) DeleteEmojiByID(ctx context.Context, id string) db.Error {
if err := e.conn.RunInTx(ctx, func(tx bun.Tx) error {
// delete links between this emoji and any statuses that use it
if _, err := tx.
NewDelete().
TableExpr("? AS ?", bun.Ident("status_to_emojis"), bun.Ident("status_to_emoji")).
Where("? = ?", bun.Ident("status_to_emoji.emoji_id"), id).
Exec(ctx); err != nil {
return err
}

// delete links between this emoji and any accounts that use it
if _, err := tx.
NewDelete().
TableExpr("? AS ?", bun.Ident("account_to_emojis"), bun.Ident("account_to_emoji")).
Where("? = ?", bun.Ident("account_to_emoji.emoji_id"), id).
Exec(ctx); err != nil {
return err
}

if _, err := tx.
NewDelete().
TableExpr("? AS ?", bun.Ident("emojis"), bun.Ident("emoji")).
Where("? = ?", bun.Ident("emoji.id"), id).
Exec(ctx); err != nil {
return e.conn.ProcessError(err)
}

return nil
}); err != nil {
return err
}

e.cache.Invalidate(id)
return nil
}

func (e *emojiDB) GetEmojis(ctx context.Context, domain string, includeDisabled bool, includeEnabled bool, shortcode string, maxShortcodeDomain string, minShortcodeDomain string, limit int) ([]*gtsmodel.Emoji, db.Error) {
emojiIDs := []string{}

Expand Down
11 changes: 11 additions & 0 deletions internal/db/bundb/emoji_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,17 @@ func (suite *EmojiTestSuite) TestGetUseableEmojis() {
suite.Equal("rainbow", emojis[0].Shortcode)
}

func (suite *EmojiTestSuite) TestDeleteEmojiByID() {
testEmoji := suite.testEmojis["rainbow"]

err := suite.db.DeleteEmojiByID(context.Background(), testEmoji.ID)
suite.NoError(err)

dbEmoji, err := suite.db.GetEmojiByID(context.Background(), testEmoji.ID)
suite.Nil(dbEmoji)
suite.ErrorIs(err, db.ErrNoEntries)
}

func (suite *EmojiTestSuite) TestGetEmojiByStaticURL() {
emoji, err := suite.db.GetEmojiByStaticURL(context.Background(), "http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/emoji/static/01F8MH9H8E4VG3KDYJR9EGPXCQ.png")
suite.NoError(err)
Expand Down
2 changes: 2 additions & 0 deletions internal/db/emoji.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ type Emoji interface {
// UpdateEmoji updates the given columns of one emoji.
// If no columns are specified, every column is updated.
UpdateEmoji(ctx context.Context, emoji *gtsmodel.Emoji, columns ...string) (*gtsmodel.Emoji, Error)
// DeleteEmojiByID deletes one emoji by its database ID.
DeleteEmojiByID(ctx context.Context, id string) Error
// GetUseableEmojis gets all emojis which are useable by accounts on this instance.
GetUseableEmojis(ctx context.Context) ([]*gtsmodel.Emoji, Error)
// GetEmojis gets emojis based on given parameters. Useful for admin actions.
Expand Down
4 changes: 4 additions & 0 deletions internal/processing/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ func (p *processor) AdminEmojiGet(ctx context.Context, authed *oauth.Auth, id st
return p.adminProcessor.EmojiGet(ctx, authed.Account, authed.User, id)
}

func (p *processor) AdminEmojiDelete(ctx context.Context, authed *oauth.Auth, id string) (*apimodel.AdminEmoji, gtserror.WithCode) {
return p.adminProcessor.EmojiDelete(ctx, id)
}

func (p *processor) AdminDomainBlockCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.DomainBlockCreateRequest) (*apimodel.DomainBlock, gtserror.WithCode) {
return p.adminProcessor.DomainBlockCreate(ctx, authed.Account, form.Domain, form.Obfuscate, form.PublicComment, form.PrivateComment, "")
}
Expand Down
1 change: 1 addition & 0 deletions internal/processing/admin/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ type Processor interface {
EmojiCreate(ctx context.Context, account *gtsmodel.Account, user *gtsmodel.User, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, gtserror.WithCode)
EmojisGet(ctx context.Context, account *gtsmodel.Account, user *gtsmodel.User, domain string, includeDisabled bool, includeEnabled bool, shortcode string, maxShortcodeDomain string, minShortcodeDomain string, limit int) (*apimodel.PageableResponse, gtserror.WithCode)
EmojiGet(ctx context.Context, account *gtsmodel.Account, user *gtsmodel.User, id string) (*apimodel.AdminEmoji, gtserror.WithCode)
EmojiDelete(ctx context.Context, id string) (*apimodel.AdminEmoji, gtserror.WithCode)
MediaPrune(ctx context.Context, mediaRemoteCacheDays int) gtserror.WithCode
}

Expand Down

0 comments on commit f7416d6

Please sign in to comment.