From 6a95f5fa67ad27937198e0c9e25e3d8fe1f2bd2c Mon Sep 17 00:00:00 2001 From: tobi <31960611+tsmethurst@users.noreply.github.com> Date: Thu, 13 Oct 2022 16:37:55 +0200 Subject: [PATCH] [feature] Add `/api/v1/admin/custom_emojis/{id}` endpoint for single emoji GET (#910) * fix error in prev swagger docs * add GET for single admin emoji --- docs/api/swagger.yaml | 35 +++- internal/api/client/admin/admin_test.go | 2 + internal/api/client/admin/emojiget.go | 145 ++------------ internal/api/client/admin/emojiget_test.go | 59 ++---- internal/api/client/admin/emojisget.go | 211 ++++++++++++++++++++ internal/api/client/admin/emojisget_test.go | 114 +++++++++++ internal/processing/admin.go | 4 + internal/processing/admin/admin.go | 1 + internal/processing/admin/getemoji.go | 54 +++++ internal/processing/processor.go | 2 + 10 files changed, 455 insertions(+), 172 deletions(-) create mode 100644 internal/api/client/admin/emojisget.go create mode 100644 internal/api/client/admin/emojisget_test.go create mode 100644 internal/processing/admin/getemoji.go diff --git a/docs/api/swagger.yaml b/docs/api/swagger.yaml index 5330d080f..bef064102 100644 --- a/docs/api/swagger.yaml +++ b/docs/api/swagger.yaml @@ -2773,8 +2773,8 @@ paths: in: query name: filter type: string - - default: 30 - description: Number of emojis to return. If below 1, will be set to 1, if greater than 50, will be set to 50. + - default: 50 + description: Number of emojis to return. Less than 1, or not set, means unlimited (all emojis). in: query name: limit type: integer @@ -2861,6 +2861,37 @@ paths: summary: Upload and create a new instance emoji. tags: - admin + /api/v1/admin/custom_emojis/{id}: + get: + operationId: emojiGet + parameters: + - description: The id of the emoji. + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: A single 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 + summary: Get the admin view of a single emoji. + tags: + - admin /api/v1/admin/domain_blocks: get: operationId: domainBlocksGet diff --git a/internal/api/client/admin/admin_test.go b/internal/api/client/admin/admin_test.go index 5017afd05..ce026a67b 100644 --- a/internal/api/client/admin/admin_test.go +++ b/internal/api/client/admin/admin_test.go @@ -60,6 +60,7 @@ type AdminStandardTestSuite struct { testAccounts map[string]*gtsmodel.Account testAttachments map[string]*gtsmodel.MediaAttachment testStatuses map[string]*gtsmodel.Status + testEmojis map[string]*gtsmodel.Emoji // module being tested adminModule *admin.Module @@ -73,6 +74,7 @@ func (suite *AdminStandardTestSuite) SetupSuite() { suite.testAccounts = testrig.NewTestAccounts() suite.testAttachments = testrig.NewTestAttachments() suite.testStatuses = testrig.NewTestStatuses() + suite.testEmojis = testrig.NewTestEmojis() } func (suite *AdminStandardTestSuite) SetupTest() { diff --git a/internal/api/client/admin/emojiget.go b/internal/api/client/admin/emojiget.go index 7c44f45d4..60f7d5948 100644 --- a/internal/api/client/admin/emojiget.go +++ b/internal/api/client/admin/emojiget.go @@ -19,27 +19,19 @@ package admin import ( + "errors" "fmt" "net/http" - "strconv" - "strings" "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/config" - "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) -// EmojisGETHandler swagger:operation GET /api/v1/admin/custom_emojis emojisGet +// EmojiGETHandler swagger:operation GET /api/v1/admin/custom_emojis/{id} emojiGet // -// View local and remote emojis available to / known by this instance. -// -// The next and previous queries can be parsed from the returned Link header. -// Example: -// -// `; rel="next", ; rel="prev"` +// Get the admin view of a single emoji. // // --- // tags: @@ -50,66 +42,17 @@ import ( // // parameters: // - -// name: filter +// name: id // type: string -// description: |- -// Comma-separated list of filters to apply to results. Recognized filters are: -// -// `domain:[domain]` -- show emojis from the given domain, eg `?filter=domain:example.org` will show emojis from `example.org` only. -// Instead of giving a specific domain, you can also give either one of the key words `local` or `all` to show either local emojis only (`domain:local`) or show all emojis from all domains (`domain:all`). -// Note: `domain:*` is equivalent to `domain:all` (including local). -// If no domain filter is provided, `domain:all` will be assumed. -// -// `disabled` -- include emojis that have been disabled. -// -// `enabled` -- include emojis that are enabled. -// -// `shortcode:[shortcode]` -- show only emojis with the given shortcode, eg `?filter=shortcode:blob_cat_uwu` will show only emojis with the shortcode `blob_cat_uwu` (case sensitive). -// -// If neither `disabled` or `enabled` are provided, both disabled and enabled emojis will be shown. -// -// If no filter query string is provided, the default `domain:all` will be used, which will show all emojis from all domains. -// in: query -// required: false -// default: "domain:all" -// - -// name: limit -// type: integer -// description: Number of emojis to return. Less than 1, or not set, means unlimited (all emojis). -// default: 50 -// in: query -// - -// name: max_shortcode_domain -// type: string -// description: >- -// Return only emojis with `[shortcode]@[domain]` *LOWER* (alphabetically) than given `[shortcode]@[domain]`. -// For example, if `max_shortcode_domain=beep@example.org`, then returned values might include emojis with -// `[shortcode]@[domain]`s like `car@example.org`, `debian@aaa.com`, `test@` (local emoji), etc. -// -// Emoji with the given `[shortcode]@[domain]` will not be included in the result set. -// in: query -// - -// name: min_shortcode_domain -// type: string -// description: >- -// Return only emojis with `[shortcode]@[domain]` *HIGHER* (alphabetically) than given `[shortcode]@[domain]`. -// For example, if `max_shortcode_domain=beep@example.org`, then returned values might include emojis with -// `[shortcode]@[domain]`s like `arse@test.com`, `0101_binary@hackers.net`, `bee@` (local emoji), etc. -// -// Emoji with the given `[shortcode]@[domain]` will not be included in the result set. -// in: query +// description: The id of the emoji. +// in: path +// required: true // // responses: // '200': -// headers: -// Link: -// type: string -// description: Links to the next and previous queries. -// description: An array of emojis, arranged alphabetically by shortcode and domain. +// description: A single emoji. // schema: -// type: array -// items: -// "$ref": "#/definitions/adminEmoji" +// "$ref": "#/definitions/adminEmoji" // '400': // description: bad request // '401': @@ -122,7 +65,7 @@ import ( // description: not acceptable // '500': // description: internal server error -func (m *Module) EmojisGETHandler(c *gin.Context) { +func (m *Module) EmojiGETHandler(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) @@ -140,72 +83,18 @@ func (m *Module) EmojisGETHandler(c *gin.Context) { return } - maxShortcodeDomain := c.Query(MaxShortcodeDomainKey) - minShortcodeDomain := c.Query(MinShortcodeDomainKey) - - limit := 50 - limitString := c.Query(LimitKey) - if limitString != "" { - i, err := strconv.ParseInt(limitString, 10, 64) - if err != nil { - err := fmt.Errorf("error parsing %s: %s", LimitKey, err) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) - return - } - limit = int(i) - } - if limit < 0 { - limit = 0 + 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 } - var domain string - var includeDisabled bool - var includeEnabled bool - var shortcode string - if filterParam := c.Query(FilterQueryKey); filterParam != "" { - filters := strings.Split(filterParam, ",") - for _, filter := range filters { - lower := strings.ToLower(filter) - switch { - case strings.HasPrefix(lower, "domain:"): - domain = strings.TrimPrefix(lower, "domain:") - case lower == "disabled": - includeDisabled = true - case lower == "enabled": - includeEnabled = true - case strings.HasPrefix(lower, "shortcode:"): - shortcode = strings.Trim(filter[10:], ":") // remove any errant ":" - default: - err := fmt.Errorf("filter %s not recognized; accepted values are 'domain:[domain]', 'disabled', 'enabled', 'shortcode:[shortcode]'", filter) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) - return - } - } - } - - if domain == "" { - // default is to show all domains - domain = db.EmojiAllDomains - } else if domain == "local" || domain == config.GetHost() || domain == config.GetAccountDomain() { - // pass empty string for local domain - domain = "" - } - - // normalize filters - if !includeDisabled && !includeEnabled { - // include both if neither specified - includeDisabled = true - includeEnabled = true - } - - resp, errWithCode := m.processor.AdminEmojisGet(c.Request.Context(), authed, domain, includeDisabled, includeEnabled, shortcode, maxShortcodeDomain, minShortcodeDomain, limit) + emoji, errWithCode := m.processor.AdminEmojiGet(c.Request.Context(), authed, emojiID) if errWithCode != nil { api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) return } - if resp.LinkHeader != "" { - c.Header("Link", resp.LinkHeader) - } - c.JSON(http.StatusOK, resp.Items) + c.JSON(http.StatusOK, emoji) } diff --git a/internal/api/client/admin/emojiget_test.go b/internal/api/client/admin/emojiget_test.go index bba5561af..d94e2bf78 100644 --- a/internal/api/client/admin/emojiget_test.go +++ b/internal/api/client/admin/emojiget_test.go @@ -19,7 +19,6 @@ package admin_test import ( - "encoding/json" "io" "net/http" "net/http/httptest" @@ -27,86 +26,62 @@ import ( "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/api/client/admin" - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" ) type EmojiGetTestSuite struct { AdminStandardTestSuite } -func (suite *EmojiGetTestSuite) TestEmojiGet() { +func (suite *EmojiGetTestSuite) TestEmojiGet1() { recorder := httptest.NewRecorder() + testEmoji := suite.testEmojis["rainbow"] - path := admin.EmojiPath + "?filter=domain:all&limit=1" + path := admin.EmojiPathWithID ctx := suite.newContext(recorder, http.MethodGet, nil, path, "application/json") + ctx.AddParam(admin.IDKey, testEmoji.ID) - suite.adminModule.EmojisGETHandler(ctx) + suite.adminModule.EmojiGETHandler(ctx) suite.Equal(http.StatusOK, recorder.Code) b, err := io.ReadAll(recorder.Body) suite.NoError(err) suite.NotNil(b) - apiEmojis := []*apimodel.AdminEmoji{} - if err := json.Unmarshal(b, &apiEmojis); err != nil { - suite.FailNow(err.Error()) - } - - suite.Len(apiEmojis, 1) - suite.Equal("rainbow", apiEmojis[0].Shortcode) - suite.Equal("", apiEmojis[0].Domain) - - suite.Equal(`; rel="next", ; rel="prev"`, recorder.Header().Get("link")) + 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)) } func (suite *EmojiGetTestSuite) TestEmojiGet2() { recorder := httptest.NewRecorder() + testEmoji := suite.testEmojis["yell"] - path := admin.EmojiPath + "?filter=domain:all&limit=1&max_shortcode_domain=rainbow@" + path := admin.EmojiPathWithID ctx := suite.newContext(recorder, http.MethodGet, nil, path, "application/json") + ctx.AddParam(admin.IDKey, testEmoji.ID) - suite.adminModule.EmojisGETHandler(ctx) + suite.adminModule.EmojiGETHandler(ctx) suite.Equal(http.StatusOK, recorder.Code) b, err := io.ReadAll(recorder.Body) suite.NoError(err) suite.NotNil(b) - apiEmojis := []*apimodel.AdminEmoji{} - if err := json.Unmarshal(b, &apiEmojis); err != nil { - suite.FailNow(err.Error()) - } - - suite.Len(apiEmojis, 1) - suite.Equal("yell", apiEmojis[0].Shortcode) - suite.Equal("fossbros-anonymous.io", apiEmojis[0].Domain) - - suite.Equal(`; rel="next", ; rel="prev"`, recorder.Header().Get("link")) + suite.Equal(`{"shortcode":"yell","url":"http://localhost:8080/fileserver/01GD5KR15NHTY8FZ01CD4D08XP/emoji/original/01GD5KP5CQEE1R3X43Y1EHS2CW.png","static_url":"http://localhost:8080/fileserver/01GD5KR15NHTY8FZ01CD4D08XP/emoji/static/01GD5KP5CQEE1R3X43Y1EHS2CW.png","visible_in_picker":false,"id":"01GD5KP5CQEE1R3X43Y1EHS2CW","disabled":false,"domain":"fossbros-anonymous.io","updated_at":"2020-03-18T12:12:00.000Z","total_file_size":21697,"content_type":"image/png","uri":"http://fossbros-anonymous.io/emoji/01GD5KP5CQEE1R3X43Y1EHS2CW"}`, string(b)) } -func (suite *EmojiGetTestSuite) TestEmojiGet3() { +func (suite *EmojiGetTestSuite) TestEmojiGetNotFound() { recorder := httptest.NewRecorder() - path := admin.EmojiPath + "?filter=domain:all&limit=1&min_shortcode_domain=yell@fossbros-anonymous.io" + path := admin.EmojiPathWithID ctx := suite.newContext(recorder, http.MethodGet, nil, path, "application/json") + ctx.AddParam(admin.IDKey, "01GF8VRXX1R00X7XH8973Z29R1") - suite.adminModule.EmojisGETHandler(ctx) - suite.Equal(http.StatusOK, recorder.Code) + suite.adminModule.EmojiGETHandler(ctx) + suite.Equal(http.StatusNotFound, recorder.Code) b, err := io.ReadAll(recorder.Body) suite.NoError(err) suite.NotNil(b) - - apiEmojis := []*apimodel.AdminEmoji{} - if err := json.Unmarshal(b, &apiEmojis); err != nil { - suite.FailNow(err.Error()) - } - - suite.Len(apiEmojis, 1) - suite.Equal("rainbow", apiEmojis[0].Shortcode) - suite.Equal("", apiEmojis[0].Domain) - - suite.Equal(`; rel="next", ; rel="prev"`, recorder.Header().Get("link")) + suite.Equal(`{"error":"Not Found"}`, string(b)) } func TestEmojiGetTestSuite(t *testing.T) { diff --git a/internal/api/client/admin/emojisget.go b/internal/api/client/admin/emojisget.go new file mode 100644 index 000000000..7c44f45d4 --- /dev/null +++ b/internal/api/client/admin/emojisget.go @@ -0,0 +1,211 @@ +/* + 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 . +*/ + +package admin + +import ( + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/oauth" +) + +// EmojisGETHandler swagger:operation GET /api/v1/admin/custom_emojis emojisGet +// +// View local and remote emojis available to / known by this instance. +// +// The next and previous queries can be parsed from the returned Link header. +// Example: +// +// `; rel="next", ; rel="prev"` +// +// --- +// tags: +// - admin +// +// produces: +// - application/json +// +// parameters: +// - +// name: filter +// type: string +// description: |- +// Comma-separated list of filters to apply to results. Recognized filters are: +// +// `domain:[domain]` -- show emojis from the given domain, eg `?filter=domain:example.org` will show emojis from `example.org` only. +// Instead of giving a specific domain, you can also give either one of the key words `local` or `all` to show either local emojis only (`domain:local`) or show all emojis from all domains (`domain:all`). +// Note: `domain:*` is equivalent to `domain:all` (including local). +// If no domain filter is provided, `domain:all` will be assumed. +// +// `disabled` -- include emojis that have been disabled. +// +// `enabled` -- include emojis that are enabled. +// +// `shortcode:[shortcode]` -- show only emojis with the given shortcode, eg `?filter=shortcode:blob_cat_uwu` will show only emojis with the shortcode `blob_cat_uwu` (case sensitive). +// +// If neither `disabled` or `enabled` are provided, both disabled and enabled emojis will be shown. +// +// If no filter query string is provided, the default `domain:all` will be used, which will show all emojis from all domains. +// in: query +// required: false +// default: "domain:all" +// - +// name: limit +// type: integer +// description: Number of emojis to return. Less than 1, or not set, means unlimited (all emojis). +// default: 50 +// in: query +// - +// name: max_shortcode_domain +// type: string +// description: >- +// Return only emojis with `[shortcode]@[domain]` *LOWER* (alphabetically) than given `[shortcode]@[domain]`. +// For example, if `max_shortcode_domain=beep@example.org`, then returned values might include emojis with +// `[shortcode]@[domain]`s like `car@example.org`, `debian@aaa.com`, `test@` (local emoji), etc. +// +// Emoji with the given `[shortcode]@[domain]` will not be included in the result set. +// in: query +// - +// name: min_shortcode_domain +// type: string +// description: >- +// Return only emojis with `[shortcode]@[domain]` *HIGHER* (alphabetically) than given `[shortcode]@[domain]`. +// For example, if `max_shortcode_domain=beep@example.org`, then returned values might include emojis with +// `[shortcode]@[domain]`s like `arse@test.com`, `0101_binary@hackers.net`, `bee@` (local emoji), etc. +// +// Emoji with the given `[shortcode]@[domain]` will not be included in the result set. +// in: query +// +// responses: +// '200': +// headers: +// Link: +// type: string +// description: Links to the next and previous queries. +// description: An array of emojis, arranged alphabetically by shortcode and domain. +// schema: +// type: array +// items: +// "$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) EmojisGETHandler(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 + } + + maxShortcodeDomain := c.Query(MaxShortcodeDomainKey) + minShortcodeDomain := c.Query(MinShortcodeDomainKey) + + limit := 50 + limitString := c.Query(LimitKey) + if limitString != "" { + i, err := strconv.ParseInt(limitString, 10, 64) + if err != nil { + err := fmt.Errorf("error parsing %s: %s", LimitKey, err) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + return + } + limit = int(i) + } + if limit < 0 { + limit = 0 + } + + var domain string + var includeDisabled bool + var includeEnabled bool + var shortcode string + if filterParam := c.Query(FilterQueryKey); filterParam != "" { + filters := strings.Split(filterParam, ",") + for _, filter := range filters { + lower := strings.ToLower(filter) + switch { + case strings.HasPrefix(lower, "domain:"): + domain = strings.TrimPrefix(lower, "domain:") + case lower == "disabled": + includeDisabled = true + case lower == "enabled": + includeEnabled = true + case strings.HasPrefix(lower, "shortcode:"): + shortcode = strings.Trim(filter[10:], ":") // remove any errant ":" + default: + err := fmt.Errorf("filter %s not recognized; accepted values are 'domain:[domain]', 'disabled', 'enabled', 'shortcode:[shortcode]'", filter) + api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) + return + } + } + } + + if domain == "" { + // default is to show all domains + domain = db.EmojiAllDomains + } else if domain == "local" || domain == config.GetHost() || domain == config.GetAccountDomain() { + // pass empty string for local domain + domain = "" + } + + // normalize filters + if !includeDisabled && !includeEnabled { + // include both if neither specified + includeDisabled = true + includeEnabled = true + } + + resp, errWithCode := m.processor.AdminEmojisGet(c.Request.Context(), authed, domain, includeDisabled, includeEnabled, shortcode, maxShortcodeDomain, minShortcodeDomain, limit) + if errWithCode != nil { + api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) + return + } + + if resp.LinkHeader != "" { + c.Header("Link", resp.LinkHeader) + } + c.JSON(http.StatusOK, resp.Items) +} diff --git a/internal/api/client/admin/emojisget_test.go b/internal/api/client/admin/emojisget_test.go new file mode 100644 index 000000000..07f0b49eb --- /dev/null +++ b/internal/api/client/admin/emojisget_test.go @@ -0,0 +1,114 @@ +/* + 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 . +*/ + +package admin_test + +import ( + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/suite" + "github.com/superseriousbusiness/gotosocial/internal/api/client/admin" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" +) + +type EmojisGetTestSuite struct { + AdminStandardTestSuite +} + +func (suite *EmojisGetTestSuite) TestEmojiGet() { + recorder := httptest.NewRecorder() + + path := admin.EmojiPath + "?filter=domain:all&limit=1" + ctx := suite.newContext(recorder, http.MethodGet, nil, path, "application/json") + + suite.adminModule.EmojisGETHandler(ctx) + suite.Equal(http.StatusOK, recorder.Code) + + b, err := io.ReadAll(recorder.Body) + suite.NoError(err) + suite.NotNil(b) + + apiEmojis := []*apimodel.AdminEmoji{} + if err := json.Unmarshal(b, &apiEmojis); err != nil { + suite.FailNow(err.Error()) + } + + suite.Len(apiEmojis, 1) + suite.Equal("rainbow", apiEmojis[0].Shortcode) + suite.Equal("", apiEmojis[0].Domain) + + suite.Equal(`; rel="next", ; rel="prev"`, recorder.Header().Get("link")) +} + +func (suite *EmojisGetTestSuite) TestEmojiGet2() { + recorder := httptest.NewRecorder() + + path := admin.EmojiPath + "?filter=domain:all&limit=1&max_shortcode_domain=rainbow@" + ctx := suite.newContext(recorder, http.MethodGet, nil, path, "application/json") + + suite.adminModule.EmojisGETHandler(ctx) + suite.Equal(http.StatusOK, recorder.Code) + + b, err := io.ReadAll(recorder.Body) + suite.NoError(err) + suite.NotNil(b) + + apiEmojis := []*apimodel.AdminEmoji{} + if err := json.Unmarshal(b, &apiEmojis); err != nil { + suite.FailNow(err.Error()) + } + + suite.Len(apiEmojis, 1) + suite.Equal("yell", apiEmojis[0].Shortcode) + suite.Equal("fossbros-anonymous.io", apiEmojis[0].Domain) + + suite.Equal(`; rel="next", ; rel="prev"`, recorder.Header().Get("link")) +} + +func (suite *EmojisGetTestSuite) TestEmojiGet3() { + recorder := httptest.NewRecorder() + + path := admin.EmojiPath + "?filter=domain:all&limit=1&min_shortcode_domain=yell@fossbros-anonymous.io" + ctx := suite.newContext(recorder, http.MethodGet, nil, path, "application/json") + + suite.adminModule.EmojisGETHandler(ctx) + suite.Equal(http.StatusOK, recorder.Code) + + b, err := io.ReadAll(recorder.Body) + suite.NoError(err) + suite.NotNil(b) + + apiEmojis := []*apimodel.AdminEmoji{} + if err := json.Unmarshal(b, &apiEmojis); err != nil { + suite.FailNow(err.Error()) + } + + suite.Len(apiEmojis, 1) + suite.Equal("rainbow", apiEmojis[0].Shortcode) + suite.Equal("", apiEmojis[0].Domain) + + suite.Equal(`; rel="next", ; rel="prev"`, recorder.Header().Get("link")) +} + +func TestEmojisGetTestSuite(t *testing.T) { + suite.Run(t, &EmojisGetTestSuite{}) +} diff --git a/internal/processing/admin.go b/internal/processing/admin.go index 59a4f8f1b..0ebce4d4e 100644 --- a/internal/processing/admin.go +++ b/internal/processing/admin.go @@ -38,6 +38,10 @@ func (p *processor) AdminEmojisGet(ctx context.Context, authed *oauth.Auth, doma return p.adminProcessor.EmojisGet(ctx, authed.Account, authed.User, domain, includeDisabled, includeEnabled, shortcode, maxShortcodeDomain, minShortcodeDomain, limit) } +func (p *processor) AdminEmojiGet(ctx context.Context, authed *oauth.Auth, id string) (*apimodel.AdminEmoji, gtserror.WithCode) { + return p.adminProcessor.EmojiGet(ctx, authed.Account, authed.User, 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, "") } diff --git a/internal/processing/admin/admin.go b/internal/processing/admin/admin.go index 0de165fb9..49c02d3db 100644 --- a/internal/processing/admin/admin.go +++ b/internal/processing/admin/admin.go @@ -42,6 +42,7 @@ type Processor interface { AccountAction(ctx context.Context, account *gtsmodel.Account, form *apimodel.AdminAccountActionRequest) gtserror.WithCode 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) MediaPrune(ctx context.Context, mediaRemoteCacheDays int) gtserror.WithCode } diff --git a/internal/processing/admin/getemoji.go b/internal/processing/admin/getemoji.go new file mode 100644 index 000000000..ee3f997de --- /dev/null +++ b/internal/processing/admin/getemoji.go @@ -0,0 +1,54 @@ +/* + 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 . +*/ + +package admin + +import ( + "context" + "errors" + "fmt" + + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +func (p *processor) EmojiGet(ctx context.Context, account *gtsmodel.Account, user *gtsmodel.User, id string) (*apimodel.AdminEmoji, gtserror.WithCode) { + if !*user.Admin { + return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("user %s not an admin", user.ID), "user is not an admin") + } + + emoji, err := p.db.GetEmojiByID(ctx, id) + if err != nil { + if errors.Is(err, db.ErrNoEntries) { + err = fmt.Errorf("EmojiGet: no emoji with id %s found in the db", id) + return nil, gtserror.NewErrorNotFound(err) + } + err := fmt.Errorf("EmojiGet: db error: %s", err) + return nil, gtserror.NewErrorInternalError(err) + } + + adminEmoji, err := p.tc.EmojiToAdminAPIEmoji(ctx, emoji) + if err != nil { + err = fmt.Errorf("EmojiGet: error converting emoji to admin api emoji: %s", err) + return nil, gtserror.NewErrorInternalError(err) + } + + return adminEmoji, nil +} diff --git a/internal/processing/processor.go b/internal/processing/processor.go index b616511ea..ff465c926 100644 --- a/internal/processing/processor.go +++ b/internal/processing/processor.go @@ -114,6 +114,8 @@ type Processor interface { AdminEmojiCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, gtserror.WithCode) // AdminEmojisGet allows admins to view emojis based on various filters. AdminEmojisGet(ctx context.Context, authed *oauth.Auth, domain string, includeDisabled bool, includeEnabled bool, shortcode string, maxShortcodeDomain string, minShortcodeDomain string, limit int) (*apimodel.PageableResponse, gtserror.WithCode) + // AdminEmojiGet returns the admin view of an emoji with the given ID + AdminEmojiGet(ctx context.Context, authed *oauth.Auth, id string) (*apimodel.AdminEmoji, gtserror.WithCode) // AdminDomainBlockCreate handles the creation of a new domain block by an admin, using the given form. AdminDomainBlockCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.DomainBlockCreateRequest) (*apimodel.DomainBlock, gtserror.WithCode) // AdminDomainBlocksImport handles the import of multiple domain blocks by an admin, using the given form.