Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feature] filter API v2: restore keywords_attributes and statuses_attributes #2995

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions docs/api/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9245,6 +9245,27 @@ paths:
in: formData
name: filter_action
type: string
- collectionFormat: multi
description: Keywords to be added (if not using id param) or updated (if using id param).
in: formData
items:
type: string
name: keywords_attributes[][keyword]
type: array
- collectionFormat: multi
description: Should each keyword consider word boundaries?
in: formData
items:
type: boolean
name: keywords_attributes[][whole_word]
type: array
- collectionFormat: multi
description: Statuses to be added to the filter.
in: formData
items:
type: string
name: statuses_attributes[][status_id]
type: array
produces:
- application/json
responses:
Expand Down Expand Up @@ -9360,6 +9381,27 @@ paths:
name: title
required: true
type: string
- collectionFormat: multi
description: Keywords to be added to the created filter.
in: formData
items:
type: string
name: keywords_attributes[][keyword]
type: array
- collectionFormat: multi
description: Should each keyword consider word boundaries?
in: formData
items:
type: boolean
name: keywords_attributes[][whole_word]
type: array
- collectionFormat: multi
description: Statuses to be added to the newly created filter.
in: formData
items:
type: string
name: statuses_attributes[][status_id]
type: array
- collectionFormat: multi
description: |-
The contexts in which the filter should be applied.
Expand Down
61 changes: 61 additions & 0 deletions internal/api/client/filters/v2/filterpost.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,30 @@ import (
// - warn
// - hide
// default: warn
// -
// name: keywords_attributes[][keyword]
// in: formData
// type: array
// items:
// type: string
// description: Keywords to be added (if not using id param) or updated (if using id param).
// collectionFormat: multi
// -
// name: keywords_attributes[][whole_word]
// in: formData
// type: array
// items:
// type: boolean
// description: Should each keyword consider word boundaries?
// collectionFormat: multi
// -
// name: statuses_attributes[][status_id]
// in: formData
// type: array
// items:
// type: string
// description: Statuses to be added to the filter.
// collectionFormat: multi
//
// security:
// - OAuth2 Bearer:
Expand Down Expand Up @@ -176,6 +200,30 @@ func validateNormalizeCreateFilter(form *apimodel.FilterCreateRequestV2) error {
return err
}

// Parse form variant of normal filter keyword creation structs.
if len(form.KeywordsAttributesKeyword) > 0 {
form.Keywords = make([]apimodel.FilterKeywordCreateUpdateRequest, 0, len(form.KeywordsAttributesKeyword))
for i, keyword := range form.KeywordsAttributesKeyword {
formKeyword := apimodel.FilterKeywordCreateUpdateRequest{
Keyword: keyword,
}
if i < len(form.KeywordsAttributesWholeWord) {
formKeyword.WholeWord = &form.KeywordsAttributesWholeWord[i]
}
form.Keywords = append(form.Keywords, formKeyword)
}
}

// Parse form variant of normal filter status creation structs.
if len(form.StatusesAttributesStatusID) > 0 {
form.Statuses = make([]apimodel.FilterStatusCreateRequest, 0, len(form.StatusesAttributesStatusID))
for _, statusID := range form.StatusesAttributesStatusID {
form.Statuses = append(form.Statuses, apimodel.FilterStatusCreateRequest{
StatusID: statusID,
})
}
}

// Apply defaults for missing fields.
form.FilterAction = util.Ptr(action)

Expand All @@ -200,5 +248,18 @@ func validateNormalizeCreateFilter(form *apimodel.FilterCreateRequestV2) error {
}
}

// Normalize and validate new keywords and statuses.
for i, formKeyword := range form.Keywords {
if err := validate.FilterKeyword(formKeyword.Keyword); err != nil {
return err
}
form.Keywords[i].WholeWord = util.Ptr(util.PtrValueOr(formKeyword.WholeWord, false))
}
for _, formStatus := range form.Statuses {
if err := validate.ULID(formStatus.StatusID, "status_id"); err != nil {
return err
}
}

return nil
}
102 changes: 88 additions & 14 deletions internal/api/client/filters/v2/filterpost_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"net/http"
"net/http/httptest"
"net/url"
"slices"
"strconv"
"strings"

Expand All @@ -35,7 +36,7 @@ import (
"github.com/superseriousbusiness/gotosocial/testrig"
)

func (suite *FiltersTestSuite) postFilter(title *string, context *[]string, action *string, expiresIn *int, requestJson *string, expectedHTTPStatus int, expectedBody string) (*apimodel.FilterV2, error) {
func (suite *FiltersTestSuite) postFilter(title *string, context *[]string, action *string, expiresIn *int, keywordsAttributesKeyword *[]string, keywordsAttributesWholeWord *[]bool, statusesAttributesStatusID *[]string, requestJson *string, expectedHTTPStatus int, expectedBody string) (*apimodel.FilterV2, error) {
// instantiate recorder + test context
recorder := httptest.NewRecorder()
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
Expand Down Expand Up @@ -64,6 +65,19 @@ func (suite *FiltersTestSuite) postFilter(title *string, context *[]string, acti
if expiresIn != nil {
ctx.Request.Form["expires_in"] = []string{strconv.Itoa(*expiresIn)}
}
if keywordsAttributesKeyword != nil {
ctx.Request.Form["keywords_attributes[][keyword]"] = *keywordsAttributesKeyword
}
if keywordsAttributesWholeWord != nil {
formatted := []string{}
for _, value := range *keywordsAttributesWholeWord {
formatted = append(formatted, strconv.FormatBool(value))
}
ctx.Request.Form["keywords_attributes[][whole_word]"] = formatted
}
if statusesAttributesStatusID != nil {
ctx.Request.Form["statuses_attributes[][status_id]"] = *statusesAttributesStatusID
}
}

// trigger the handler
Expand Down Expand Up @@ -111,7 +125,12 @@ func (suite *FiltersTestSuite) TestPostFilterFull() {
context := []string{"home", "public"}
action := "warn"
expiresIn := 86400
filter, err := suite.postFilter(&title, &context, &action, &expiresIn, nil, http.StatusOK, "")
// Checked in lexical order by keyword, so keep this sorted.
keywordsAttributesKeyword := []string{"GNU", "Linux"}
keywordsAttributesWholeWord := []bool{true, false}
// Checked in lexical order by status ID, so keep this sorted.
statusAttributesStatusID := []string{"01HEN2QRFA8H3C6QPN7RD4KSR6", "01HEWV37MHV8BAC8ANFGVRRM5D"}
filter, err := suite.postFilter(&title, &context, &action, &expiresIn, &keywordsAttributesKeyword, &keywordsAttributesWholeWord, &statusAttributesStatusID, nil, http.StatusOK, "")
if err != nil {
suite.FailNow(err.Error())
}
Expand All @@ -126,8 +145,25 @@ func (suite *FiltersTestSuite) TestPostFilterFull() {
if suite.NotNil(filter.ExpiresAt) {
suite.NotEmpty(*filter.ExpiresAt)
}
suite.Empty(filter.Keywords)
suite.Empty(filter.Statuses)

if suite.Len(filter.Keywords, len(keywordsAttributesKeyword)) {
slices.SortFunc(filter.Keywords, func(lhs, rhs apimodel.FilterKeyword) int {
return strings.Compare(lhs.Keyword, rhs.Keyword)
})
for i, filterKeyword := range filter.Keywords {
suite.Equal(keywordsAttributesKeyword[i], filterKeyword.Keyword)
suite.Equal(keywordsAttributesWholeWord[i], filterKeyword.WholeWord)
}
}

if suite.Len(filter.Statuses, len(statusAttributesStatusID)) {
slices.SortFunc(filter.Statuses, func(lhs, rhs apimodel.FilterStatus) int {
return strings.Compare(lhs.StatusID, rhs.StatusID)
})
for i, filterStatus := range filter.Statuses {
suite.Equal(statusAttributesStatusID[i], filterStatus.StatusID)
}
}

suite.checkStreamed(homeStream, true, "", stream.EventTypeFiltersChanged)
}
Expand All @@ -141,9 +177,27 @@ func (suite *FiltersTestSuite) TestPostFilterFullJSON() {
"context": ["home", "public"],
"filter_action": "warn",
"whole_word": true,
"expires_in": 86400.1
"expires_in": 86400.1,
"keywords_attributes": [
{
"keyword": "GNU",
"whole_word": true
},
{
"keyword": "Linux",
"whole_word": false
}
],
"statuses_attributes": [
{
"status_id": "01HEN2QRFA8H3C6QPN7RD4KSR6"
},
{
"status_id": "01HEWV37MHV8BAC8ANFGVRRM5D"
}
]
}`
filter, err := suite.postFilter(nil, nil, nil, nil, &requestJson, http.StatusOK, "")
filter, err := suite.postFilter(nil, nil, nil, nil, nil, nil, nil, &requestJson, http.StatusOK, "")
if err != nil {
suite.FailNow(err.Error())
}
Expand All @@ -160,8 +214,28 @@ func (suite *FiltersTestSuite) TestPostFilterFullJSON() {
if suite.NotNil(filter.ExpiresAt) {
suite.NotEmpty(*filter.ExpiresAt)
}
suite.Empty(filter.Keywords)
suite.Empty(filter.Statuses)

if suite.Len(filter.Keywords, 2) {
slices.SortFunc(filter.Keywords, func(lhs, rhs apimodel.FilterKeyword) int {
return strings.Compare(lhs.Keyword, rhs.Keyword)
})

suite.Equal("GNU", filter.Keywords[0].Keyword)
suite.True(filter.Keywords[0].WholeWord)

suite.Equal("Linux", filter.Keywords[1].Keyword)
suite.False(filter.Keywords[1].WholeWord)
}

if suite.Len(filter.Statuses, 2) {
slices.SortFunc(filter.Statuses, func(lhs, rhs apimodel.FilterStatus) int {
return strings.Compare(lhs.StatusID, rhs.StatusID)
})

suite.Equal("01HEN2QRFA8H3C6QPN7RD4KSR6", filter.Statuses[0].StatusID)

suite.Equal("01HEWV37MHV8BAC8ANFGVRRM5D", filter.Statuses[1].StatusID)
}

suite.checkStreamed(homeStream, true, "", stream.EventTypeFiltersChanged)
}
Expand All @@ -171,7 +245,7 @@ func (suite *FiltersTestSuite) TestPostFilterMinimal() {

title := "GNU/Linux"
context := []string{"home"}
filter, err := suite.postFilter(&title, &context, nil, nil, nil, http.StatusOK, "")
filter, err := suite.postFilter(&title, &context, nil, nil, nil, nil, nil, nil, http.StatusOK, "")
if err != nil {
suite.FailNow(err.Error())
}
Expand All @@ -193,15 +267,15 @@ func (suite *FiltersTestSuite) TestPostFilterMinimal() {
func (suite *FiltersTestSuite) TestPostFilterEmptyTitle() {
title := ""
context := []string{"home"}
_, err := suite.postFilter(&title, &context, nil, nil, nil, http.StatusUnprocessableEntity, "")
_, err := suite.postFilter(&title, &context, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
if err != nil {
suite.FailNow(err.Error())
}
}

func (suite *FiltersTestSuite) TestPostFilterMissingTitle() {
context := []string{"home"}
_, err := suite.postFilter(nil, &context, nil, nil, nil, http.StatusUnprocessableEntity, "")
_, err := suite.postFilter(nil, &context, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
if err != nil {
suite.FailNow(err.Error())
}
Expand All @@ -210,15 +284,15 @@ func (suite *FiltersTestSuite) TestPostFilterMissingTitle() {
func (suite *FiltersTestSuite) TestPostFilterEmptyContext() {
title := "GNU/Linux"
context := []string{}
_, err := suite.postFilter(&title, &context, nil, nil, nil, http.StatusUnprocessableEntity, "")
_, err := suite.postFilter(&title, &context, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
if err != nil {
suite.FailNow(err.Error())
}
}

func (suite *FiltersTestSuite) TestPostFilterMissingContext() {
title := "GNU/Linux"
_, err := suite.postFilter(&title, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
_, err := suite.postFilter(&title, nil, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
if err != nil {
suite.FailNow(err.Error())
}
Expand All @@ -227,7 +301,7 @@ func (suite *FiltersTestSuite) TestPostFilterMissingContext() {
// Creating another filter with the same title should fail.
func (suite *FiltersTestSuite) TestPostFilterTitleConflict() {
title := suite.testFilters["local_account_1_filter_1"].Title
_, err := suite.postFilter(&title, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
_, err := suite.postFilter(&title, nil, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
if err != nil {
suite.FailNow(err.Error())
}
Expand Down
Loading