diff options
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 -} |