diff options
Diffstat (limited to 'internal/api/client/admin')
| -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 | 
4 files changed, 341 insertions, 3 deletions
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)  | 
