commit eb85ef7325300727bf69f3ce620d4362f983b2e7
parent 5cd087241b841d14037424930e2dd498eca536b4
Author: tobi <31960611+tsmethurst@users.noreply.github.com>
Date: Wed, 12 Oct 2022 15:01:42 +0200
[feature] Add `/api/v1/admin/custom_emojis` endpoint (#902)
* add admin emojis get path + model + docs
* stub admin emojis get processor function
* add id + disabled fields to admin emoji
* add emoji -> api admin emoji converter
* tidy up a bit
* add GetEmojis function
* finish up get emojis function
* order by shortcodedomain
* ASC
* tidy up + explain
* update to allow paging
* make admin emojis pageable
* fix mixed case paging
* normalize emoji queries a bit better
* test emoji get paging
* make limit optional
* fix incorrect path in media cleanup tests
* i have bad coder syndrome
* don't trimspace
* rename -> GetUseableEmojis
* wrap emoji query in subquery
avoid selecting more than we need
* fix a bit of sillyness teehee
* fix subquery postgres woes
Diffstat:
18 files changed, 887 insertions(+), 9 deletions(-)
diff --git a/docs/api/swagger.yaml b/docs/api/swagger.yaml
@@ -407,6 +407,73 @@ definitions:
type: object
x-go-name: Relationship
x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
+ adminEmoji:
+ properties:
+ category:
+ description: Used for sorting custom emoji in the picker.
+ example: blobcats
+ type: string
+ x-go-name: Category
+ content_type:
+ description: The MIME content type of the emoji.
+ example: image/png
+ type: string
+ x-go-name: ContentType
+ disabled:
+ description: True if this emoji has been disabled by an admin action.
+ example: false
+ type: boolean
+ x-go-name: Disabled
+ domain:
+ description: The domain from which the emoji originated. Only defined for remote domains, otherwise key will not be set.
+ example: example.org
+ type: string
+ x-go-name: Domain
+ id:
+ description: The ID of the emoji.
+ example: 01GEM7SFDZ7GZNRXFVZ3X4E4N1
+ type: string
+ x-go-name: ID
+ shortcode:
+ description: The name of the custom emoji.
+ example: blobcat_uwu
+ type: string
+ x-go-name: Shortcode
+ static_url:
+ description: A link to a static copy of the custom emoji.
+ example: https://example.org/fileserver/emojis/blogcat_uwu.png
+ type: string
+ x-go-name: StaticURL
+ total_file_size:
+ description: The total file size taken up by the emoji in bytes, including static and animated versions.
+ example: 69420
+ format: int64
+ type: integer
+ x-go-name: TotalFileSize
+ updated_at:
+ description: Time when the emoji image was last updated.
+ example: "2022-10-05T09:21:26.419Z"
+ type: string
+ x-go-name: UpdatedAt
+ uri:
+ description: The ActivityPub URI of the emoji.
+ example: https://example.org/emojis/016T5Q3SQKBT337DAKVSKNXXW1
+ type: string
+ x-go-name: URI
+ url:
+ description: Web URL of the custom emoji.
+ example: https://example.org/fileserver/emojis/blogcat_uwu.gif
+ type: string
+ x-go-name: URL
+ visible_in_picker:
+ description: Emoji is visible in the emoji picker of the instance.
+ example: true
+ type: boolean
+ x-go-name: VisibleInPicker
+ title: AdminEmoji models the admin view of a custom emoji.
+ type: object
+ x-go-name: AdminEmoji
+ x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
advancedStatusCreateForm:
description: |-
AdvancedStatusCreateForm wraps the mastodon-compatible status create form along with the GTS advanced
@@ -2677,6 +2744,80 @@ paths:
tags:
- admin
/api/v1/admin/custom_emojis:
+ get:
+ description: |-
+ The next and previous queries can be parsed from the returned Link header.
+ Example:
+
+ `<http://localhost:8080/api/v1/admin/custom_emojis?limit=30&max_shortcode_domain=yell@fossbros-anonymous.io&filter=domain:all>; rel="next", <http://localhost:8080/api/v1/admin/custom_emojis?limit=30&min_shortcode_domain=rainbow@&filter=domain:all>; rel="prev"`
+ operationId: emojisGet
+ parameters:
+ - default: domain:all
+ 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
+ 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.
+ in: query
+ name: limit
+ type: integer
+ - 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: max_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
+ name: min_shortcode_domain
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: An array of emojis, arranged alphabetically by shortcode and domain.
+ headers:
+ Link:
+ description: Links to the next and previous queries.
+ type: string
+ schema:
+ items:
+ $ref: '#/definitions/adminEmoji'
+ type: array
+ "400":
+ description: bad request
+ "401":
+ description: unauthorized
+ "403":
+ description: forbidden
+ "404":
+ description: not found
+ "406":
+ description: not acceptable
+ "500":
+ description: internal server error
+ summary: View local and remote emojis available to / known by this instance.
+ tags:
+ - admin
post:
consumes:
- multipart/form-data
diff --git a/internal/api/client/admin/admin.go b/internal/api/client/admin/admin.go
@@ -31,6 +31,8 @@ const (
BasePath = "/api/v1/admin"
// EmojiPath is used for posting/deleting custom emojis.
EmojiPath = BasePath + "/custom_emojis"
+ // EmojiPathWithID is used for interacting with a single emoji.
+ EmojiPathWithID = EmojiPath + "/:" + IDKey
// DomainBlocksPath is used for posting domain blocks.
DomainBlocksPath = BasePath + "/domain_blocks"
// DomainBlocksPathWithID is used for interacting with a single domain block.
@@ -49,6 +51,16 @@ const (
ImportQueryKey = "import"
// IDKey specifies the ID of a single item being interacted with.
IDKey = "id"
+ // FilterKey is for applying filters to admin views of accounts, emojis, etc.
+ FilterQueryKey = "filter"
+ // MaxShortcodeDomainKey is the url query for returning emoji results lower (alphabetically)
+ // than the given `[shortcode]@[domain]` parameter.
+ MaxShortcodeDomainKey = "max_shortcode_domain"
+ // MaxShortcodeDomainKey is the url query for returning emoji results higher (alphabetically)
+ // than the given `[shortcode]@[domain]` parameter.
+ MinShortcodeDomainKey = "min_shortcode_domain"
+ // LimitKey is for specifying maximum number of results to return.
+ LimitKey = "limit"
)
// Module implements the ClientAPIModule interface for admin-related actions (reports, emojis, etc)
@@ -66,6 +78,7 @@ func New(processor processing.Processor) api.ClientModule {
// Route attaches all routes from this module to the given router
func (m *Module) Route(r router.Router) error {
r.AttachHandler(http.MethodPost, EmojiPath, m.EmojiCreatePOSTHandler)
+ r.AttachHandler(http.MethodGet, EmojiPath, m.EmojisGETHandler)
r.AttachHandler(http.MethodPost, DomainBlocksPath, m.DomainBlocksPOSTHandler)
r.AttachHandler(http.MethodGet, DomainBlocksPath, m.DomainBlocksGETHandler)
r.AttachHandler(http.MethodGet, DomainBlocksPathWithID, m.DomainBlockGETHandler)
diff --git a/internal/api/client/admin/emojiget.go b/internal/api/client/admin/emojiget.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 <http://www.gnu.org/licenses/>.
+*/
+
+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:
+//
+// `<http://localhost:8080/api/v1/admin/custom_emojis?limit=30&max_shortcode_domain=yell@fossbros-anonymous.io&filter=domain:all>; rel="next", <http://localhost:8080/api/v1/admin/custom_emojis?limit=30&min_shortcode_domain=rainbow@&filter=domain:all>; 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/emojiget_test.go b/internal/api/client/admin/emojiget_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 <http://www.gnu.org/licenses/>.
+*/
+
+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 EmojiGetTestSuite struct {
+ AdminStandardTestSuite
+}
+
+func (suite *EmojiGetTestSuite) 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(`<http://localhost:8080/api/v1/admin/custom_emojis?limit=1&max_shortcode_domain=rainbow@&filter=domain:all>; rel="next", <http://localhost:8080/api/v1/admin/custom_emojis?limit=1&min_shortcode_domain=rainbow@&filter=domain:all>; rel="prev"`, recorder.Header().Get("link"))
+}
+
+func (suite *EmojiGetTestSuite) 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(`<http://localhost:8080/api/v1/admin/custom_emojis?limit=1&max_shortcode_domain=yell@fossbros-anonymous.io&filter=domain:all>; rel="next", <http://localhost:8080/api/v1/admin/custom_emojis?limit=1&min_shortcode_domain=yell@fossbros-anonymous.io&filter=domain:all>; rel="prev"`, recorder.Header().Get("link"))
+}
+
+func (suite *EmojiGetTestSuite) 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(`<http://localhost:8080/api/v1/admin/custom_emojis?limit=1&max_shortcode_domain=rainbow@&filter=domain:all>; rel="next", <http://localhost:8080/api/v1/admin/custom_emojis?limit=1&min_shortcode_domain=rainbow@&filter=domain:all>; rel="prev"`, recorder.Header().Get("link"))
+}
+
+func TestEmojiGetTestSuite(t *testing.T) {
+ suite.Run(t, &EmojiGetTestSuite{})
+}
diff --git a/internal/api/client/admin/mediacleanup_test.go b/internal/api/client/admin/mediacleanup_test.go
@@ -40,7 +40,7 @@ func (suite *MediaCleanupTestSuite) TestMediaCleanup() {
// set up the request
recorder := httptest.NewRecorder()
- ctx := suite.newContext(recorder, http.MethodPost, []byte("{\"remote_cache_days\": 1}"), admin.EmojiPath, "application/json")
+ ctx := suite.newContext(recorder, http.MethodPost, []byte("{\"remote_cache_days\": 1}"), admin.MediaCleanupPath, "application/json")
// call the handler
suite.adminModule.MediaCleanupPOSTHandler(ctx)
@@ -66,7 +66,7 @@ func (suite *MediaCleanupTestSuite) TestMediaCleanupNoArg() {
// set up the request
recorder := httptest.NewRecorder()
- ctx := suite.newContext(recorder, http.MethodPost, []byte("{}"), admin.EmojiPath, "application/json")
+ ctx := suite.newContext(recorder, http.MethodPost, []byte("{}"), admin.MediaCleanupPath, "application/json")
// call the handler
suite.adminModule.MediaCleanupPOSTHandler(ctx)
@@ -90,7 +90,7 @@ func (suite *MediaCleanupTestSuite) TestMediaCleanupNotOldEnough() {
// set up the request
recorder := httptest.NewRecorder()
- ctx := suite.newContext(recorder, http.MethodPost, []byte("{\"remote_cache_days\": 10000}"), admin.EmojiPath, "application/json")
+ ctx := suite.newContext(recorder, http.MethodPost, []byte("{\"remote_cache_days\": 10000}"), admin.MediaCleanupPath, "application/json")
// call the handler
suite.adminModule.MediaCleanupPOSTHandler(ctx)
diff --git a/internal/api/model/admin.go b/internal/api/model/admin.go
@@ -80,6 +80,35 @@ type AdminReportInfo struct {
Statuses []Status `json:"statuses"`
}
+// AdminEmoji models the admin view of a custom emoji.
+//
+// swagger:model adminEmoji
+type AdminEmoji struct {
+ Emoji
+ // The ID of the emoji.
+ // example: 01GEM7SFDZ7GZNRXFVZ3X4E4N1
+ ID string `json:"id"`
+ // True if this emoji has been disabled by an admin action.
+ // example: false
+ Disabled bool `json:"disabled"`
+ // The domain from which the emoji originated. Only defined for remote domains, otherwise key will not be set.
+ //
+ // example: example.org
+ Domain string `json:"domain,omitempty"`
+ // Time when the emoji image was last updated.
+ // example: 2022-10-05T09:21:26.419Z
+ UpdatedAt string `json:"updated_at"`
+ // The total file size taken up by the emoji in bytes, including static and animated versions.
+ // example: 69420
+ TotalFileSize int `json:"total_file_size"`
+ // The MIME content type of the emoji.
+ // example: image/png
+ ContentType string `json:"content_type"`
+ // The ActivityPub URI of the emoji.
+ // example: https://example.org/emojis/016T5Q3SQKBT337DAKVSKNXXW1
+ URI string `json:"uri"`
+}
+
// AdminAccountActionRequest models the admin view of an account's details.
//
// swagger:ignore
diff --git a/internal/db/bundb/emoji.go b/internal/db/bundb/emoji.go
@@ -27,6 +27,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/uptrace/bun"
+ "github.com/uptrace/bun/dialect"
)
type emojiDB struct {
@@ -49,7 +50,124 @@ func (e *emojiDB) PutEmoji(ctx context.Context, emoji *gtsmodel.Emoji) db.Error
return nil
}
-func (e *emojiDB) GetCustomEmojis(ctx context.Context) ([]*gtsmodel.Emoji, db.Error) {
+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{}
+
+ subQuery := e.conn.
+ NewSelect().
+ ColumnExpr("? AS ?", bun.Ident("emoji.id"), bun.Ident("emoji_ids"))
+
+ // To ensure consistent ordering and make paging possible, we sort not by shortcode
+ // but by [shortcode]@[domain]. Because sqlite and postgres have different syntax
+ // for concatenation, that means we need to switch here. Depending on which driver
+ // is in use, query will look something like this (sqlite):
+ //
+ // SELECT
+ // "emoji"."id" AS "emoji_ids",
+ // lower("emoji"."shortcode" || '@' || COALESCE("emoji"."domain", '')) AS "shortcode_domain"
+ // FROM
+ // "emojis" AS "emoji"
+ // ORDER BY
+ // "shortcode_domain" ASC
+ //
+ // Or like this (postgres):
+ //
+ // SELECT
+ // "emoji"."id" AS "emoji_ids",
+ // LOWER(CONCAT("emoji"."shortcode", '@', COALESCE("emoji"."domain", ''))) AS "shortcode_domain"
+ // FROM
+ // "emojis" AS "emoji"
+ // ORDER BY
+ // "shortcode_domain" ASC
+ switch e.conn.Dialect().Name() {
+ case dialect.SQLite:
+ subQuery = subQuery.ColumnExpr("LOWER(? || ? || COALESCE(?, ?)) AS ?", bun.Ident("emoji.shortcode"), "@", bun.Ident("emoji.domain"), "", bun.Ident("shortcode_domain"))
+ case dialect.PG:
+ subQuery = subQuery.ColumnExpr("LOWER(CONCAT(?, ?, COALESCE(?, ?))) AS ?", bun.Ident("emoji.shortcode"), "@", bun.Ident("emoji.domain"), "", bun.Ident("shortcode_domain"))
+ default:
+ panic("db conn was neither pg not sqlite")
+ }
+
+ subQuery = subQuery.TableExpr("? AS ?", bun.Ident("emojis"), bun.Ident("emoji"))
+
+ if domain == "" {
+ subQuery = subQuery.Where("? IS NULL", bun.Ident("emoji.domain"))
+ } else if domain != db.EmojiAllDomains {
+ subQuery = subQuery.Where("? = ?", bun.Ident("emoji.domain"), domain)
+ }
+
+ switch {
+ case includeDisabled && !includeEnabled:
+ // show only disabled emojis
+ subQuery = subQuery.Where("? = ?", bun.Ident("emoji.disabled"), true)
+ case includeEnabled && !includeDisabled:
+ // show only enabled emojis
+ subQuery = subQuery.Where("? = ?", bun.Ident("emoji.disabled"), false)
+ default:
+ // show emojis regardless of emoji.disabled value
+ }
+
+ if shortcode != "" {
+ subQuery = subQuery.Where("LOWER(?) = LOWER(?)", bun.Ident("emoji.shortcode"), shortcode)
+ }
+
+ // assume we want to sort ASC (a-z) unless informed otherwise
+ order := "ASC"
+
+ if maxShortcodeDomain != "" {
+ subQuery = subQuery.Where("? > LOWER(?)", bun.Ident("shortcode_domain"), maxShortcodeDomain)
+ }
+
+ if minShortcodeDomain != "" {
+ subQuery = subQuery.Where("? < LOWER(?)", bun.Ident("shortcode_domain"), minShortcodeDomain)
+ // if we have a minShortcodeDomain we're paging upwards/backwards
+ order = "DESC"
+ }
+
+ subQuery = subQuery.Order("shortcode_domain " + order)
+
+ if limit > 0 {
+ subQuery = subQuery.Limit(limit)
+ }
+
+ // Wrap the subQuery in a query, since we don't need to select the shortcode_domain column.
+ //
+ // The final query will come out looking something like...
+ //
+ // SELECT
+ // "subquery"."emoji_ids"
+ // FROM (
+ // SELECT
+ // "emoji"."id" AS "emoji_ids",
+ // LOWER("emoji"."shortcode" || '@' || COALESCE("emoji"."domain", '')) AS "shortcode_domain"
+ // FROM
+ // "emojis" AS "emoji"
+ // ORDER BY
+ // "shortcode_domain" ASC
+ // ) AS "subquery"
+ if err := e.conn.
+ NewSelect().
+ Column("subquery.emoji_ids").
+ TableExpr("(?) AS ?", subQuery, bun.Ident("subquery")).
+ Scan(ctx, &emojiIDs); err != nil {
+ return nil, e.conn.ProcessError(err)
+ }
+
+ if order == "DESC" {
+ // Reverse the slice order so the caller still
+ // gets emojis in expected a-z alphabetical order.
+ //
+ // See https://github.com/golang/go/wiki/SliceTricks#reversing
+ for i := len(emojiIDs)/2 - 1; i >= 0; i-- {
+ opp := len(emojiIDs) - 1 - i
+ emojiIDs[i], emojiIDs[opp] = emojiIDs[opp], emojiIDs[i]
+ }
+ }
+
+ return e.emojisFromIDs(ctx, emojiIDs)
+}
+
+func (e *emojiDB) GetUseableEmojis(ctx context.Context) ([]*gtsmodel.Emoji, db.Error) {
emojiIDs := []string{}
q := e.conn.
diff --git a/internal/db/bundb/emoji_test.go b/internal/db/bundb/emoji_test.go
@@ -23,20 +23,108 @@ import (
"testing"
"github.com/stretchr/testify/suite"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
)
type EmojiTestSuite struct {
BunDBStandardTestSuite
}
-func (suite *EmojiTestSuite) TestGetCustomEmojis() {
- emojis, err := suite.db.GetCustomEmojis(context.Background())
+func (suite *EmojiTestSuite) TestGetUseableEmojis() {
+ emojis, err := suite.db.GetUseableEmojis(context.Background())
suite.NoError(err)
suite.Equal(1, len(emojis))
suite.Equal("rainbow", emojis[0].Shortcode)
}
+func (suite *EmojiTestSuite) TestGetAllEmojis() {
+ emojis, err := suite.db.GetEmojis(context.Background(), db.EmojiAllDomains, true, true, "", "", "", 0)
+
+ suite.NoError(err)
+ suite.Equal(2, len(emojis))
+ suite.Equal("rainbow", emojis[0].Shortcode)
+ suite.Equal("yell", emojis[1].Shortcode)
+}
+
+func (suite *EmojiTestSuite) TestGetAllEmojisLimit1() {
+ emojis, err := suite.db.GetEmojis(context.Background(), db.EmojiAllDomains, true, true, "", "", "", 1)
+
+ suite.NoError(err)
+ suite.Equal(1, len(emojis))
+ suite.Equal("rainbow", emojis[0].Shortcode)
+}
+
+func (suite *EmojiTestSuite) TestGetAllEmojisMaxID() {
+ emojis, err := suite.db.GetEmojis(context.Background(), db.EmojiAllDomains, true, true, "", "rainbow@", "", 0)
+
+ suite.NoError(err)
+ suite.Equal(1, len(emojis))
+ suite.Equal("yell", emojis[0].Shortcode)
+}
+
+func (suite *EmojiTestSuite) TestGetAllEmojisMinID() {
+ emojis, err := suite.db.GetEmojis(context.Background(), db.EmojiAllDomains, true, true, "", "", "yell@fossbros-anonymous.io", 0)
+
+ suite.NoError(err)
+ suite.Equal(1, len(emojis))
+ suite.Equal("rainbow", emojis[0].Shortcode)
+}
+
+func (suite *EmojiTestSuite) TestGetAllDisabledEmojis() {
+ emojis, err := suite.db.GetEmojis(context.Background(), db.EmojiAllDomains, true, false, "", "", "", 0)
+
+ suite.ErrorIs(err, db.ErrNoEntries)
+ suite.Equal(0, len(emojis))
+}
+
+func (suite *EmojiTestSuite) TestGetAllEnabledEmojis() {
+ emojis, err := suite.db.GetEmojis(context.Background(), db.EmojiAllDomains, false, true, "", "", "", 0)
+
+ suite.NoError(err)
+ suite.Equal(2, len(emojis))
+ suite.Equal("rainbow", emojis[0].Shortcode)
+ suite.Equal("yell", emojis[1].Shortcode)
+}
+
+func (suite *EmojiTestSuite) TestGetLocalEnabledEmojis() {
+ emojis, err := suite.db.GetEmojis(context.Background(), "", false, true, "", "", "", 0)
+
+ suite.NoError(err)
+ suite.Equal(1, len(emojis))
+ suite.Equal("rainbow", emojis[0].Shortcode)
+}
+
+func (suite *EmojiTestSuite) TestGetLocalDisabledEmojis() {
+ emojis, err := suite.db.GetEmojis(context.Background(), "", true, false, "", "", "", 0)
+
+ suite.ErrorIs(err, db.ErrNoEntries)
+ suite.Equal(0, len(emojis))
+}
+
+func (suite *EmojiTestSuite) TestGetAllEmojisFromDomain() {
+ emojis, err := suite.db.GetEmojis(context.Background(), "peepee.poopoo", true, true, "", "", "", 0)
+
+ suite.ErrorIs(err, db.ErrNoEntries)
+ suite.Equal(0, len(emojis))
+}
+
+func (suite *EmojiTestSuite) TestGetAllEmojisFromDomain2() {
+ emojis, err := suite.db.GetEmojis(context.Background(), "fossbros-anonymous.io", true, true, "", "", "", 0)
+
+ suite.NoError(err)
+ suite.Equal(1, len(emojis))
+ suite.Equal("yell", emojis[0].Shortcode)
+}
+
+func (suite *EmojiTestSuite) TestGetSpecificEmojisFromDomain2() {
+ emojis, err := suite.db.GetEmojis(context.Background(), "fossbros-anonymous.io", true, true, "yell", "", "", 0)
+
+ suite.NoError(err)
+ suite.Equal(1, len(emojis))
+ suite.Equal("yell", emojis[0].Shortcode)
+}
+
func TestEmojiTestSuite(t *testing.T) {
suite.Run(t, new(EmojiTestSuite))
}
diff --git a/internal/db/emoji.go b/internal/db/emoji.go
@@ -24,12 +24,18 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
+// EmojiAllDomains can be used as the `domain` value in a GetEmojis
+// query to indicate that emojis from all domains should be returned.
+const EmojiAllDomains string = "all"
+
// Emoji contains functions for getting emoji in the database.
type Emoji interface {
// PutEmoji puts one emoji in the database.
PutEmoji(ctx context.Context, emoji *gtsmodel.Emoji) Error
- // GetCustomEmojis gets all custom emoji for the instance
- GetCustomEmojis(ctx context.Context) ([]*gtsmodel.Emoji, 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.
+ GetEmojis(ctx context.Context, domain string, includeDisabled bool, includeEnabled bool, shortcode string, maxShortcodeDomain string, minShortcodeDomain string, limit int) ([]*gtsmodel.Emoji, Error)
// GetEmojiByID gets a specific emoji by its database ID.
GetEmojiByID(ctx context.Context, id string) (*gtsmodel.Emoji, Error)
// GetEmojiByShortcodeDomain gets an emoji based on its shortcode and domain.
diff --git a/internal/processing/admin.go b/internal/processing/admin.go
@@ -34,6 +34,10 @@ func (p *processor) AdminEmojiCreate(ctx context.Context, authed *oauth.Auth, fo
return p.adminProcessor.EmojiCreate(ctx, authed.Account, authed.User, form)
}
+func (p *processor) 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) {
+ return p.adminProcessor.EmojisGet(ctx, authed.Account, authed.User, domain, includeDisabled, includeEnabled, shortcode, maxShortcodeDomain, minShortcodeDomain, limit)
+}
+
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
@@ -41,6 +41,7 @@ type Processor interface {
DomainBlockDelete(ctx context.Context, account *gtsmodel.Account, id string) (*apimodel.DomainBlock, gtserror.WithCode)
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)
MediaPrune(ctx context.Context, mediaRemoteCacheDays int) gtserror.WithCode
}
diff --git a/internal/processing/admin/emoji.go b/internal/processing/admin/createemoji.go
diff --git a/internal/processing/admin/getemojis.go b/internal/processing/admin/getemojis.go
@@ -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
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "strings"
+
+ 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"
+ "github.com/superseriousbusiness/gotosocial/internal/util"
+)
+
+func (p *processor) 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) {
+ if !*user.Admin {
+ return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("user %s not an admin", user.ID), "user is not an admin")
+ }
+
+ emojis, err := p.db.GetEmojis(ctx, domain, includeDisabled, includeEnabled, shortcode, maxShortcodeDomain, minShortcodeDomain, limit)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := fmt.Errorf("EmojisGet: db error: %s", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ count := len(emojis)
+ if count == 0 {
+ return util.EmptyPageableResponse(), nil
+ }
+
+ items := make([]interface{}, 0, count)
+ for _, emoji := range emojis {
+ adminEmoji, err := p.tc.EmojiToAdminAPIEmoji(ctx, emoji)
+ if err != nil {
+ err := fmt.Errorf("EmojisGet: error converting emoji to admin model emoji: %s", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+ items = append(items, adminEmoji)
+ }
+
+ filterBuilder := strings.Builder{}
+ filterBuilder.WriteString("filter=")
+
+ switch domain {
+ case "", "local":
+ filterBuilder.WriteString("domain:local")
+ case db.EmojiAllDomains:
+ filterBuilder.WriteString("domain:all")
+ default:
+ filterBuilder.WriteString("domain:")
+ filterBuilder.WriteString(domain)
+ }
+
+ if includeDisabled != includeEnabled {
+ if includeDisabled {
+ filterBuilder.WriteString(",disabled")
+ }
+ if includeEnabled {
+ filterBuilder.WriteString(",enabled")
+ }
+ }
+
+ if shortcode != "" {
+ filterBuilder.WriteString(",shortcode:")
+ filterBuilder.WriteString(shortcode)
+ }
+
+ return util.PackagePageableResponse(util.PageableResponseParams{
+ Items: items,
+ Path: "api/v1/admin/custom_emojis",
+ NextMaxIDKey: "max_shortcode_domain",
+ NextMaxIDValue: shortcodeDomain(emojis[count-1]),
+ PrevMinIDKey: "min_shortcode_domain",
+ PrevMinIDValue: shortcodeDomain(emojis[0]),
+ Limit: limit,
+ ExtraQueryParams: []string{filterBuilder.String()},
+ })
+}
+
+func shortcodeDomain(emoji *gtsmodel.Emoji) string {
+ return emoji.Shortcode + "@" + emoji.Domain
+}
diff --git a/internal/processing/media/getemoji.go b/internal/processing/media/getemoji.go
@@ -29,7 +29,7 @@ import (
)
func (p *processor) GetCustomEmojis(ctx context.Context) ([]*apimodel.Emoji, gtserror.WithCode) {
- emojis, err := p.db.GetCustomEmojis(ctx)
+ emojis, err := p.db.GetUseableEmojis(ctx)
if err != nil {
if err != db.ErrNoEntries {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("db error retrieving custom emojis: %s", err))
diff --git a/internal/processing/processor.go b/internal/processing/processor.go
@@ -112,6 +112,8 @@ type Processor interface {
AdminAccountAction(ctx context.Context, authed *oauth.Auth, form *apimodel.AdminAccountActionRequest) gtserror.WithCode
// AdminEmojiCreate handles the creation of a new instance emoji by an admin, using the given form.
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)
// 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.
diff --git a/internal/typeutils/converter.go b/internal/typeutils/converter.go
@@ -67,6 +67,8 @@ type TypeConverter interface {
MentionToAPIMention(ctx context.Context, m *gtsmodel.Mention) (model.Mention, error)
// EmojiToAPIEmoji converts a gts model emoji into its api (frontend) representation for serialization on the API.
EmojiToAPIEmoji(ctx context.Context, e *gtsmodel.Emoji) (model.Emoji, error)
+ // EmojiToAdminAPIEmoji converts a gts model emoji into an API representation with extra admin information.
+ EmojiToAdminAPIEmoji(ctx context.Context, e *gtsmodel.Emoji) (*model.AdminEmoji, error)
// TagToAPITag converts a gts model tag into its api (frontend) representation for serialization on the API.
TagToAPITag(ctx context.Context, t *gtsmodel.Tag) (model.Tag, error)
// StatusToAPIStatus converts a gts model status into its api (frontend) representation for serialization on the API.
diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go
@@ -363,6 +363,24 @@ func (c *converter) EmojiToAPIEmoji(ctx context.Context, e *gtsmodel.Emoji) (mod
}, nil
}
+func (c *converter) EmojiToAdminAPIEmoji(ctx context.Context, e *gtsmodel.Emoji) (*model.AdminEmoji, error) {
+ emoji, err := c.EmojiToAPIEmoji(ctx, e)
+ if err != nil {
+ return nil, err
+ }
+
+ return &model.AdminEmoji{
+ Emoji: emoji,
+ ID: e.ID,
+ Disabled: *e.Disabled,
+ Domain: e.Domain,
+ UpdatedAt: util.FormatISO8601(e.UpdatedAt),
+ TotalFileSize: e.ImageFileSize + e.ImageStaticFileSize,
+ ContentType: e.ImageContentType,
+ URI: e.URI,
+ }, nil
+}
+
func (c *converter) TagToAPITag(ctx context.Context, t *gtsmodel.Tag) (model.Tag, error) {
return model.Tag{
Name: t.Name,
diff --git a/internal/typeutils/internaltofrontend_test.go b/internal/typeutils/internaltofrontend_test.go
@@ -141,6 +141,36 @@ func (suite *InternalToFrontendTestSuite) TestInstanceToFrontendWithAdminAccount
suite.Equal(`{"uri":"https://example.org","title":"example instance","description":"a much longer description","short_description":"a little description","email":"someone@example.org","version":"software-from-hell 0.666","registrations":false,"approval_required":false,"invites_enabled":false,"thumbnail":"","contact_account":{"id":"01FHMQX3GAABWSM0S2VZEC2SWC","username":"some_user","acct":"some_user@example.org","display_name":"some user","locked":true,"bot":false,"created_at":"2020-08-10T12:13:28.000Z","note":"i'm a real son of a gun","url":"http://example.org/@some_user","avatar":"","avatar_static":"","header":"http://localhost:8080/assets/default_header.png","header_static":"http://localhost:8080/assets/default_header.png","followers_count":0,"following_count":0,"statuses_count":0,"last_status_at":"","emojis":[],"fields":[]},"max_toot_chars":0}`, string(b))
}
+func (suite *InternalToFrontendTestSuite) TestEmojiToFrontend() {
+ emoji, err := suite.typeconverter.EmojiToAPIEmoji(context.Background(), suite.testEmojis["rainbow"])
+ suite.NoError(err)
+
+ b, err := json.Marshal(emoji)
+ suite.NoError(err)
+
+ 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}`, string(b))
+}
+
+func (suite *InternalToFrontendTestSuite) TestEmojiToFrontendAdmin1() {
+ emoji, err := suite.typeconverter.EmojiToAdminAPIEmoji(context.Background(), suite.testEmojis["rainbow"])
+ suite.NoError(err)
+
+ b, err := json.Marshal(emoji)
+ suite.NoError(err)
+
+ 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 *InternalToFrontendTestSuite) TestEmojiToFrontendAdmin2() {
+ emoji, err := suite.typeconverter.EmojiToAdminAPIEmoji(context.Background(), suite.testEmojis["yell"])
+ suite.NoError(err)
+
+ b, err := json.Marshal(emoji)
+ suite.NoError(err)
+
+ 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 TestInternalToFrontendTestSuite(t *testing.T) {
suite.Run(t, new(InternalToFrontendTestSuite))
}