diff options
| author | 2022-11-14 23:47:27 +0100 | |
|---|---|---|
| committer | 2022-11-14 22:47:27 +0000 | |
| commit | 4cd00d546c495b085487d11f2fe2c4928600dc10 (patch) | |
| tree | 6605424baafddf020a4a6e0a0ddcde9293c1cb56 /internal/api | |
| parent | [chore] Remove unused `admin account suspend` action (#1047) (diff) | |
| download | gotosocial-4cd00d546c495b085487d11f2fe2c4928600dc10.tar.xz | |
[feature] Allow newly uploaded emojis to be placed in categories (#939)
* [feature] Add emoji categories GET
Serialize emojis in appropriate categories; make it possible to get categories via the admin API
* [feature] Create (or use existing) category for new emoji uploads
* fix lint issue
* update misleading line in swagger docs
Diffstat (limited to 'internal/api')
| -rw-r--r-- | internal/api/client/admin/admin.go | 3 | ||||
| -rw-r--r-- | internal/api/client/admin/admin_test.go | 18 | ||||
| -rw-r--r-- | internal/api/client/admin/emojicategoriesget.go | 94 | ||||
| -rw-r--r-- | internal/api/client/admin/emojicategoriesget_test.go | 53 | ||||
| -rw-r--r-- | internal/api/client/admin/emojicreate.go | 15 | ||||
| -rw-r--r-- | internal/api/client/admin/emojicreate_test.go | 149 | ||||
| -rw-r--r-- | internal/api/client/admin/emojidelete_test.go | 2 | ||||
| -rw-r--r-- | internal/api/client/admin/emojiget_test.go | 2 | ||||
| -rw-r--r-- | internal/api/model/emoji.go | 3 | ||||
| -rw-r--r-- | internal/api/model/emojicategory.go | 29 | 
10 files changed, 356 insertions, 12 deletions
diff --git a/internal/api/client/admin/admin.go b/internal/api/client/admin/admin.go index 0ef8b4fcc..e34bac1cf 100644 --- a/internal/api/client/admin/admin.go +++ b/internal/api/client/admin/admin.go @@ -33,6 +33,8 @@ const (  	EmojiPath = BasePath + "/custom_emojis"  	// EmojiPathWithID is used for interacting with a single emoji.  	EmojiPathWithID = EmojiPath + "/:" + IDKey +	// EmojiCategoriesPath is used for interacting with emoji categories. +	EmojiCategoriesPath = EmojiPath + "/categories"  	// DomainBlocksPath is used for posting domain blocks.  	DomainBlocksPath = BasePath + "/domain_blocks"  	// DomainBlocksPathWithID is used for interacting with a single domain block. @@ -87,5 +89,6 @@ func (m *Module) Route(r router.Router) error {  	r.AttachHandler(http.MethodDelete, DomainBlocksPathWithID, m.DomainBlockDELETEHandler)  	r.AttachHandler(http.MethodPost, AccountsActionPath, m.AccountActionPOSTHandler)  	r.AttachHandler(http.MethodPost, MediaCleanupPath, m.MediaCleanupPOSTHandler) +	r.AttachHandler(http.MethodGet, EmojiCategoriesPath, m.EmojiCategoriesGETHandler)  	return nil  } diff --git a/internal/api/client/admin/admin_test.go b/internal/api/client/admin/admin_test.go index ce026a67b..9303ee3f2 100644 --- a/internal/api/client/admin/admin_test.go +++ b/internal/api/client/admin/admin_test.go @@ -53,14 +53,15 @@ type AdminStandardTestSuite struct {  	sentEmails   map[string]string  	// standard suite models -	testTokens       map[string]*gtsmodel.Token -	testClients      map[string]*gtsmodel.Client -	testApplications map[string]*gtsmodel.Application -	testUsers        map[string]*gtsmodel.User -	testAccounts     map[string]*gtsmodel.Account -	testAttachments  map[string]*gtsmodel.MediaAttachment -	testStatuses     map[string]*gtsmodel.Status -	testEmojis       map[string]*gtsmodel.Emoji +	testTokens          map[string]*gtsmodel.Token +	testClients         map[string]*gtsmodel.Client +	testApplications    map[string]*gtsmodel.Application +	testUsers           map[string]*gtsmodel.User +	testAccounts        map[string]*gtsmodel.Account +	testAttachments     map[string]*gtsmodel.MediaAttachment +	testStatuses        map[string]*gtsmodel.Status +	testEmojis          map[string]*gtsmodel.Emoji +	testEmojiCategories map[string]*gtsmodel.EmojiCategory  	// module being tested  	adminModule *admin.Module @@ -75,6 +76,7 @@ func (suite *AdminStandardTestSuite) SetupSuite() {  	suite.testAttachments = testrig.NewTestAttachments()  	suite.testStatuses = testrig.NewTestStatuses()  	suite.testEmojis = testrig.NewTestEmojis() +	suite.testEmojiCategories = testrig.NewTestEmojiCategories()  }  func (suite *AdminStandardTestSuite) SetupTest() { diff --git a/internal/api/client/admin/emojicategoriesget.go b/internal/api/client/admin/emojicategoriesget.go new file mode 100644 index 000000000..d8b379674 --- /dev/null +++ b/internal/api/client/admin/emojicategoriesget.go @@ -0,0 +1,94 @@ +/* +   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 ( +	"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" +) + +// EmojiCategoriesGETHandler swagger:operation GET /api/v1/admin/custom_emojis/categories emojiCategoriesGet +// +// Get a list of existing emoji categories. +// +//	--- +//	tags: +//	- admin +// +//	produces: +//	- application/json +// +//	parameters: +//	- +//		name: id +//		type: string +//		description: The id of the emoji. +//		in: path +//		required: true +// +//	responses: +//		'200': +//			description: Array of existing emoji categories. +//			schema: +//				type: array +//				items: +//					"$ref": "#/definitions/adminEmojiCategory" +//		'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) EmojiCategoriesGETHandler(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 +	} + +	categories, errWithCode := m.processor.AdminEmojiCategoriesGet(c.Request.Context()) +	if errWithCode != nil { +		api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) +		return +	} + +	c.JSON(http.StatusOK, categories) +} diff --git a/internal/api/client/admin/emojicategoriesget_test.go b/internal/api/client/admin/emojicategoriesget_test.go new file mode 100644 index 000000000..ac6b73931 --- /dev/null +++ b/internal/api/client/admin/emojicategoriesget_test.go @@ -0,0 +1,53 @@ +/* +   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 ( +	"io" +	"net/http" +	"net/http/httptest" +	"testing" + +	"github.com/stretchr/testify/suite" +	"github.com/superseriousbusiness/gotosocial/internal/api/client/admin" +) + +type EmojiCategoriesGetTestSuite struct { +	AdminStandardTestSuite +} + +func (suite *EmojiCategoriesGetTestSuite) TestEmojiCategoriesGet() { +	recorder := httptest.NewRecorder() + +	path := admin.EmojiCategoriesPath +	ctx := suite.newContext(recorder, http.MethodGet, nil, path, "application/json") + +	suite.adminModule.EmojiCategoriesGETHandler(ctx) +	suite.Equal(http.StatusOK, recorder.Code) + +	b, err := io.ReadAll(recorder.Body) +	suite.NoError(err) +	suite.NotNil(b) + +	suite.Equal(`[{"id":"01GGQ989PTT9PMRN4FZ1WWK2B9","name":"cute stuff"},{"id":"01GGQ8V4993XK67B2JB396YFB7","name":"reactions"}]`, string(b)) +} + +func TestEmojiCategoriesGetTestSuite(t *testing.T) { +	suite.Run(t, &EmojiCategoriesGetTestSuite{}) +} diff --git a/internal/api/client/admin/emojicreate.go b/internal/api/client/admin/emojicreate.go index b8dbfe43e..2a075708f 100644 --- a/internal/api/client/admin/emojicreate.go +++ b/internal/api/client/admin/emojicreate.go @@ -64,6 +64,15 @@ import (  //			To ensure compatibility with other fedi implementations, emoji size limit is 50kb by default.  //		type: file  //		required: true +//	- +//		name: category +//		in: formData +//		description: >- +//			Category in which to place the new emoji. 64 characters or less. +//			If left blank, emoji will be uncategorized. If a category with the +//			given name doesn't exist yet, it will be created. +//		type: string +//		required: false  //  //	security:  //	- OAuth2 Bearer: @@ -136,5 +145,9 @@ func validateCreateEmoji(form *model.EmojiCreateRequest) error {  		return fmt.Errorf("emoji image too large: image is %dKB but size limit for custom emojis is %dKB", form.Image.Size/1024, maxSize/1024)  	} -	return validate.EmojiShortcode(form.Shortcode) +	if err := validate.EmojiShortcode(form.Shortcode); err != nil { +		return err +	} + +	return validate.EmojiCategory(form.CategoryName)  } diff --git a/internal/api/client/admin/emojicreate_test.go b/internal/api/client/admin/emojicreate_test.go index 3131e0816..9078fe16e 100644 --- a/internal/api/client/admin/emojicreate_test.go +++ b/internal/api/client/admin/emojicreate_test.go @@ -36,12 +36,159 @@ type EmojiCreateTestSuite struct {  	AdminStandardTestSuite  } -func (suite *EmojiCreateTestSuite) TestEmojiCreate() { +func (suite *EmojiCreateTestSuite) TestEmojiCreateNewCategory() {  	// set up the request  	requestBody, w, err := testrig.CreateMultipartFormData(  		"image", "../../../../testrig/media/rainbow-original.png",  		map[string]string{  			"shortcode": "new_emoji", +			"category":  "Test Emojis", // this category doesn't exist yet +		}) +	if err != nil { +		panic(err) +	} +	bodyBytes := requestBody.Bytes() +	recorder := httptest.NewRecorder() +	ctx := suite.newContext(recorder, http.MethodPost, bodyBytes, admin.EmojiPath, w.FormDataContentType()) + +	// call the handler +	suite.adminModule.EmojiCreatePOSTHandler(ctx) + +	// 1. we should have OK because our request was valid +	suite.Equal(http.StatusOK, recorder.Code) + +	// 2. we should have no error message in the result body +	result := recorder.Result() +	defer result.Body.Close() + +	// check the response +	b, err := ioutil.ReadAll(result.Body) +	suite.NoError(err) +	suite.NotEmpty(b) + +	// response should be an api model emoji +	apiEmoji := &apimodel.Emoji{} +	err = json.Unmarshal(b, apiEmoji) +	suite.NoError(err) + +	// appropriate fields should be set +	suite.Equal("new_emoji", apiEmoji.Shortcode) +	suite.NotEmpty(apiEmoji.URL) +	suite.NotEmpty(apiEmoji.StaticURL) +	suite.True(apiEmoji.VisibleInPicker) + +	// emoji should be in the db +	dbEmoji, err := suite.db.GetEmojiByShortcodeDomain(context.Background(), apiEmoji.Shortcode, "") +	suite.NoError(err) + +	// check fields on the emoji +	suite.NotEmpty(dbEmoji.ID) +	suite.Equal("new_emoji", dbEmoji.Shortcode) +	suite.Empty(dbEmoji.Domain) +	suite.Empty(dbEmoji.ImageRemoteURL) +	suite.Empty(dbEmoji.ImageStaticRemoteURL) +	suite.Equal(apiEmoji.URL, dbEmoji.ImageURL) +	suite.Equal(apiEmoji.StaticURL, dbEmoji.ImageStaticURL) +	suite.NotEmpty(dbEmoji.ImagePath) +	suite.NotEmpty(dbEmoji.ImageStaticPath) +	suite.Equal("image/png", dbEmoji.ImageContentType) +	suite.Equal("image/png", dbEmoji.ImageStaticContentType) +	suite.Equal(36702, dbEmoji.ImageFileSize) +	suite.Equal(10413, dbEmoji.ImageStaticFileSize) +	suite.False(*dbEmoji.Disabled) +	suite.NotEmpty(dbEmoji.URI) +	suite.True(*dbEmoji.VisibleInPicker) +	suite.NotEmpty(dbEmoji.CategoryID) + +	// emoji should be in storage +	emojiBytes, err := suite.storage.Get(ctx, dbEmoji.ImagePath) +	suite.NoError(err) +	suite.Len(emojiBytes, dbEmoji.ImageFileSize) +	emojiStaticBytes, err := suite.storage.Get(ctx, dbEmoji.ImageStaticPath) +	suite.NoError(err) +	suite.Len(emojiStaticBytes, dbEmoji.ImageStaticFileSize) +} + +func (suite *EmojiCreateTestSuite) TestEmojiCreateExistingCategory() { +	// set up the request +	requestBody, w, err := testrig.CreateMultipartFormData( +		"image", "../../../../testrig/media/rainbow-original.png", +		map[string]string{ +			"shortcode": "new_emoji", +			"category":  "cute stuff", // this category already exists +		}) +	if err != nil { +		panic(err) +	} +	bodyBytes := requestBody.Bytes() +	recorder := httptest.NewRecorder() +	ctx := suite.newContext(recorder, http.MethodPost, bodyBytes, admin.EmojiPath, w.FormDataContentType()) + +	// call the handler +	suite.adminModule.EmojiCreatePOSTHandler(ctx) + +	// 1. we should have OK because our request was valid +	suite.Equal(http.StatusOK, recorder.Code) + +	// 2. we should have no error message in the result body +	result := recorder.Result() +	defer result.Body.Close() + +	// check the response +	b, err := ioutil.ReadAll(result.Body) +	suite.NoError(err) +	suite.NotEmpty(b) + +	// response should be an api model emoji +	apiEmoji := &apimodel.Emoji{} +	err = json.Unmarshal(b, apiEmoji) +	suite.NoError(err) + +	// appropriate fields should be set +	suite.Equal("new_emoji", apiEmoji.Shortcode) +	suite.NotEmpty(apiEmoji.URL) +	suite.NotEmpty(apiEmoji.StaticURL) +	suite.True(apiEmoji.VisibleInPicker) + +	// emoji should be in the db +	dbEmoji, err := suite.db.GetEmojiByShortcodeDomain(context.Background(), apiEmoji.Shortcode, "") +	suite.NoError(err) + +	// check fields on the emoji +	suite.NotEmpty(dbEmoji.ID) +	suite.Equal("new_emoji", dbEmoji.Shortcode) +	suite.Empty(dbEmoji.Domain) +	suite.Empty(dbEmoji.ImageRemoteURL) +	suite.Empty(dbEmoji.ImageStaticRemoteURL) +	suite.Equal(apiEmoji.URL, dbEmoji.ImageURL) +	suite.Equal(apiEmoji.StaticURL, dbEmoji.ImageStaticURL) +	suite.NotEmpty(dbEmoji.ImagePath) +	suite.NotEmpty(dbEmoji.ImageStaticPath) +	suite.Equal("image/png", dbEmoji.ImageContentType) +	suite.Equal("image/png", dbEmoji.ImageStaticContentType) +	suite.Equal(36702, dbEmoji.ImageFileSize) +	suite.Equal(10413, dbEmoji.ImageStaticFileSize) +	suite.False(*dbEmoji.Disabled) +	suite.NotEmpty(dbEmoji.URI) +	suite.True(*dbEmoji.VisibleInPicker) +	suite.Equal(suite.testEmojiCategories["cute stuff"].ID, dbEmoji.CategoryID) + +	// emoji should be in storage +	emojiBytes, err := suite.storage.Get(ctx, dbEmoji.ImagePath) +	suite.NoError(err) +	suite.Len(emojiBytes, dbEmoji.ImageFileSize) +	emojiStaticBytes, err := suite.storage.Get(ctx, dbEmoji.ImageStaticPath) +	suite.NoError(err) +	suite.Len(emojiStaticBytes, dbEmoji.ImageStaticFileSize) +} + +func (suite *EmojiCreateTestSuite) TestEmojiCreateNoCategory() { +	// set up the request +	requestBody, w, err := testrig.CreateMultipartFormData( +		"image", "../../../../testrig/media/rainbow-original.png", +		map[string]string{ +			"shortcode": "new_emoji", +			"category":  "",  		})  	if err != nil {  		panic(err) diff --git a/internal/api/client/admin/emojidelete_test.go b/internal/api/client/admin/emojidelete_test.go index c930c377a..350eb1159 100644 --- a/internal/api/client/admin/emojidelete_test.go +++ b/internal/api/client/admin/emojidelete_test.go @@ -49,7 +49,7 @@ func (suite *EmojiDeleteTestSuite) TestEmojiDelete1() {  	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)) +	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,"category":"reactions","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) diff --git a/internal/api/client/admin/emojiget_test.go b/internal/api/client/admin/emojiget_test.go index d94e2bf78..6e1882c70 100644 --- a/internal/api/client/admin/emojiget_test.go +++ b/internal/api/client/admin/emojiget_test.go @@ -47,7 +47,7 @@ func (suite *EmojiGetTestSuite) TestEmojiGet1() {  	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)) +	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,"category":"reactions","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))  }  func (suite *EmojiGetTestSuite) TestEmojiGet2() { diff --git a/internal/api/model/emoji.go b/internal/api/model/emoji.go index 2fa858596..eb636f63c 100644 --- a/internal/api/model/emoji.go +++ b/internal/api/model/emoji.go @@ -50,4 +50,7 @@ type EmojiCreateRequest struct {  	Shortcode string `form:"shortcode" validation:"required"`  	// Image file to use for the emoji. Must be png or gif and no larger than 50kb.  	Image *multipart.FileHeader `form:"image" validation:"required"` +	// Category in which to place the new emoji. Will be uncategorized by default. +	// CategoryName length should not exceed 64 characters. +	CategoryName string `form:"category"`  } diff --git a/internal/api/model/emojicategory.go b/internal/api/model/emojicategory.go new file mode 100644 index 000000000..0a14b303e --- /dev/null +++ b/internal/api/model/emojicategory.go @@ -0,0 +1,29 @@ +/* +   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 model + +// EmojiCategory represents a custom emoji category. +// +// swagger:model emojiCategory +type EmojiCategory struct { +	// The ID of the custom emoji category. +	ID string `json:"id"` +	// The name of the custom emoji category. +	Name string `json:"name"` +}  | 
