diff options
| -rw-r--r-- | docs/api/swagger.yaml | 141 | ||||
| -rw-r--r-- | internal/api/client/admin/admin.go | 13 | ||||
| -rw-r--r-- | internal/api/client/admin/emojiget.go | 211 | ||||
| -rw-r--r-- | internal/api/client/admin/emojiget_test.go | 114 | ||||
| -rw-r--r-- | internal/api/client/admin/mediacleanup_test.go | 6 | ||||
| -rw-r--r-- | internal/api/model/admin.go | 29 | ||||
| -rw-r--r-- | internal/db/bundb/emoji.go | 120 | ||||
| -rw-r--r-- | internal/db/bundb/emoji_test.go | 92 | ||||
| -rw-r--r-- | internal/db/emoji.go | 10 | ||||
| -rw-r--r-- | internal/processing/admin.go | 4 | ||||
| -rw-r--r-- | internal/processing/admin/admin.go | 1 | ||||
| -rw-r--r-- | internal/processing/admin/createemoji.go (renamed from internal/processing/admin/emoji.go) | 0 | ||||
| -rw-r--r-- | internal/processing/admin/getemojis.go | 101 | ||||
| -rw-r--r-- | internal/processing/media/getemoji.go | 2 | ||||
| -rw-r--r-- | internal/processing/processor.go | 2 | ||||
| -rw-r--r-- | internal/typeutils/converter.go | 2 | ||||
| -rw-r--r-- | internal/typeutils/internaltofrontend.go | 18 | ||||
| -rw-r--r-- | internal/typeutils/internaltofrontend_test.go | 30 | 
18 files changed, 887 insertions, 9 deletions
diff --git a/docs/api/swagger.yaml b/docs/api/swagger.yaml index e09d07f14..5330d080f 100644 --- 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 index 2044c4ab0..e8aac0dee 100644 --- 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 new file mode 100644 index 000000000..7c44f45d4 --- /dev/null +++ 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 new file mode 100644 index 000000000..bba5561af --- /dev/null +++ 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 index 039bb7598..d2713084e 100644 --- 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 index 023ba42b1..e5c956e0d 100644 --- 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 index e781e2f00..640e354c4 100644 --- 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 index 0a1546d91..3c61fb620 100644 --- 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 index 374fd7b12..4316a43ef 100644 --- 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 index cbbea05b1..59a4f8f1b 100644 --- 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 index c528f0fb8..0de165fb9 100644 --- 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 index 50399279c..50399279c 100644 --- 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 new file mode 100644 index 000000000..d44b4d250 --- /dev/null +++ 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 index ee33c25eb..83a75eb66 100644 --- 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 index c76b1623b..b616511ea 100644 --- 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 index b1a771458..1ad7264ed 100644 --- 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 index 09bd5fc7d..7b4c3e8cc 100644 --- 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 index 9dd8ed4e3..f2514e719 100644 --- 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))  }  | 
