diff options
| author | 2023-07-07 11:34:12 +0200 | |
|---|---|---|
| committer | 2023-07-07 11:34:12 +0200 | |
| commit | e70bf8a6c82e3d5c943550b364fc6f8120f6f07e (patch) | |
| tree | f408ccff2e6f2451bf95ee9a5d96e5b678d686d5 /internal/api | |
| parent | [chore/performance] Remove remaining 'whereEmptyOrNull' funcs (#1946) (diff) | |
| download | gotosocial-e70bf8a6c82e3d5c943550b364fc6f8120f6f07e.tar.xz | |
[chore/bugfix] Domain block tidying up, Implement first pass of `207 Multi-Status` (#1886)
* [chore/refactor] update domain block processing
* expose domain block import errors a lil better
* move/remove unused query keys
Diffstat (limited to 'internal/api')
| -rw-r--r-- | internal/api/client/admin/admin.go | 2 | ||||
| -rw-r--r-- | internal/api/client/admin/domainblockcreate.go | 71 | ||||
| -rw-r--r-- | internal/api/client/admin/domainblockget.go | 25 | ||||
| -rw-r--r-- | internal/api/client/admin/domainblocksget.go | 15 | ||||
| -rw-r--r-- | internal/api/model/multistatus.go | 90 | ||||
| -rw-r--r-- | internal/api/util/parsequery.go | 132 | 
6 files changed, 210 insertions, 125 deletions
| diff --git a/internal/api/client/admin/admin.go b/internal/api/client/admin/admin.go index 4079dd979..a6c825b2b 100644 --- a/internal/api/client/admin/admin.go +++ b/internal/api/client/admin/admin.go @@ -42,8 +42,6 @@ const (  	EmailPath              = BasePath + "/email"  	EmailTestPath          = EmailPath + "/test" -	ExportQueryKey        = "export" -	ImportQueryKey        = "import"  	IDKey                 = "id"  	FilterQueryKey        = "filter"  	MaxShortcodeDomainKey = "max_shortcode_domain" diff --git a/internal/api/client/admin/domainblockcreate.go b/internal/api/client/admin/domainblockcreate.go index 5177cb03b..148fad7c9 100644 --- a/internal/api/client/admin/domainblockcreate.go +++ b/internal/api/client/admin/domainblockcreate.go @@ -21,7 +21,6 @@ import (  	"errors"  	"fmt"  	"net/http" -	"strconv"  	"github.com/gin-gonic/gin"  	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" @@ -140,48 +139,78 @@ func (m *Module) DomainBlocksPOSTHandler(c *gin.Context) {  		return  	} -	imp := false -	importString := c.Query(ImportQueryKey) -	if importString != "" { -		i, err := strconv.ParseBool(importString) -		if err != nil { -			err := fmt.Errorf("error parsing %s: %s", ImportQueryKey, err) -			apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1) -			return -		} -		imp = i +	importing, errWithCode := apiutil.ParseDomainBlockImport(c.Query(apiutil.DomainBlockImportKey), false) +	if errWithCode != nil { +		apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) +		return  	} -	form := &apimodel.DomainBlockCreateRequest{} +	form := new(apimodel.DomainBlockCreateRequest)  	if err := c.ShouldBind(form); err != nil {  		apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)  		return  	} -	if err := validateCreateDomainBlock(form, imp); err != nil { -		err := fmt.Errorf("error validating form: %s", err) +	if err := validateCreateDomainBlock(form, importing); err != nil { +		err := fmt.Errorf("error validating form: %w", err)  		apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)  		return  	} -	if imp { -		// we're importing multiple blocks -		domainBlocks, errWithCode := m.processor.Admin().DomainBlocksImport(c.Request.Context(), authed.Account, form.Domains) +	if !importing { +		// Single domain block creation. +		domainBlock, errWithCode := m.processor.Admin().DomainBlockCreate( +			c.Request.Context(), +			authed.Account, +			form.Domain, +			form.Obfuscate, +			form.PublicComment, +			form.PrivateComment, +			"", // No sub ID for single block creation. +		)  		if errWithCode != nil {  			apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)  			return  		} -		c.JSON(http.StatusOK, domainBlocks) + +		c.JSON(http.StatusOK, domainBlock)  		return  	} -	// we're just creating one block -	domainBlock, errWithCode := m.processor.Admin().DomainBlockCreate(c.Request.Context(), authed.Account, form.Domain, form.Obfuscate, form.PublicComment, form.PrivateComment, "") +	// We're importing multiple domain blocks, +	// so we're looking at a multi-status response. +	multiStatus, errWithCode := m.processor.Admin().DomainBlocksImport( +		c.Request.Context(), +		authed.Account, +		form.Domains, // Pass the file through. +	)  	if errWithCode != nil {  		apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)  		return  	} -	c.JSON(http.StatusOK, domainBlock) + +	// TODO: Return 207 and multiStatus data nicely +	//       when supported by the admin panel. + +	if multiStatus.Metadata.Failure != 0 { +		failures := make(map[string]any, multiStatus.Metadata.Failure) +		for _, entry := range multiStatus.Data { +			// nolint:forcetypeassert +			failures[entry.Resource.(string)] = entry.Message +		} + +		err := fmt.Errorf("one or more errors importing domain blocks: %+v", failures) +		apiutil.ErrorHandler(c, gtserror.NewErrorUnprocessableEntity(err, err.Error()), m.processor.InstanceGetV1) +		return +	} + +	// Success, return slice of domain blocks. +	domainBlocks := make([]any, 0, multiStatus.Metadata.Success) +	for _, entry := range multiStatus.Data { +		domainBlocks = append(domainBlocks, entry.Resource) +	} + +	c.JSON(http.StatusOK, domainBlocks)  }  func validateCreateDomainBlock(form *apimodel.DomainBlockCreateRequest, imp bool) error { diff --git a/internal/api/client/admin/domainblockget.go b/internal/api/client/admin/domainblockget.go index 335faed90..87bb75a27 100644 --- a/internal/api/client/admin/domainblockget.go +++ b/internal/api/client/admin/domainblockget.go @@ -18,10 +18,8 @@  package admin  import ( -	"errors"  	"fmt"  	"net/http" -	"strconv"  	"github.com/gin-gonic/gin"  	apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" @@ -87,26 +85,19 @@ func (m *Module) DomainBlockGETHandler(c *gin.Context) {  		return  	} -	domainBlockID := c.Param(IDKey) -	if domainBlockID == "" { -		err := errors.New("no domain block id specified") -		apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1) +	domainBlockID, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey)) +	if errWithCode != nil { +		apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)  		return  	} -	export := false -	exportString := c.Query(ExportQueryKey) -	if exportString != "" { -		i, err := strconv.ParseBool(exportString) -		if err != nil { -			err := fmt.Errorf("error parsing %s: %s", ExportQueryKey, err) -			apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1) -			return -		} -		export = i +	export, errWithCode := apiutil.ParseDomainBlockExport(c.Query(apiutil.DomainBlockExportKey), false) +	if errWithCode != nil { +		apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) +		return  	} -	domainBlock, errWithCode := m.processor.Admin().DomainBlockGet(c.Request.Context(), authed.Account, domainBlockID, export) +	domainBlock, errWithCode := m.processor.Admin().DomainBlockGet(c.Request.Context(), domainBlockID, export)  	if errWithCode != nil {  		apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)  		return diff --git a/internal/api/client/admin/domainblocksget.go b/internal/api/client/admin/domainblocksget.go index d641fc0e1..68947f471 100644 --- a/internal/api/client/admin/domainblocksget.go +++ b/internal/api/client/admin/domainblocksget.go @@ -20,7 +20,6 @@ package admin  import (  	"fmt"  	"net/http" -	"strconv"  	"github.com/gin-gonic/gin"  	apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" @@ -92,16 +91,10 @@ func (m *Module) DomainBlocksGETHandler(c *gin.Context) {  		return  	} -	export := false -	exportString := c.Query(ExportQueryKey) -	if exportString != "" { -		i, err := strconv.ParseBool(exportString) -		if err != nil { -			err := fmt.Errorf("error parsing %s: %s", ExportQueryKey, err) -			apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1) -			return -		} -		export = i +	export, errWithCode := apiutil.ParseDomainBlockExport(c.Query(apiutil.DomainBlockExportKey), false) +	if errWithCode != nil { +		apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) +		return  	}  	domainBlocks, errWithCode := m.processor.Admin().DomainBlocksGet(c.Request.Context(), authed.Account, export) diff --git a/internal/api/model/multistatus.go b/internal/api/model/multistatus.go new file mode 100644 index 000000000..cac8b4f9b --- /dev/null +++ b/internal/api/model/multistatus.go @@ -0,0 +1,90 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program.  If not, see <http://www.gnu.org/licenses/>. + +package model + +// MultiStatus models a multistatus HTTP response body. +// This model should be transmitted along with http code +// 207 MULTI-STATUS to indicate a mixture of responses. +// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/207 +// +// swagger:model multiStatus +type MultiStatus struct { +	Data     []MultiStatusEntry  `json:"data"` +	Metadata MultiStatusMetadata `json:"metadata"` +} + +// MultiStatusEntry models one entry in multistatus data. +// It can model either a success or a failure. The type +// and value of `Resource` is left to the discretion of +// the caller, but at minimum it should be expected to be +// JSON-serializable. +// +// swagger:model multiStatusEntry +type MultiStatusEntry struct { +	// The resource/result for this entry. +	// Value may be any type, check the docs +	// per endpoint to see which to expect. +	Resource any `json:"resource"` +	// Message/error message for this entry. +	Message string `json:"message"` +	// HTTP status code of this entry. +	Status int `json:"status"` +} + +// MultiStatusMetadata models an at-a-glance summary of +// the data contained in the MultiStatus. +// +// swagger:model multiStatusMetadata +type MultiStatusMetadata struct { +	// Success count + failure count. +	Total int `json:"total"` +	// Count of successful results (2xx). +	Success int `json:"success"` +	// Count of unsuccessful results (!2xx). +	Failure int `json:"failure"` +} + +// NewMultiStatus returns a new MultiStatus API model with +// the provided entries, which will be iterated through to +// look for 2xx and non 2xx status codes, in order to count +// successes and failures. +func NewMultiStatus(entries []MultiStatusEntry) *MultiStatus { +	var ( +		successCount int +		failureCount int +		total        = len(entries) +	) + +	for _, e := range entries { +		// Outside 2xx range = failure. +		if e.Status > 299 || e.Status < 200 { +			failureCount++ +		} else { +			successCount++ +		} +	} + +	return &MultiStatus{ +		Data: entries, +		Metadata: MultiStatusMetadata{ +			Total:   total, +			Success: successCount, +			Failure: failureCount, +		}, +	} +} diff --git a/internal/api/util/parsequery.go b/internal/api/util/parsequery.go index 460ca3e05..f5966bca1 100644 --- a/internal/api/util/parsequery.go +++ b/internal/api/util/parsequery.go @@ -27,6 +27,7 @@ import (  const (  	/* Common keys */ +	IDKey    = "id"  	LimitKey = "limit"  	LocalKey = "local"  	MaxIDKey = "max_id" @@ -41,6 +42,11 @@ const (  	SearchQueryKey             = "q"  	SearchResolveKey           = "resolve"  	SearchTypeKey              = "type" + +	/* Domain block keys */ + +	DomainBlockExportKey = "export" +	DomainBlockImportKey = "import"  )  // parseError returns gtserror.WithCode set to 400 Bad Request, to indicate @@ -50,6 +56,8 @@ func parseError(key string, value, defaultValue any, err error) gtserror.WithCod  	return gtserror.NewErrorBadRequest(err, err.Error())  } +// requiredError returns gtserror.WithCode set to 400 Bad Request, to indicate +// to the caller a required key value was not provided, or was empty.  func requiredError(key string) gtserror.WithCode {  	err := fmt.Errorf("required key %s was not set or had empty value", key)  	return gtserror.NewErrorBadRequest(err, err.Error()) @@ -60,59 +68,76 @@ func requiredError(key string) gtserror.WithCode {  */  func ParseLimit(value string, defaultValue int, max, min int) (int, gtserror.WithCode) { -	key := LimitKey +	return parseInt(value, defaultValue, max, min, LimitKey) +} -	if value == "" { -		return defaultValue, nil -	} +func ParseLocal(value string, defaultValue bool) (bool, gtserror.WithCode) { +	return parseBool(value, defaultValue, LocalKey) +} -	i, err := strconv.Atoi(value) -	if err != nil { -		return defaultValue, parseError(key, value, defaultValue, err) -	} +func ParseSearchExcludeUnreviewed(value string, defaultValue bool) (bool, gtserror.WithCode) { +	return parseBool(value, defaultValue, SearchExcludeUnreviewedKey) +} -	if i > max { -		i = max -	} else if i < min { -		i = min -	} +func ParseSearchFollowing(value string, defaultValue bool) (bool, gtserror.WithCode) { +	return parseBool(value, defaultValue, SearchFollowingKey) +} -	return i, nil +func ParseSearchOffset(value string, defaultValue int, max, min int) (int, gtserror.WithCode) { +	return parseInt(value, defaultValue, max, min, SearchOffsetKey)  } -func ParseLocal(value string, defaultValue bool) (bool, gtserror.WithCode) { -	key := LimitKey +func ParseSearchResolve(value string, defaultValue bool) (bool, gtserror.WithCode) { +	return parseBool(value, defaultValue, SearchResolveKey) +} -	if value == "" { -		return defaultValue, nil -	} +func ParseDomainBlockExport(value string, defaultValue bool) (bool, gtserror.WithCode) { +	return parseBool(value, defaultValue, DomainBlockExportKey) +} -	i, err := strconv.ParseBool(value) -	if err != nil { -		return defaultValue, parseError(key, value, defaultValue, err) +func ParseDomainBlockImport(value string, defaultValue bool) (bool, gtserror.WithCode) { +	return parseBool(value, defaultValue, DomainBlockImportKey) +} + +/* +	Parse functions for *REQUIRED* parameters. +*/ + +func ParseID(value string) (string, gtserror.WithCode) { +	key := IDKey + +	if value == "" { +		return "", requiredError(key)  	} -	return i, nil +	return value, nil  } -func ParseSearchExcludeUnreviewed(value string, defaultValue bool) (bool, gtserror.WithCode) { -	key := SearchExcludeUnreviewedKey +func ParseSearchLookup(value string) (string, gtserror.WithCode) { +	key := SearchLookupKey  	if value == "" { -		return defaultValue, nil +		return "", requiredError(key)  	} -	i, err := strconv.ParseBool(value) -	if err != nil { -		return defaultValue, parseError(key, value, defaultValue, err) +	return value, nil +} + +func ParseSearchQuery(value string) (string, gtserror.WithCode) { +	key := SearchQueryKey + +	if value == "" { +		return "", requiredError(key)  	} -	return i, nil +	return value, nil  } -func ParseSearchFollowing(value string, defaultValue bool) (bool, gtserror.WithCode) { -	key := SearchFollowingKey +/* +	Internal functions +*/ +func parseBool(value string, defaultValue bool, key string) (bool, gtserror.WithCode) {  	if value == "" {  		return defaultValue, nil  	} @@ -125,9 +150,7 @@ func ParseSearchFollowing(value string, defaultValue bool) (bool, gtserror.WithC  	return i, nil  } -func ParseSearchOffset(value string, defaultValue int, max, min int) (int, gtserror.WithCode) { -	key := SearchOffsetKey - +func parseInt(value string, defaultValue int, max int, min int, key string) (int, gtserror.WithCode) {  	if value == "" {  		return defaultValue, nil  	} @@ -145,42 +168,3 @@ func ParseSearchOffset(value string, defaultValue int, max, min int) (int, gtser  	return i, nil  } - -func ParseSearchResolve(value string, defaultValue bool) (bool, gtserror.WithCode) { -	key := SearchResolveKey - -	if value == "" { -		return defaultValue, nil -	} - -	i, err := strconv.ParseBool(value) -	if err != nil { -		return defaultValue, parseError(key, value, defaultValue, err) -	} - -	return i, nil -} - -/* -	Parse functions for *REQUIRED* parameters. -*/ - -func ParseSearchLookup(value string) (string, gtserror.WithCode) { -	key := SearchLookupKey - -	if value == "" { -		return "", requiredError(key) -	} - -	return value, nil -} - -func ParseSearchQuery(value string) (string, gtserror.WithCode) { -	key := SearchQueryKey - -	if value == "" { -		return "", requiredError(key) -	} - -	return value, nil -} | 
