summaryrefslogtreecommitdiff
path: root/internal/api
diff options
context:
space:
mode:
authorLibravatar Vyr Cossont <VyrCossont@users.noreply.github.com>2024-06-14 01:11:41 -0700
committerLibravatar GitHub <noreply@github.com>2024-06-14 10:11:41 +0200
commitb789fe2bc72a1b1bca50da498ae22c10a4e7acc2 (patch)
treed43f049b4ee30bfbab2d1c1a10c02af904df9fbb /internal/api
parent[docs] Rework README a bit, import into docs (#3006) (diff)
downloadgotosocial-b789fe2bc72a1b1bca50da498ae22c10a4e7acc2.tar.xz
[feature] filter API v2: Restore keywords_attributes and statuses_attributes (#2995)
These filter API v2 features were cut late in development because the form encoding version is hard to implement correctly and because I thought no clients actually used `keywords_attributes`. Unfortunately, Phanpy does use `keywords_attributes`.
Diffstat (limited to 'internal/api')
-rw-r--r--internal/api/client/filters/v2/filterpost.go61
-rw-r--r--internal/api/client/filters/v2/filterpost_test.go102
-rw-r--r--internal/api/client/filters/v2/filterput.go114
-rw-r--r--internal/api/client/filters/v2/filterput_test.go134
-rw-r--r--internal/api/model/filterv2.go69
5 files changed, 450 insertions, 30 deletions
diff --git a/internal/api/client/filters/v2/filterpost.go b/internal/api/client/filters/v2/filterpost.go
index 9e8f87fd0..732b81041 100644
--- a/internal/api/client/filters/v2/filterpost.go
+++ b/internal/api/client/filters/v2/filterpost.go
@@ -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:
@@ -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)
@@ -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
}
diff --git a/internal/api/client/filters/v2/filterpost_test.go b/internal/api/client/filters/v2/filterpost_test.go
index 6656c4b59..6e378874c 100644
--- a/internal/api/client/filters/v2/filterpost_test.go
+++ b/internal/api/client/filters/v2/filterpost_test.go
@@ -23,6 +23,7 @@ import (
"net/http"
"net/http/httptest"
"net/url"
+ "slices"
"strconv"
"strings"
@@ -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)
@@ -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
@@ -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())
}
@@ -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)
}
@@ -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())
}
@@ -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)
}
@@ -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())
}
@@ -193,7 +267,7 @@ 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())
}
@@ -201,7 +275,7 @@ func (suite *FiltersTestSuite) TestPostFilterEmptyTitle() {
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())
}
@@ -210,7 +284,7 @@ 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())
}
@@ -218,7 +292,7 @@ func (suite *FiltersTestSuite) TestPostFilterEmptyContext() {
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())
}
@@ -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())
}
diff --git a/internal/api/client/filters/v2/filterput.go b/internal/api/client/filters/v2/filterput.go
index 24071a150..cc3531838 100644
--- a/internal/api/client/filters/v2/filterput.go
+++ b/internal/api/client/filters/v2/filterput.go
@@ -18,6 +18,7 @@
package v2
import (
+ "errors"
"fmt"
"net/http"
"strconv"
@@ -68,6 +69,30 @@ import (
// minLength: 1
// maxLength: 200
// -
+// name: keywords_attributes[][keyword]
+// in: formData
+// type: array
+// items:
+// type: string
+// description: Keywords to be added to the created filter.
+// 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 newly created filter.
+// collectionFormat: multi
+// -
// name: context[]
// in: formData
// required: true
@@ -183,6 +208,58 @@ func validateNormalizeUpdateFilter(form *apimodel.FilterUpdateRequestV2) error {
}
}
+ // Parse form variant of normal filter keyword update structs.
+ // All filter keyword update struct fields are optional.
+ numFormKeywords := max(
+ len(form.KeywordsAttributesID),
+ len(form.KeywordsAttributesKeyword),
+ len(form.KeywordsAttributesWholeWord),
+ len(form.KeywordsAttributesDestroy),
+ )
+ if numFormKeywords > 0 {
+ form.Keywords = make([]apimodel.FilterKeywordCreateUpdateDeleteRequest, 0, numFormKeywords)
+ for i := 0; i < numFormKeywords; i++ {
+ formKeyword := apimodel.FilterKeywordCreateUpdateDeleteRequest{}
+ if i < len(form.KeywordsAttributesID) && form.KeywordsAttributesID[i] != "" {
+ formKeyword.ID = &form.KeywordsAttributesID[i]
+ }
+ if i < len(form.KeywordsAttributesKeyword) && form.KeywordsAttributesKeyword[i] != "" {
+ formKeyword.Keyword = &form.KeywordsAttributesKeyword[i]
+ }
+ if i < len(form.KeywordsAttributesWholeWord) {
+ formKeyword.WholeWord = &form.KeywordsAttributesWholeWord[i]
+ }
+ if i < len(form.KeywordsAttributesDestroy) {
+ formKeyword.Destroy = &form.KeywordsAttributesDestroy[i]
+ }
+ form.Keywords = append(form.Keywords, formKeyword)
+ }
+ }
+
+ // Parse form variant of normal filter status update structs.
+ // All filter status update struct fields are optional.
+ numFormStatuses := max(
+ len(form.StatusesAttributesID),
+ len(form.StatusesAttributesStatusID),
+ len(form.StatusesAttributesDestroy),
+ )
+ if numFormStatuses > 0 {
+ form.Statuses = make([]apimodel.FilterStatusCreateDeleteRequest, 0, numFormStatuses)
+ for i := 0; i < numFormStatuses; i++ {
+ formStatus := apimodel.FilterStatusCreateDeleteRequest{}
+ if i < len(form.StatusesAttributesID) && form.StatusesAttributesID[i] != "" {
+ formStatus.ID = &form.StatusesAttributesID[i]
+ }
+ if i < len(form.StatusesAttributesStatusID) && form.StatusesAttributesStatusID[i] != "" {
+ formStatus.StatusID = &form.StatusesAttributesStatusID[i]
+ }
+ if i < len(form.StatusesAttributesDestroy) {
+ formStatus.Destroy = &form.StatusesAttributesDestroy[i]
+ }
+ form.Statuses = append(form.Statuses, formStatus)
+ }
+ }
+
// Normalize filter expiry if necessary.
// If we parsed this as JSON, expires_in
// may be either a float64 or a string.
@@ -204,5 +281,42 @@ func validateNormalizeUpdateFilter(form *apimodel.FilterUpdateRequestV2) error {
}
}
+ // Normalize and validate updates.
+ for i, formKeyword := range form.Keywords {
+ if formKeyword.Keyword != nil {
+ if err := validate.FilterKeyword(*formKeyword.Keyword); err != nil {
+ return err
+ }
+ }
+
+ destroy := util.PtrValueOr(formKeyword.Destroy, false)
+ form.Keywords[i].Destroy = &destroy
+
+ if destroy && formKeyword.ID == nil {
+ return errors.New("can't delete a filter keyword without an ID")
+ } else if formKeyword.ID == nil && formKeyword.Keyword == nil {
+ return errors.New("can't create a filter keyword without a keyword")
+ }
+ }
+ for i, formStatus := range form.Statuses {
+ if formStatus.StatusID != nil {
+ if err := validate.ULID(*formStatus.StatusID, "status_id"); err != nil {
+ return err
+ }
+ }
+
+ destroy := util.PtrValueOr(formStatus.Destroy, false)
+ form.Statuses[i].Destroy = &destroy
+
+ switch {
+ case destroy && formStatus.ID == nil:
+ return errors.New("can't delete a filter status without an ID")
+ case formStatus.ID != nil:
+ return errors.New("filter status IDs here can only be used to delete them")
+ case formStatus.StatusID == nil:
+ return errors.New("can't create a filter status without a status ID")
+ }
+ }
+
return nil
}
diff --git a/internal/api/client/filters/v2/filterput_test.go b/internal/api/client/filters/v2/filterput_test.go
index 6c1c315d1..d82d84b20 100644
--- a/internal/api/client/filters/v2/filterput_test.go
+++ b/internal/api/client/filters/v2/filterput_test.go
@@ -23,6 +23,7 @@ import (
"net/http"
"net/http/httptest"
"net/url"
+ "slices"
"strconv"
"strings"
@@ -35,7 +36,7 @@ import (
"github.com/superseriousbusiness/gotosocial/testrig"
)
-func (suite *FiltersTestSuite) putFilter(filterID string, title *string, context *[]string, action *string, expiresIn *int, requestJson *string, expectedHTTPStatus int, expectedBody string) (*apimodel.FilterV2, error) {
+func (suite *FiltersTestSuite) putFilter(filterID string, title *string, context *[]string, action *string, expiresIn *int, keywordsAttributesID *[]string, keywordsAttributesKeyword *[]string, keywordsAttributesWholeWord *[]bool, keywordsAttributesDestroy *[]bool, statusesAttributesID *[]string, statusesAttributesStatusID *[]string, statusesAttributesDestroy *[]bool, requestJson *string, expectedHTTPStatus int, expectedBody string) (*apimodel.FilterV2, error) {
// instantiate recorder + test context
recorder := httptest.NewRecorder()
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
@@ -64,6 +65,39 @@ func (suite *FiltersTestSuite) putFilter(filterID string, title *string, context
if expiresIn != nil {
ctx.Request.Form["expires_in"] = []string{strconv.Itoa(*expiresIn)}
}
+ if keywordsAttributesID != nil {
+ ctx.Request.Form["keywords_attributes[][id]"] = *keywordsAttributesID
+ }
+ 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 keywordsAttributesWholeWord != nil {
+ formatted := []string{}
+ for _, value := range *keywordsAttributesDestroy {
+ formatted = append(formatted, strconv.FormatBool(value))
+ }
+ ctx.Request.Form["keywords_attributes[][_destroy]"] = formatted
+ }
+ if statusesAttributesID != nil {
+ ctx.Request.Form["statuses_attributes[][id]"] = *statusesAttributesID
+ }
+ if statusesAttributesStatusID != nil {
+ ctx.Request.Form["statuses_attributes[][status_id]"] = *statusesAttributesStatusID
+ }
+ if statusesAttributesDestroy != nil {
+ formatted := []string{}
+ for _, value := range *statusesAttributesDestroy {
+ formatted = append(formatted, strconv.FormatBool(value))
+ }
+ ctx.Request.Form["statuses_attributes[][_destroy]"] = formatted
+ }
}
ctx.AddParam("id", filterID)
@@ -114,7 +148,18 @@ func (suite *FiltersTestSuite) TestPutFilterFull() {
context := []string{"home", "public"}
action := "hide"
expiresIn := 86400
- filter, err := suite.putFilter(id, &title, &context, &action, &expiresIn, nil, http.StatusOK, "")
+ // Tests attributes arrays that aren't the same length, just in case.
+ keywordsAttributesID := []string{
+ suite.testFilterKeywords["local_account_1_filter_2_keyword_1"].ID,
+ suite.testFilterKeywords["local_account_1_filter_2_keyword_2"].ID,
+ }
+ keywordsAttributesKeyword := []string{"fū", "", "blah"}
+ // If using the form version of this API, you have to always set whole_word to the previous value for that keyword;
+ // there's no way to represent a nullable boolean in it.
+ keywordsAttributesWholeWord := []bool{true, false, true}
+ keywordsAttributesDestroy := []bool{false, true}
+ statusesAttributesStatusID := []string{suite.testStatuses["remote_account_1_status_2"].ID}
+ filter, err := suite.putFilter(id, &title, &context, &action, &expiresIn, &keywordsAttributesID, &keywordsAttributesKeyword, &keywordsAttributesWholeWord, &keywordsAttributesDestroy, nil, &statusesAttributesStatusID, nil, nil, http.StatusOK, "")
if err != nil {
suite.FailNow(err.Error())
}
@@ -129,8 +174,29 @@ func (suite *FiltersTestSuite) TestPutFilterFull() {
if suite.NotNil(filter.ExpiresAt) {
suite.NotEmpty(*filter.ExpiresAt)
}
- suite.Len(filter.Keywords, 3)
- suite.Len(filter.Statuses, 0)
+
+ if suite.Len(filter.Keywords, 3) {
+ slices.SortFunc(filter.Keywords, func(lhs, rhs apimodel.FilterKeyword) int {
+ return strings.Compare(lhs.ID, rhs.ID)
+ })
+
+ suite.Equal("fū", filter.Keywords[0].Keyword)
+ suite.True(filter.Keywords[0].WholeWord)
+
+ suite.Equal("quux", filter.Keywords[1].Keyword)
+ suite.True(filter.Keywords[1].WholeWord)
+
+ suite.Equal("blah", filter.Keywords[2].Keyword)
+ suite.True(filter.Keywords[1].WholeWord)
+ }
+
+ if suite.Len(filter.Statuses, 1) {
+ slices.SortFunc(filter.Statuses, func(lhs, rhs apimodel.FilterStatus) int {
+ return strings.Compare(lhs.ID, rhs.ID)
+ })
+
+ suite.Equal(suite.testStatuses["remote_account_1_status_2"].ID, filter.Statuses[0].StatusID)
+ }
suite.checkStreamed(homeStream, true, "", stream.EventTypeFiltersChanged)
}
@@ -144,9 +210,28 @@ func (suite *FiltersTestSuite) TestPutFilterFullJSON() {
"title": "messy synoptic varblabbles",
"context": ["home", "public"],
"filter_action": "hide",
- "expires_in": 86400.1
+ "expires_in": 86400.1,
+ "keywords_attributes": [
+ {
+ "id": "01HN277Y11ENG4EC1ERMAC9FH4",
+ "keyword": "fū"
+ },
+ {
+ "id": "01HN278494N88BA2FY4DZ5JTNS",
+ "_destroy": true
+ },
+ {
+ "keyword": "blah",
+ "whole_word": true
+ }
+ ],
+ "statuses_attributes": [
+ {
+ "status_id": "01HEN2QRFA8H3C6QPN7RD4KSR6"
+ }
+ ]
}`
- filter, err := suite.putFilter(id, nil, nil, nil, nil, &requestJson, http.StatusOK, "")
+ filter, err := suite.putFilter(id, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, &requestJson, http.StatusOK, "")
if err != nil {
suite.FailNow(err.Error())
}
@@ -163,8 +248,29 @@ func (suite *FiltersTestSuite) TestPutFilterFullJSON() {
if suite.NotNil(filter.ExpiresAt) {
suite.NotEmpty(*filter.ExpiresAt)
}
- suite.Len(filter.Keywords, 3)
- suite.Len(filter.Statuses, 0)
+
+ if suite.Len(filter.Keywords, 3) {
+ slices.SortFunc(filter.Keywords, func(lhs, rhs apimodel.FilterKeyword) int {
+ return strings.Compare(lhs.ID, rhs.ID)
+ })
+
+ suite.Equal("fū", filter.Keywords[0].Keyword)
+ suite.True(filter.Keywords[0].WholeWord)
+
+ suite.Equal("quux", filter.Keywords[1].Keyword)
+ suite.True(filter.Keywords[1].WholeWord)
+
+ suite.Equal("blah", filter.Keywords[2].Keyword)
+ suite.True(filter.Keywords[1].WholeWord)
+ }
+
+ if suite.Len(filter.Statuses, 1) {
+ slices.SortFunc(filter.Statuses, func(lhs, rhs apimodel.FilterStatus) int {
+ return strings.Compare(lhs.ID, rhs.ID)
+ })
+
+ suite.Equal("01HEN2QRFA8H3C6QPN7RD4KSR6", filter.Statuses[0].StatusID)
+ }
suite.checkStreamed(homeStream, true, "", stream.EventTypeFiltersChanged)
}
@@ -175,7 +281,7 @@ func (suite *FiltersTestSuite) TestPutFilterMinimal() {
id := suite.testFilters["local_account_1_filter_1"].ID
title := "GNU/Linux"
context := []string{"home"}
- filter, err := suite.putFilter(id, &title, &context, nil, nil, nil, http.StatusOK, "")
+ filter, err := suite.putFilter(id, &title, &context, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusOK, "")
if err != nil {
suite.FailNow(err.Error())
}
@@ -196,7 +302,7 @@ func (suite *FiltersTestSuite) TestPutFilterEmptyTitle() {
id := suite.testFilters["local_account_1_filter_1"].ID
title := ""
context := []string{"home"}
- _, err := suite.putFilter(id, &title, &context, nil, nil, nil, http.StatusUnprocessableEntity, `{"error":"Unprocessable Entity: filter title must be provided, and must be no more than 200 chars"}`)
+ _, err := suite.putFilter(id, &title, &context, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, `{"error":"Unprocessable Entity: filter title must be provided, and must be no more than 200 chars"}`)
if err != nil {
suite.FailNow(err.Error())
}
@@ -206,7 +312,7 @@ func (suite *FiltersTestSuite) TestPutFilterEmptyContext() {
id := suite.testFilters["local_account_1_filter_1"].ID
title := "GNU/Linux"
context := []string{}
- _, err := suite.putFilter(id, &title, &context, nil, nil, nil, http.StatusUnprocessableEntity, `{"error":"Unprocessable Entity: at least one filter context is required"}`)
+ _, err := suite.putFilter(id, &title, &context, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusUnprocessableEntity, `{"error":"Unprocessable Entity: at least one filter context is required"}`)
if err != nil {
suite.FailNow(err.Error())
}
@@ -216,7 +322,7 @@ func (suite *FiltersTestSuite) TestPutFilterEmptyContext() {
func (suite *FiltersTestSuite) TestPutFilterTitleConflict() {
id := suite.testFilters["local_account_1_filter_1"].ID
title := suite.testFilters["local_account_1_filter_2"].Title
- _, err := suite.putFilter(id, &title, nil, nil, nil, nil, http.StatusConflict, `{"error":"Conflict: you already have a filter with this title"}`)
+ _, err := suite.putFilter(id, &title, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusConflict, `{"error":"Conflict: you already have a filter with this title"}`)
if err != nil {
suite.FailNow(err.Error())
}
@@ -226,7 +332,7 @@ func (suite *FiltersTestSuite) TestPutAnotherAccountsFilter() {
id := suite.testFilters["local_account_2_filter_1"].ID
title := "GNU/Linux"
context := []string{"home"}
- _, err := suite.putFilter(id, &title, &context, nil, nil, nil, http.StatusNotFound, `{"error":"Not Found"}`)
+ _, err := suite.putFilter(id, &title, &context, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusNotFound, `{"error":"Not Found"}`)
if err != nil {
suite.FailNow(err.Error())
}
@@ -236,7 +342,7 @@ 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, http.StatusNotFound, `{"error":"Not Found"}`)
+ _, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, http.StatusNotFound, `{"error":"Not Found"}`)
if err != nil {
suite.FailNow(err.Error())
}
diff --git a/internal/api/model/filterv2.go b/internal/api/model/filterv2.go
index 51dabacb2..242c569dc 100644
--- a/internal/api/model/filterv2.go
+++ b/internal/api/model/filterv2.go
@@ -135,9 +135,21 @@ type FilterCreateRequestV2 struct {
//
// Example: 86400
ExpiresInI interface{} `json:"expires_in"`
+
+ // Keywords to be added to the newly created filter.
+ Keywords []FilterKeywordCreateUpdateRequest `form:"-" json:"keywords_attributes" xml:"keywords_attributes"`
+ // Form data version of Keywords[].Keyword.
+ KeywordsAttributesKeyword []string `form:"keywords_attributes[][keyword]" json:"-" xml:"-"`
+ // Form data version of Keywords[].WholeWord.
+ KeywordsAttributesWholeWord []bool `form:"keywords_attributes[][whole_word]" json:"-" xml:"-"`
+
+ // Statuses to be added to the newly created filter.
+ Statuses []FilterStatusCreateRequest `form:"-" json:"statuses_attributes" xml:"statuses_attributes"`
+ // Form data version of Statuses[].StatusID.
+ StatusesAttributesStatusID []string `form:"statuses_attributes[][status_id]" json:"-" xml:"-"`
}
-// FilterKeywordCreateUpdateRequest captures params for creating or updating a filter keyword.
+// FilterKeywordCreateUpdateRequest captures params for creating or updating a filter keyword while creating a v2 filter or as a standalone operation.
//
// swagger:ignore
type FilterKeywordCreateUpdateRequest struct {
@@ -152,7 +164,7 @@ type FilterKeywordCreateUpdateRequest struct {
WholeWord *bool `form:"whole_word" json:"whole_word" xml:"whole_word"`
}
-// FilterStatusCreateRequest captures params for creating a filter status.
+// FilterStatusCreateRequest captures params for a status while creating a v2 filter or filter status.
//
// swagger:ignore
type FilterStatusCreateRequest struct {
@@ -188,4 +200,57 @@ type FilterUpdateRequestV2 struct {
//
// Example: 86400
ExpiresInI interface{} `json:"expires_in"`
+
+ // Keywords to be added to the filter, modified, or removed.
+ Keywords []FilterKeywordCreateUpdateDeleteRequest `form:"-" json:"keywords_attributes" xml:"keywords_attributes"`
+ // Form data version of Keywords[].ID.
+ KeywordsAttributesID []string `form:"keywords_attributes[][id]" json:"-" xml:"-"`
+ // Form data version of Keywords[].Keyword.
+ KeywordsAttributesKeyword []string `form:"keywords_attributes[][keyword]" json:"-" xml:"-"`
+ // Form data version of Keywords[].WholeWord.
+ KeywordsAttributesWholeWord []bool `form:"keywords_attributes[][whole_word]" json:"-" xml:"-"`
+ // Form data version of Keywords[].Destroy.
+ KeywordsAttributesDestroy []bool `form:"keywords_attributes[][_destroy]" json:"-" xml:"-"`
+
+ // Statuses to be added to the filter, or removed.
+ Statuses []FilterStatusCreateDeleteRequest `form:"-" json:"statuses_attributes" xml:"statuses_attributes"`
+ // Form data version of Statuses[].ID.
+ StatusesAttributesID []string `form:"statuses_attributes[][id]" json:"-" xml:"-"`
+ // Form data version of Statuses[].ID.
+ StatusesAttributesStatusID []string `form:"statuses_attributes[][status_id]" json:"-" xml:"-"`
+ // Form data version of Statuses[].Destroy.
+ StatusesAttributesDestroy []bool `form:"statuses_attributes[][_destroy]" json:"-" xml:"-"`
+}
+
+// FilterKeywordCreateUpdateDeleteRequest captures params for creating, updating, or deleting a keyword while updating a v2 filter.
+//
+// swagger:ignore
+type FilterKeywordCreateUpdateDeleteRequest struct {
+ // The ID of the filter keyword entry in the database.
+ // Optional: use to modify or delete an existing keyword instead of adding a new one.
+ ID *string `json:"id" xml:"id"`
+ // The text to be filtered.
+ //
+ // Example: fnord
+ // Maximum length: 40
+ Keyword *string `json:"keyword" xml:"keyword"`
+ // Should the filter keyword consider word boundaries?
+ //
+ // Example: true
+ WholeWord *bool `json:"whole_word" xml:"whole_word"`
+ // Remove this filter keyword. Requires an ID.
+ Destroy *bool `json:"_destroy" xml:"_destroy"`
+}
+
+// FilterStatusCreateDeleteRequest captures params for creating or deleting a status while updating a v2 filter.
+//
+// swagger:ignore
+type FilterStatusCreateDeleteRequest struct {
+ // The ID of the filter status entry in the database.
+ // Optional: use to delete an existing status instead of adding a new one.
+ ID *string `json:"id" xml:"id"`
+ // The status ID to be filtered.
+ StatusID *string `json:"status_id" xml:"status_id"`
+ // Remove this filter status. Requires an ID.
+ Destroy *bool `json:"_destroy" xml:"_destroy"`
}