mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-11-18 12:47:29 -06:00
[feature] Filters v1 (#2594)
* Implement client-side v1 filters * Exclude linter false positives * Update test/envparsing.sh * Fix minor Swagger, style, and Bun usage issues * Regenerate Swagger * De-generify filter keywords * Remove updating filter statuses This is an operation that the Mastodon v2 filter API doesn't actually have, because filter statuses, unlike keywords, don't have options: the only info they contain is the status ID to be filtered. * Add a test for filter statuses specifically * De-generify filter statuses * Inline FilterEntry * Use vertical style for Bun operations consistently * Add comment on Filter DB interface * Remove GoLand linter control comments Our existing linters should catch these, or they don't matter very much * Reduce memory ratio for filters
This commit is contained in:
parent
7bc536d1f7
commit
61a2b91f45
50 changed files with 4672 additions and 52 deletions
|
|
@ -15,20 +15,23 @@
|
|||
// 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 filter
|
||||
package v1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const (
|
||||
// BasePath is the base path for serving the filters API, minus the 'api' prefix
|
||||
BasePath = "/v1/filters"
|
||||
// BasePathWithID is the base path with the ID key in it, for operations on an existing filter.
|
||||
BasePathWithID = BasePath + "/:" + apiutil.IDKey
|
||||
)
|
||||
|
||||
// Module implements APIs for client-side aka "v1" filtering.
|
||||
type Module struct {
|
||||
processor *processing.Processor
|
||||
}
|
||||
|
|
@ -41,4 +44,8 @@ func New(processor *processing.Processor) *Module {
|
|||
|
||||
func (m *Module) Route(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) {
|
||||
attachHandler(http.MethodGet, BasePath, m.FiltersGETHandler)
|
||||
attachHandler(http.MethodPost, BasePath, m.FilterPOSTHandler)
|
||||
attachHandler(http.MethodGet, BasePathWithID, m.FilterGETHandler)
|
||||
attachHandler(http.MethodPut, BasePathWithID, m.FilterPUTHandler)
|
||||
attachHandler(http.MethodDelete, BasePathWithID, m.FilterDELETEHandler)
|
||||
}
|
||||
117
internal/api/client/filters/v1/filter_test.go
Normal file
117
internal/api/client/filters/v1/filter_test.go
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
// 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 v1_test
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/suite"
|
||||
filtersV1 "github.com/superseriousbusiness/gotosocial/internal/api/client/filters/v1"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/email"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/storage"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type FiltersTestSuite struct {
|
||||
suite.Suite
|
||||
db db.DB
|
||||
storage *storage.Driver
|
||||
mediaManager *media.Manager
|
||||
federator *federation.Federator
|
||||
processor *processing.Processor
|
||||
emailSender email.Sender
|
||||
sentEmails map[string]string
|
||||
state state.State
|
||||
|
||||
// standard suite models
|
||||
testTokens map[string]*gtsmodel.Token
|
||||
testClients map[string]*gtsmodel.Client
|
||||
testApplications map[string]*gtsmodel.Application
|
||||
testUsers map[string]*gtsmodel.User
|
||||
testAccounts map[string]*gtsmodel.Account
|
||||
testStatuses map[string]*gtsmodel.Status
|
||||
testFilters map[string]*gtsmodel.Filter
|
||||
testFilterKeywords map[string]*gtsmodel.FilterKeyword
|
||||
testFilterStatuses map[string]*gtsmodel.FilterStatus
|
||||
|
||||
// module being tested
|
||||
filtersModule *filtersV1.Module
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) SetupSuite() {
|
||||
suite.testTokens = testrig.NewTestTokens()
|
||||
suite.testClients = testrig.NewTestClients()
|
||||
suite.testApplications = testrig.NewTestApplications()
|
||||
suite.testUsers = testrig.NewTestUsers()
|
||||
suite.testAccounts = testrig.NewTestAccounts()
|
||||
suite.testStatuses = testrig.NewTestStatuses()
|
||||
suite.testFilters = testrig.NewTestFilters()
|
||||
suite.testFilterKeywords = testrig.NewTestFilterKeywords()
|
||||
suite.testFilterStatuses = testrig.NewTestFilterStatuses()
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) SetupTest() {
|
||||
suite.state.Caches.Init()
|
||||
testrig.StartNoopWorkers(&suite.state)
|
||||
|
||||
testrig.InitTestConfig()
|
||||
config.Config(func(cfg *config.Configuration) {
|
||||
cfg.WebAssetBaseDir = "../../../../../web/assets/"
|
||||
cfg.WebTemplateBaseDir = "../../../../../web/templates/"
|
||||
})
|
||||
testrig.InitTestLog()
|
||||
|
||||
suite.db = testrig.NewTestDB(&suite.state)
|
||||
suite.state.DB = suite.db
|
||||
suite.storage = testrig.NewInMemoryStorage()
|
||||
suite.state.Storage = suite.storage
|
||||
|
||||
testrig.StartTimelines(
|
||||
&suite.state,
|
||||
visibility.NewFilter(&suite.state),
|
||||
typeutils.NewConverter(&suite.state),
|
||||
)
|
||||
|
||||
suite.mediaManager = testrig.NewTestMediaManager(&suite.state)
|
||||
suite.federator = testrig.NewTestFederator(&suite.state, testrig.NewTestTransportController(&suite.state, testrig.NewMockHTTPClient(nil, "../../../../../testrig/media")), suite.mediaManager)
|
||||
suite.sentEmails = make(map[string]string)
|
||||
suite.emailSender = testrig.NewEmailSender("../../../../../web/template/", suite.sentEmails)
|
||||
suite.processor = testrig.NewTestProcessor(&suite.state, suite.federator, suite.emailSender, suite.mediaManager)
|
||||
suite.filtersModule = filtersV1.New(suite.processor)
|
||||
|
||||
testrig.StandardDBSetup(suite.db, nil)
|
||||
testrig.StandardStorageSetup(suite.storage, "../../../../../testrig/media")
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TearDownTest() {
|
||||
testrig.StandardDBTeardown(suite.db)
|
||||
testrig.StandardStorageTeardown(suite.storage)
|
||||
testrig.StopWorkers(&suite.state)
|
||||
}
|
||||
|
||||
func TestFiltersTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(FiltersTestSuite))
|
||||
}
|
||||
90
internal/api/client/filters/v1/filterdelete.go
Normal file
90
internal/api/client/filters/v1/filterdelete.go
Normal file
|
|
@ -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 v1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// FilterDELETEHandler swagger:operation DELETE /api/v1/filters/{id} filterV1Delete
|
||||
//
|
||||
// Delete a single filter with the given ID.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - filters
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: id
|
||||
// type: string
|
||||
// description: ID of the list
|
||||
// in: path
|
||||
// required: true
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - write:filters
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// description: filter deleted
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) FilterDELETEHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
id, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
errWithCode = m.processor.FiltersV1().Delete(c.Request.Context(), authed.Account, id)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, apiutil.EmptyJSONObject)
|
||||
}
|
||||
112
internal/api/client/filters/v1/filterdelete_test.go
Normal file
112
internal/api/client/filters/v1/filterdelete_test.go
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
// 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 v1_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
||||
filtersV1 "github.com/superseriousbusiness/gotosocial/internal/api/client/filters/v1"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
func (suite *FiltersTestSuite) deleteFilter(
|
||||
filterKeywordID string,
|
||||
expectedHTTPStatus int,
|
||||
expectedBody string,
|
||||
) error {
|
||||
// instantiate recorder + test context
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedToken, oauth.DBTokenToToken(suite.testTokens["local_account_1"]))
|
||||
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
||||
|
||||
// create the request
|
||||
ctx.Request = httptest.NewRequest(http.MethodDelete, config.GetProtocol()+"://"+config.GetHost()+"/api/"+filtersV1.BasePath+"/"+filterKeywordID, nil)
|
||||
ctx.Request.Header.Set("accept", "application/json")
|
||||
|
||||
ctx.AddParam("id", filterKeywordID)
|
||||
|
||||
// trigger the handler
|
||||
suite.filtersModule.FilterDELETEHandler(ctx)
|
||||
|
||||
// read the response
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(result.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
errs := gtserror.NewMultiError(2)
|
||||
|
||||
// check code + body
|
||||
if resultCode := recorder.Code; expectedHTTPStatus != resultCode {
|
||||
errs.Appendf("expected %d got %d", expectedHTTPStatus, resultCode)
|
||||
}
|
||||
|
||||
// if we got an expected body, return early
|
||||
if expectedBody != "" {
|
||||
if string(b) != expectedBody {
|
||||
errs.Appendf("expected %s got %s", expectedBody, string(b))
|
||||
}
|
||||
return errs.Combine()
|
||||
}
|
||||
|
||||
resp := &struct{}{}
|
||||
if err := json.Unmarshal(b, resp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestDeleteFilter() {
|
||||
id := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"].ID
|
||||
|
||||
err := suite.deleteFilter(id, http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestDeleteAnotherAccountsFilter() {
|
||||
id := suite.testFilterKeywords["local_account_2_filter_1_keyword_1"].ID
|
||||
|
||||
err := suite.deleteFilter(id, http.StatusNotFound, `{"error":"Not Found"}`)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestDeleteNonexistentFilter() {
|
||||
id := "not_even_a_real_ULID"
|
||||
|
||||
err := suite.deleteFilter(id, http.StatusNotFound, `{"error":"Not Found"}`)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
93
internal/api/client/filters/v1/filterget.go
Normal file
93
internal/api/client/filters/v1/filterget.go
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
// 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 v1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// FilterGETHandler swagger:operation GET /api/v1/filters/{id} filterV1Get
|
||||
//
|
||||
// Get a single filter with the given ID.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - filters
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: id
|
||||
// type: string
|
||||
// description: ID of the filter
|
||||
// in: path
|
||||
// required: true
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - read:filters
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// name: filter
|
||||
// description: Requested filter.
|
||||
// schema:
|
||||
// "$ref": "#/definitions/filterV1"
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) FilterGETHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
id, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiFilter, errWithCode := m.processor.FiltersV1().Get(c.Request.Context(), authed.Account, id)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, apiFilter)
|
||||
}
|
||||
121
internal/api/client/filters/v1/filterget_test.go
Normal file
121
internal/api/client/filters/v1/filterget_test.go
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
// 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 v1_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
||||
filtersV1 "github.com/superseriousbusiness/gotosocial/internal/api/client/filters/v1"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
func (suite *FiltersTestSuite) getFilter(
|
||||
filterKeywordID string,
|
||||
expectedHTTPStatus int,
|
||||
expectedBody string,
|
||||
) (*apimodel.FilterV1, error) {
|
||||
// instantiate recorder + test context
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedToken, oauth.DBTokenToToken(suite.testTokens["local_account_1"]))
|
||||
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
||||
|
||||
// create the request
|
||||
ctx.Request = httptest.NewRequest(http.MethodGet, config.GetProtocol()+"://"+config.GetHost()+"/api/"+filtersV1.BasePath+"/"+filterKeywordID, nil)
|
||||
ctx.Request.Header.Set("accept", "application/json")
|
||||
|
||||
ctx.AddParam("id", filterKeywordID)
|
||||
|
||||
// trigger the handler
|
||||
suite.filtersModule.FilterGETHandler(ctx)
|
||||
|
||||
// read the response
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(result.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
errs := gtserror.NewMultiError(2)
|
||||
|
||||
// check code + body
|
||||
if resultCode := recorder.Code; expectedHTTPStatus != resultCode {
|
||||
errs.Appendf("expected %d got %d", expectedHTTPStatus, resultCode)
|
||||
}
|
||||
|
||||
// if we got an expected body, return early
|
||||
if expectedBody != "" {
|
||||
if string(b) != expectedBody {
|
||||
errs.Appendf("expected %s got %s", expectedBody, string(b))
|
||||
}
|
||||
return nil, errs.Combine()
|
||||
}
|
||||
|
||||
resp := &apimodel.FilterV1{}
|
||||
if err := json.Unmarshal(b, resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestGetFilter() {
|
||||
// v1 filters map to individual filter keywords, but also use the settings of the associated filter.
|
||||
expectedFilterGtsModel := suite.testFilters["local_account_1_filter_1"]
|
||||
expectedFilterKeywordGtsModel := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"]
|
||||
|
||||
filter, err := suite.getFilter(expectedFilterKeywordGtsModel.ID, http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
suite.NotEmpty(filter)
|
||||
suite.Equal(expectedFilterGtsModel.Action == gtsmodel.FilterActionHide, filter.Irreversible)
|
||||
suite.Equal(expectedFilterKeywordGtsModel.ID, filter.ID)
|
||||
suite.Equal(expectedFilterKeywordGtsModel.Keyword, filter.Phrase)
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestGetAnotherAccountsFilter() {
|
||||
id := suite.testFilterKeywords["local_account_2_filter_1_keyword_1"].ID
|
||||
|
||||
_, err := suite.getFilter(id, http.StatusNotFound, `{"error":"Not Found"}`)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestGetNonexistentFilter() {
|
||||
id := "not_even_a_real_ULID"
|
||||
|
||||
_, err := suite.getFilter(id, http.StatusNotFound, `{"error":"Not Found"}`)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
147
internal/api/client/filters/v1/filterpost.go
Normal file
147
internal/api/client/filters/v1/filterpost.go
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
// 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 v1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// FilterPOSTHandler swagger:operation POST /api/v1/filters filterV1Post
|
||||
//
|
||||
// Create a single filter.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - filters
|
||||
//
|
||||
// consumes:
|
||||
// - application/json
|
||||
// - application/xml
|
||||
// - application/x-www-form-urlencoded
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: phrase
|
||||
// in: formData
|
||||
// required: true
|
||||
// description: The text to be filtered.
|
||||
// maxLength: 40
|
||||
// type: string
|
||||
// example: "fnord"
|
||||
// -
|
||||
// name: context
|
||||
// in: formData
|
||||
// required: true
|
||||
// description: The contexts in which the filter should be applied.
|
||||
// enum:
|
||||
// - home
|
||||
// - notifications
|
||||
// - public
|
||||
// - thread
|
||||
// - account
|
||||
// example:
|
||||
// - home
|
||||
// - public
|
||||
// items:
|
||||
// $ref: '#/definitions/filterContext'
|
||||
// minLength: 1
|
||||
// type: array
|
||||
// uniqueItems: true
|
||||
// -
|
||||
// name: expires_in
|
||||
// in: formData
|
||||
// description: Number of seconds from now that the filter should expire. If omitted, filter never expires.
|
||||
// type: number
|
||||
// example: 86400
|
||||
// -
|
||||
// name: irreversible
|
||||
// in: formData
|
||||
// description: Should matching entities be removed from the user's timelines/views, instead of hidden? Not supported yet.
|
||||
// type: boolean
|
||||
// default: false
|
||||
// example: false
|
||||
// -
|
||||
// name: whole_word
|
||||
// in: formData
|
||||
// description: Should the filter consider word boundaries?
|
||||
// type: boolean
|
||||
// default: false
|
||||
// example: true
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - write:filters
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// name: filter
|
||||
// description: New filter.
|
||||
// schema:
|
||||
// "$ref": "#/definitions/filterV1"
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '422':
|
||||
// description: unprocessable content
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) FilterPOSTHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
form := &apimodel.FilterCreateUpdateRequestV1{}
|
||||
if err := c.ShouldBind(form); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if err := validateNormalizeCreateUpdateFilter(form); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnprocessableEntity(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiFilter, errWithCode := m.processor.FiltersV1().Create(c.Request.Context(), authed.Account, form)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiutil.JSON(c, http.StatusOK, apiFilter)
|
||||
}
|
||||
239
internal/api/client/filters/v1/filterpost_test.go
Normal file
239
internal/api/client/filters/v1/filterpost_test.go
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
// 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 v1_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
filtersV1 "github.com/superseriousbusiness/gotosocial/internal/api/client/filters/v1"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
func (suite *FiltersTestSuite) postFilter(
|
||||
phrase *string,
|
||||
context *[]string,
|
||||
irreversible *bool,
|
||||
wholeWord *bool,
|
||||
expiresIn *int,
|
||||
requestJson *string,
|
||||
expectedHTTPStatus int,
|
||||
expectedBody string,
|
||||
) (*apimodel.FilterV1, error) {
|
||||
// instantiate recorder + test context
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedToken, oauth.DBTokenToToken(suite.testTokens["local_account_1"]))
|
||||
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
||||
|
||||
// create the request
|
||||
ctx.Request = httptest.NewRequest(http.MethodPost, config.GetProtocol()+"://"+config.GetHost()+"/api/"+filtersV1.BasePath, nil)
|
||||
ctx.Request.Header.Set("accept", "application/json")
|
||||
if requestJson != nil {
|
||||
ctx.Request.Header.Set("content-type", "application/json")
|
||||
ctx.Request.Body = io.NopCloser(strings.NewReader(*requestJson))
|
||||
} else {
|
||||
ctx.Request.Form = make(url.Values)
|
||||
if phrase != nil {
|
||||
ctx.Request.Form["phrase"] = []string{*phrase}
|
||||
}
|
||||
if context != nil {
|
||||
ctx.Request.Form["context[]"] = *context
|
||||
}
|
||||
if irreversible != nil {
|
||||
ctx.Request.Form["irreversible"] = []string{strconv.FormatBool(*irreversible)}
|
||||
}
|
||||
if wholeWord != nil {
|
||||
ctx.Request.Form["whole_word"] = []string{strconv.FormatBool(*wholeWord)}
|
||||
}
|
||||
if expiresIn != nil {
|
||||
ctx.Request.Form["expires_in"] = []string{strconv.Itoa(*expiresIn)}
|
||||
}
|
||||
}
|
||||
|
||||
// trigger the handler
|
||||
suite.filtersModule.FilterPOSTHandler(ctx)
|
||||
|
||||
// read the response
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(result.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
errs := gtserror.NewMultiError(2)
|
||||
|
||||
// check code + body
|
||||
if resultCode := recorder.Code; expectedHTTPStatus != resultCode {
|
||||
errs.Appendf("expected %d got %d", expectedHTTPStatus, resultCode)
|
||||
}
|
||||
|
||||
// if we got an expected body, return early
|
||||
if expectedBody != "" {
|
||||
if string(b) != expectedBody {
|
||||
errs.Appendf("expected %s got %s", expectedBody, string(b))
|
||||
}
|
||||
return nil, errs.Combine()
|
||||
}
|
||||
|
||||
resp := &apimodel.FilterV1{}
|
||||
if err := json.Unmarshal(b, resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPostFilterFull() {
|
||||
phrase := "GNU/Linux"
|
||||
context := []string{"home", "public"}
|
||||
irreversible := false
|
||||
wholeWord := true
|
||||
expiresIn := 86400
|
||||
filter, err := suite.postFilter(&phrase, &context, &irreversible, &wholeWord, &expiresIn, nil, http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
suite.Equal(phrase, filter.Phrase)
|
||||
filterContext := make([]string, 0, len(filter.Context))
|
||||
for _, c := range filter.Context {
|
||||
filterContext = append(filterContext, string(c))
|
||||
}
|
||||
suite.ElementsMatch(context, filterContext)
|
||||
suite.Equal(irreversible, filter.Irreversible)
|
||||
suite.Equal(wholeWord, filter.WholeWord)
|
||||
if suite.NotNil(filter.ExpiresAt) {
|
||||
suite.NotEmpty(*filter.ExpiresAt)
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPostFilterFullJSON() {
|
||||
// Use a numeric literal with a fractional part to test the JSON-specific handling for non-integer "expires_in".
|
||||
requestJson := `{
|
||||
"phrase":"GNU/Linux",
|
||||
"context": ["home", "public"],
|
||||
"irreversible": false,
|
||||
"whole_word": true,
|
||||
"expires_in": 86400.1
|
||||
}`
|
||||
filter, err := suite.postFilter(nil, nil, nil, nil, nil, &requestJson, http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
suite.Equal("GNU/Linux", filter.Phrase)
|
||||
suite.ElementsMatch(
|
||||
[]apimodel.FilterContext{
|
||||
apimodel.FilterContextHome,
|
||||
apimodel.FilterContextPublic,
|
||||
},
|
||||
filter.Context,
|
||||
)
|
||||
suite.Equal(false, filter.Irreversible)
|
||||
suite.Equal(true, filter.WholeWord)
|
||||
if suite.NotNil(filter.ExpiresAt) {
|
||||
suite.NotEmpty(*filter.ExpiresAt)
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPostFilterMinimal() {
|
||||
phrase := "GNU/Linux"
|
||||
context := []string{"home"}
|
||||
filter, err := suite.postFilter(&phrase, &context, nil, nil, nil, nil, http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
suite.Equal(phrase, filter.Phrase)
|
||||
filterContext := make([]string, 0, len(filter.Context))
|
||||
for _, c := range filter.Context {
|
||||
filterContext = append(filterContext, string(c))
|
||||
}
|
||||
suite.ElementsMatch(context, filterContext)
|
||||
suite.False(filter.Irreversible)
|
||||
suite.False(filter.WholeWord)
|
||||
suite.Nil(filter.ExpiresAt)
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPostFilterEmptyPhrase() {
|
||||
phrase := ""
|
||||
context := []string{"home"}
|
||||
_, err := suite.postFilter(&phrase, &context, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPostFilterMissingPhrase() {
|
||||
context := []string{"home"}
|
||||
_, err := suite.postFilter(nil, &context, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPostFilterEmptyContext() {
|
||||
phrase := "GNU/Linux"
|
||||
context := []string{}
|
||||
_, err := suite.postFilter(&phrase, &context, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPostFilterMissingContext() {
|
||||
phrase := "GNU/Linux"
|
||||
_, err := suite.postFilter(&phrase, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// There should be a filter with this phrase as its title in our test fixtures. Creating another should fail.
|
||||
func (suite *FiltersTestSuite) TestPostFilterTitleConflict() {
|
||||
phrase := "fnord"
|
||||
_, err := suite.postFilter(&phrase, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// FUTURE: this should be removed once we support server-side filters.
|
||||
func (suite *FiltersTestSuite) TestPostFilterIrreversibleNotSupported() {
|
||||
phrase := "GNU/Linux"
|
||||
context := []string{"home"}
|
||||
irreversible := true
|
||||
_, err := suite.postFilter(&phrase, &context, &irreversible, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
159
internal/api/client/filters/v1/filterput.go
Normal file
159
internal/api/client/filters/v1/filterput.go
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
// 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 v1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// FilterPUTHandler swagger:operation PUT /api/v1/filters/{id} filterV1Put
|
||||
//
|
||||
// Update a single filter with the given ID.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - filters
|
||||
//
|
||||
// consumes:
|
||||
// - application/json
|
||||
// - application/xml
|
||||
// - application/x-www-form-urlencoded
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: id
|
||||
// in: path
|
||||
// type: string
|
||||
// required: true
|
||||
// description: ID of the filter.
|
||||
// -
|
||||
// name: phrase
|
||||
// in: formData
|
||||
// required: true
|
||||
// description: The text to be filtered.
|
||||
// maxLength: 40
|
||||
// type: string
|
||||
// example: "fnord"
|
||||
// -
|
||||
// name: context
|
||||
// in: formData
|
||||
// required: true
|
||||
// description: The contexts in which the filter should be applied.
|
||||
// enum:
|
||||
// - home
|
||||
// - notifications
|
||||
// - public
|
||||
// - thread
|
||||
// - account
|
||||
// example:
|
||||
// - home
|
||||
// - public
|
||||
// items:
|
||||
// $ref: '#/definitions/filterContext'
|
||||
// minLength: 1
|
||||
// type: array
|
||||
// uniqueItems: true
|
||||
// -
|
||||
// name: expires_in
|
||||
// in: formData
|
||||
// description: Number of seconds from now that the filter should expire. If omitted, filter never expires.
|
||||
// type: number
|
||||
// example: 86400
|
||||
// -
|
||||
// name: irreversible
|
||||
// in: formData
|
||||
// description: Should matching entities be removed from the user's timelines/views, instead of hidden? Not supported yet.
|
||||
// type: boolean
|
||||
// default: false
|
||||
// example: false
|
||||
// -
|
||||
// name: whole_word
|
||||
// in: formData
|
||||
// description: Should the filter consider word boundaries?
|
||||
// type: boolean
|
||||
// default: false
|
||||
// example: true
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - write:filters
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// name: filter
|
||||
// description: Updated filter.
|
||||
// schema:
|
||||
// "$ref": "#/definitions/filterV1"
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '422':
|
||||
// description: unprocessable content
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) FilterPUTHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
id, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
form := &apimodel.FilterCreateUpdateRequestV1{}
|
||||
if err := c.ShouldBind(form); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if err := validateNormalizeCreateUpdateFilter(form); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnprocessableEntity(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiFilter, errWithCode := m.processor.FiltersV1().Update(c.Request.Context(), authed.Account, id, form)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiutil.JSON(c, http.StatusOK, apiFilter)
|
||||
}
|
||||
269
internal/api/client/filters/v1/filterput_test.go
Normal file
269
internal/api/client/filters/v1/filterput_test.go
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
// 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 v1_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
filtersV1 "github.com/superseriousbusiness/gotosocial/internal/api/client/filters/v1"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
func (suite *FiltersTestSuite) putFilter(
|
||||
filterKeywordID string,
|
||||
phrase *string,
|
||||
context *[]string,
|
||||
irreversible *bool,
|
||||
wholeWord *bool,
|
||||
expiresIn *int,
|
||||
requestJson *string,
|
||||
expectedHTTPStatus int,
|
||||
expectedBody string,
|
||||
) (*apimodel.FilterV1, error) {
|
||||
// instantiate recorder + test context
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedToken, oauth.DBTokenToToken(suite.testTokens["local_account_1"]))
|
||||
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
||||
|
||||
// create the request
|
||||
ctx.Request = httptest.NewRequest(http.MethodPut, config.GetProtocol()+"://"+config.GetHost()+"/api/"+filtersV1.BasePath+"/"+filterKeywordID, nil)
|
||||
ctx.Request.Header.Set("accept", "application/json")
|
||||
if requestJson != nil {
|
||||
ctx.Request.Header.Set("content-type", "application/json")
|
||||
ctx.Request.Body = io.NopCloser(strings.NewReader(*requestJson))
|
||||
} else {
|
||||
ctx.Request.Form = make(url.Values)
|
||||
if phrase != nil {
|
||||
ctx.Request.Form["phrase"] = []string{*phrase}
|
||||
}
|
||||
if context != nil {
|
||||
ctx.Request.Form["context[]"] = *context
|
||||
}
|
||||
if irreversible != nil {
|
||||
ctx.Request.Form["irreversible"] = []string{strconv.FormatBool(*irreversible)}
|
||||
}
|
||||
if wholeWord != nil {
|
||||
ctx.Request.Form["whole_word"] = []string{strconv.FormatBool(*wholeWord)}
|
||||
}
|
||||
if expiresIn != nil {
|
||||
ctx.Request.Form["expires_in"] = []string{strconv.Itoa(*expiresIn)}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.AddParam("id", filterKeywordID)
|
||||
|
||||
// trigger the handler
|
||||
suite.filtersModule.FilterPUTHandler(ctx)
|
||||
|
||||
// read the response
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(result.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
errs := gtserror.NewMultiError(2)
|
||||
|
||||
// check code + body
|
||||
if resultCode := recorder.Code; expectedHTTPStatus != resultCode {
|
||||
errs.Appendf("expected %d got %d", expectedHTTPStatus, resultCode)
|
||||
}
|
||||
|
||||
// if we got an expected body, return early
|
||||
if expectedBody != "" {
|
||||
if string(b) != expectedBody {
|
||||
errs.Appendf("expected %s got %s", expectedBody, string(b))
|
||||
}
|
||||
return nil, errs.Combine()
|
||||
}
|
||||
|
||||
resp := &apimodel.FilterV1{}
|
||||
if err := json.Unmarshal(b, resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPutFilterFull() {
|
||||
id := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"].ID
|
||||
phrase := "GNU/Linux"
|
||||
context := []string{"home", "public"}
|
||||
irreversible := false
|
||||
wholeWord := true
|
||||
expiresIn := 86400
|
||||
filter, err := suite.putFilter(id, &phrase, &context, &irreversible, &wholeWord, &expiresIn, nil, http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
suite.Equal(phrase, filter.Phrase)
|
||||
filterContext := make([]string, 0, len(filter.Context))
|
||||
for _, c := range filter.Context {
|
||||
filterContext = append(filterContext, string(c))
|
||||
}
|
||||
suite.ElementsMatch(context, filterContext)
|
||||
suite.Equal(irreversible, filter.Irreversible)
|
||||
suite.Equal(wholeWord, filter.WholeWord)
|
||||
if suite.NotNil(filter.ExpiresAt) {
|
||||
suite.NotEmpty(*filter.ExpiresAt)
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPutFilterFullJSON() {
|
||||
id := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"].ID
|
||||
// Use a numeric literal with a fractional part to test the JSON-specific handling for non-integer "expires_in".
|
||||
requestJson := `{
|
||||
"phrase":"GNU/Linux",
|
||||
"context": ["home", "public"],
|
||||
"irreversible": false,
|
||||
"whole_word": true,
|
||||
"expires_in": 86400.1
|
||||
}`
|
||||
filter, err := suite.putFilter(id, nil, nil, nil, nil, nil, &requestJson, http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
suite.Equal("GNU/Linux", filter.Phrase)
|
||||
suite.ElementsMatch(
|
||||
[]apimodel.FilterContext{
|
||||
apimodel.FilterContextHome,
|
||||
apimodel.FilterContextPublic,
|
||||
},
|
||||
filter.Context,
|
||||
)
|
||||
suite.Equal(false, filter.Irreversible)
|
||||
suite.Equal(true, filter.WholeWord)
|
||||
if suite.NotNil(filter.ExpiresAt) {
|
||||
suite.NotEmpty(*filter.ExpiresAt)
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPutFilterMinimal() {
|
||||
id := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"].ID
|
||||
phrase := "GNU/Linux"
|
||||
context := []string{"home"}
|
||||
filter, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
suite.Equal(phrase, filter.Phrase)
|
||||
filterContext := make([]string, 0, len(filter.Context))
|
||||
for _, c := range filter.Context {
|
||||
filterContext = append(filterContext, string(c))
|
||||
}
|
||||
suite.ElementsMatch(context, filterContext)
|
||||
suite.False(filter.Irreversible)
|
||||
suite.False(filter.WholeWord)
|
||||
suite.Nil(filter.ExpiresAt)
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPutFilterEmptyPhrase() {
|
||||
id := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"].ID
|
||||
phrase := ""
|
||||
context := []string{"home"}
|
||||
_, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPutFilterMissingPhrase() {
|
||||
id := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"].ID
|
||||
context := []string{"home"}
|
||||
_, err := suite.putFilter(id, nil, &context, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPutFilterEmptyContext() {
|
||||
id := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"].ID
|
||||
phrase := "GNU/Linux"
|
||||
context := []string{}
|
||||
_, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPutFilterMissingContext() {
|
||||
id := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"].ID
|
||||
phrase := "GNU/Linux"
|
||||
_, err := suite.putFilter(id, &phrase, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// There should be a filter with this phrase as its title in our test fixtures. Changing ours to that title should fail.
|
||||
func (suite *FiltersTestSuite) TestPutFilterTitleConflict() {
|
||||
id := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"].ID
|
||||
phrase := "metasyntactic variables"
|
||||
_, err := suite.putFilter(id, &phrase, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// FUTURE: this should be removed once we support server-side filters.
|
||||
func (suite *FiltersTestSuite) TestPutFilterIrreversibleNotSupported() {
|
||||
id := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"].ID
|
||||
irreversible := true
|
||||
_, err := suite.putFilter(id, nil, nil, &irreversible, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPutAnotherAccountsFilter() {
|
||||
id := suite.testFilterKeywords["local_account_2_filter_1_keyword_1"].ID
|
||||
phrase := "GNU/Linux"
|
||||
context := []string{"home"}
|
||||
_, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, http.StatusNotFound, `{"error":"Not Found"}`)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPutNonexistentFilter() {
|
||||
id := "not_even_a_real_ULID"
|
||||
phrase := "GNU/Linux"
|
||||
context := []string{"home"}
|
||||
_, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, http.StatusNotFound, `{"error":"Not Found"}`)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
// 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 filter
|
||||
package v1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
|
@ -26,9 +26,40 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// FiltersGETHandler returns a list of filters set by/for the authed account
|
||||
// FiltersGETHandler swagger:operation GET /api/v1/filters filtersV1Get
|
||||
//
|
||||
// Get all filters for the authenticated account.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - filters
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - read:filters
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// name: filter
|
||||
// description: Requested filters.
|
||||
// schema:
|
||||
// "$ref": "#/definitions/filterV1"
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) FiltersGETHandler(c *gin.Context) {
|
||||
if _, err := oauth.Authed(c, true, true, true, true); err != nil {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
|
@ -38,5 +69,11 @@ func (m *Module) FiltersGETHandler(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
apiutil.Data(c, http.StatusOK, apiutil.AppJSON, apiutil.EmptyJSONArray)
|
||||
apiFilters, errWithCode := m.processor.FiltersV1().GetAll(c.Request.Context(), authed.Account)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, apiFilters)
|
||||
}
|
||||
114
internal/api/client/filters/v1/filtersget_test.go
Normal file
114
internal/api/client/filters/v1/filtersget_test.go
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
// 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 v1_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
||||
filtersV1 "github.com/superseriousbusiness/gotosocial/internal/api/client/filters/v1"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
func (suite *FiltersTestSuite) getFilters(
|
||||
expectedHTTPStatus int,
|
||||
expectedBody string,
|
||||
) ([]*apimodel.FilterV1, error) {
|
||||
// instantiate recorder + test context
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedToken, oauth.DBTokenToToken(suite.testTokens["local_account_1"]))
|
||||
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
||||
|
||||
// create the request
|
||||
ctx.Request = httptest.NewRequest(http.MethodGet, config.GetProtocol()+"://"+config.GetHost()+"/api/"+filtersV1.BasePath, nil)
|
||||
ctx.Request.Header.Set("accept", "application/json")
|
||||
|
||||
// trigger the handler
|
||||
suite.filtersModule.FiltersGETHandler(ctx)
|
||||
|
||||
// read the response
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(result.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
errs := gtserror.NewMultiError(2)
|
||||
|
||||
// check code + body
|
||||
if resultCode := recorder.Code; expectedHTTPStatus != resultCode {
|
||||
errs.Appendf("expected %d got %d", expectedHTTPStatus, resultCode)
|
||||
}
|
||||
|
||||
// if we got an expected body, return early
|
||||
if expectedBody != "" {
|
||||
if string(b) != expectedBody {
|
||||
errs.Appendf("expected %s got %s", expectedBody, string(b))
|
||||
}
|
||||
return nil, errs.Combine()
|
||||
}
|
||||
|
||||
resp := make([]*apimodel.FilterV1, 0)
|
||||
if err := json.Unmarshal(b, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestGetFilters() {
|
||||
// v1 filters map to individual filter keywords.
|
||||
expectedFilterIDs := make([]string, 0, len(suite.testFilterKeywords))
|
||||
expectedFilterKeywords := make([]string, 0, len(suite.testFilterKeywords))
|
||||
for _, filterKeyword := range suite.testFilterKeywords {
|
||||
if filterKeyword.AccountID == suite.testAccounts["local_account_1"].ID {
|
||||
expectedFilterIDs = append(expectedFilterIDs, filterKeyword.ID)
|
||||
expectedFilterKeywords = append(expectedFilterKeywords, filterKeyword.Keyword)
|
||||
}
|
||||
}
|
||||
suite.NotEmpty(expectedFilterIDs)
|
||||
suite.NotEmpty(expectedFilterKeywords)
|
||||
|
||||
// Fetch all filters for the logged-in account.
|
||||
filters, err := suite.getFilters(http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
suite.NotEmpty(filters)
|
||||
|
||||
// Check that we got the right ones.
|
||||
actualFilterIDs := make([]string, 0, len(filters))
|
||||
actualFilterKeywords := make([]string, 0, len(filters))
|
||||
for _, filter := range filters {
|
||||
actualFilterIDs = append(actualFilterIDs, filter.ID)
|
||||
actualFilterKeywords = append(actualFilterKeywords, filter.Phrase)
|
||||
}
|
||||
suite.ElementsMatch(expectedFilterIDs, actualFilterIDs)
|
||||
suite.ElementsMatch(expectedFilterKeywords, actualFilterKeywords)
|
||||
}
|
||||
68
internal/api/client/filters/v1/validate.go
Normal file
68
internal/api/client/filters/v1/validate.go
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
// 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 v1
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/validate"
|
||||
)
|
||||
|
||||
func validateNormalizeCreateUpdateFilter(form *model.FilterCreateUpdateRequestV1) error {
|
||||
if err := validate.FilterKeyword(form.Phrase); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validate.FilterContexts(form.Context); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Apply defaults for missing fields.
|
||||
form.WholeWord = util.Ptr(util.PtrValueOr(form.WholeWord, false))
|
||||
form.Irreversible = util.Ptr(util.PtrValueOr(form.Irreversible, false))
|
||||
|
||||
if *form.Irreversible {
|
||||
return errors.New("irreversible aka server-side drop filters are not supported yet")
|
||||
}
|
||||
|
||||
// Normalize filter expiry if necessary.
|
||||
// If we parsed this as JSON, expires_in
|
||||
// may be either a float64 or a string.
|
||||
if ei := form.ExpiresInI; ei != nil {
|
||||
switch e := ei.(type) {
|
||||
case float64:
|
||||
form.ExpiresIn = util.Ptr(int(e))
|
||||
|
||||
case string:
|
||||
expiresIn, err := strconv.Atoi(e)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse expires_in value %s as integer: %w", e, err)
|
||||
}
|
||||
|
||||
form.ExpiresIn = &expiresIn
|
||||
|
||||
default:
|
||||
return fmt.Errorf("could not parse expires_in type %T as integer", ei)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue