[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`.
This commit is contained in:
parent
ee6e9b2795
commit
b789fe2bc7
|
@ -9245,6 +9245,27 @@ paths:
|
||||||
in: formData
|
in: formData
|
||||||
name: filter_action
|
name: filter_action
|
||||||
type: string
|
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:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
|
@ -9360,6 +9381,27 @@ paths:
|
||||||
name: title
|
name: title
|
||||||
required: true
|
required: true
|
||||||
type: string
|
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
|
- collectionFormat: multi
|
||||||
description: |-
|
description: |-
|
||||||
The contexts in which the filter should be applied.
|
The contexts in which the filter should be applied.
|
||||||
|
|
|
@ -100,6 +100,30 @@ import (
|
||||||
// - warn
|
// - warn
|
||||||
// - hide
|
// - hide
|
||||||
// default: warn
|
// 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:
|
// security:
|
||||||
// - OAuth2 Bearer:
|
// - OAuth2 Bearer:
|
||||||
|
@ -176,6 +200,30 @@ func validateNormalizeCreateFilter(form *apimodel.FilterCreateRequestV2) error {
|
||||||
return err
|
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.
|
// Apply defaults for missing fields.
|
||||||
form.FilterAction = util.Ptr(action)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -35,7 +36,7 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
"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
|
// instantiate recorder + test context
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||||
|
@ -64,6 +65,19 @@ func (suite *FiltersTestSuite) postFilter(title *string, context *[]string, acti
|
||||||
if expiresIn != nil {
|
if expiresIn != nil {
|
||||||
ctx.Request.Form["expires_in"] = []string{strconv.Itoa(*expiresIn)}
|
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
|
// trigger the handler
|
||||||
|
@ -111,7 +125,12 @@ func (suite *FiltersTestSuite) TestPostFilterFull() {
|
||||||
context := []string{"home", "public"}
|
context := []string{"home", "public"}
|
||||||
action := "warn"
|
action := "warn"
|
||||||
expiresIn := 86400
|
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 {
|
if err != nil {
|
||||||
suite.FailNow(err.Error())
|
suite.FailNow(err.Error())
|
||||||
}
|
}
|
||||||
|
@ -126,8 +145,25 @@ func (suite *FiltersTestSuite) TestPostFilterFull() {
|
||||||
if suite.NotNil(filter.ExpiresAt) {
|
if suite.NotNil(filter.ExpiresAt) {
|
||||||
suite.NotEmpty(*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)
|
suite.checkStreamed(homeStream, true, "", stream.EventTypeFiltersChanged)
|
||||||
}
|
}
|
||||||
|
@ -141,9 +177,27 @@ func (suite *FiltersTestSuite) TestPostFilterFullJSON() {
|
||||||
"context": ["home", "public"],
|
"context": ["home", "public"],
|
||||||
"filter_action": "warn",
|
"filter_action": "warn",
|
||||||
"whole_word": true,
|
"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 {
|
if err != nil {
|
||||||
suite.FailNow(err.Error())
|
suite.FailNow(err.Error())
|
||||||
}
|
}
|
||||||
|
@ -160,8 +214,28 @@ func (suite *FiltersTestSuite) TestPostFilterFullJSON() {
|
||||||
if suite.NotNil(filter.ExpiresAt) {
|
if suite.NotNil(filter.ExpiresAt) {
|
||||||
suite.NotEmpty(*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)
|
suite.checkStreamed(homeStream, true, "", stream.EventTypeFiltersChanged)
|
||||||
}
|
}
|
||||||
|
@ -171,7 +245,7 @@ func (suite *FiltersTestSuite) TestPostFilterMinimal() {
|
||||||
|
|
||||||
title := "GNU/Linux"
|
title := "GNU/Linux"
|
||||||
context := []string{"home"}
|
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 {
|
if err != nil {
|
||||||
suite.FailNow(err.Error())
|
suite.FailNow(err.Error())
|
||||||
}
|
}
|
||||||
|
@ -193,7 +267,7 @@ func (suite *FiltersTestSuite) TestPostFilterMinimal() {
|
||||||
func (suite *FiltersTestSuite) TestPostFilterEmptyTitle() {
|
func (suite *FiltersTestSuite) TestPostFilterEmptyTitle() {
|
||||||
title := ""
|
title := ""
|
||||||
context := []string{"home"}
|
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 {
|
if err != nil {
|
||||||
suite.FailNow(err.Error())
|
suite.FailNow(err.Error())
|
||||||
}
|
}
|
||||||
|
@ -201,7 +275,7 @@ func (suite *FiltersTestSuite) TestPostFilterEmptyTitle() {
|
||||||
|
|
||||||
func (suite *FiltersTestSuite) TestPostFilterMissingTitle() {
|
func (suite *FiltersTestSuite) TestPostFilterMissingTitle() {
|
||||||
context := []string{"home"}
|
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 {
|
if err != nil {
|
||||||
suite.FailNow(err.Error())
|
suite.FailNow(err.Error())
|
||||||
}
|
}
|
||||||
|
@ -210,7 +284,7 @@ func (suite *FiltersTestSuite) TestPostFilterMissingTitle() {
|
||||||
func (suite *FiltersTestSuite) TestPostFilterEmptyContext() {
|
func (suite *FiltersTestSuite) TestPostFilterEmptyContext() {
|
||||||
title := "GNU/Linux"
|
title := "GNU/Linux"
|
||||||
context := []string{}
|
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 {
|
if err != nil {
|
||||||
suite.FailNow(err.Error())
|
suite.FailNow(err.Error())
|
||||||
}
|
}
|
||||||
|
@ -218,7 +292,7 @@ func (suite *FiltersTestSuite) TestPostFilterEmptyContext() {
|
||||||
|
|
||||||
func (suite *FiltersTestSuite) TestPostFilterMissingContext() {
|
func (suite *FiltersTestSuite) TestPostFilterMissingContext() {
|
||||||
title := "GNU/Linux"
|
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 {
|
if err != nil {
|
||||||
suite.FailNow(err.Error())
|
suite.FailNow(err.Error())
|
||||||
}
|
}
|
||||||
|
@ -227,7 +301,7 @@ func (suite *FiltersTestSuite) TestPostFilterMissingContext() {
|
||||||
// Creating another filter with the same title should fail.
|
// Creating another filter with the same title should fail.
|
||||||
func (suite *FiltersTestSuite) TestPostFilterTitleConflict() {
|
func (suite *FiltersTestSuite) TestPostFilterTitleConflict() {
|
||||||
title := suite.testFilters["local_account_1_filter_1"].Title
|
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 {
|
if err != nil {
|
||||||
suite.FailNow(err.Error())
|
suite.FailNow(err.Error())
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
package v2
|
package v2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -68,6 +69,30 @@ import (
|
||||||
// minLength: 1
|
// minLength: 1
|
||||||
// maxLength: 200
|
// 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[]
|
// name: context[]
|
||||||
// in: formData
|
// in: formData
|
||||||
// required: true
|
// 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.
|
// Normalize filter expiry if necessary.
|
||||||
// If we parsed this as JSON, expires_in
|
// If we parsed this as JSON, expires_in
|
||||||
// may be either a float64 or a string.
|
// 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -35,7 +36,7 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
"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
|
// instantiate recorder + test context
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||||
|
@ -64,6 +65,39 @@ func (suite *FiltersTestSuite) putFilter(filterID string, title *string, context
|
||||||
if expiresIn != nil {
|
if expiresIn != nil {
|
||||||
ctx.Request.Form["expires_in"] = []string{strconv.Itoa(*expiresIn)}
|
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)
|
ctx.AddParam("id", filterID)
|
||||||
|
@ -114,7 +148,18 @@ func (suite *FiltersTestSuite) TestPutFilterFull() {
|
||||||
context := []string{"home", "public"}
|
context := []string{"home", "public"}
|
||||||
action := "hide"
|
action := "hide"
|
||||||
expiresIn := 86400
|
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 {
|
if err != nil {
|
||||||
suite.FailNow(err.Error())
|
suite.FailNow(err.Error())
|
||||||
}
|
}
|
||||||
|
@ -129,8 +174,29 @@ func (suite *FiltersTestSuite) TestPutFilterFull() {
|
||||||
if suite.NotNil(filter.ExpiresAt) {
|
if suite.NotNil(filter.ExpiresAt) {
|
||||||
suite.NotEmpty(*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)
|
suite.checkStreamed(homeStream, true, "", stream.EventTypeFiltersChanged)
|
||||||
}
|
}
|
||||||
|
@ -144,9 +210,28 @@ func (suite *FiltersTestSuite) TestPutFilterFullJSON() {
|
||||||
"title": "messy synoptic varblabbles",
|
"title": "messy synoptic varblabbles",
|
||||||
"context": ["home", "public"],
|
"context": ["home", "public"],
|
||||||
"filter_action": "hide",
|
"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 {
|
if err != nil {
|
||||||
suite.FailNow(err.Error())
|
suite.FailNow(err.Error())
|
||||||
}
|
}
|
||||||
|
@ -163,8 +248,29 @@ func (suite *FiltersTestSuite) TestPutFilterFullJSON() {
|
||||||
if suite.NotNil(filter.ExpiresAt) {
|
if suite.NotNil(filter.ExpiresAt) {
|
||||||
suite.NotEmpty(*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)
|
suite.checkStreamed(homeStream, true, "", stream.EventTypeFiltersChanged)
|
||||||
}
|
}
|
||||||
|
@ -175,7 +281,7 @@ func (suite *FiltersTestSuite) TestPutFilterMinimal() {
|
||||||
id := suite.testFilters["local_account_1_filter_1"].ID
|
id := suite.testFilters["local_account_1_filter_1"].ID
|
||||||
title := "GNU/Linux"
|
title := "GNU/Linux"
|
||||||
context := []string{"home"}
|
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 {
|
if err != nil {
|
||||||
suite.FailNow(err.Error())
|
suite.FailNow(err.Error())
|
||||||
}
|
}
|
||||||
|
@ -196,7 +302,7 @@ func (suite *FiltersTestSuite) TestPutFilterEmptyTitle() {
|
||||||
id := suite.testFilters["local_account_1_filter_1"].ID
|
id := suite.testFilters["local_account_1_filter_1"].ID
|
||||||
title := ""
|
title := ""
|
||||||
context := []string{"home"}
|
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 {
|
if err != nil {
|
||||||
suite.FailNow(err.Error())
|
suite.FailNow(err.Error())
|
||||||
}
|
}
|
||||||
|
@ -206,7 +312,7 @@ func (suite *FiltersTestSuite) TestPutFilterEmptyContext() {
|
||||||
id := suite.testFilters["local_account_1_filter_1"].ID
|
id := suite.testFilters["local_account_1_filter_1"].ID
|
||||||
title := "GNU/Linux"
|
title := "GNU/Linux"
|
||||||
context := []string{}
|
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 {
|
if err != nil {
|
||||||
suite.FailNow(err.Error())
|
suite.FailNow(err.Error())
|
||||||
}
|
}
|
||||||
|
@ -216,7 +322,7 @@ func (suite *FiltersTestSuite) TestPutFilterEmptyContext() {
|
||||||
func (suite *FiltersTestSuite) TestPutFilterTitleConflict() {
|
func (suite *FiltersTestSuite) TestPutFilterTitleConflict() {
|
||||||
id := suite.testFilters["local_account_1_filter_1"].ID
|
id := suite.testFilters["local_account_1_filter_1"].ID
|
||||||
title := suite.testFilters["local_account_1_filter_2"].Title
|
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 {
|
if err != nil {
|
||||||
suite.FailNow(err.Error())
|
suite.FailNow(err.Error())
|
||||||
}
|
}
|
||||||
|
@ -226,7 +332,7 @@ func (suite *FiltersTestSuite) TestPutAnotherAccountsFilter() {
|
||||||
id := suite.testFilters["local_account_2_filter_1"].ID
|
id := suite.testFilters["local_account_2_filter_1"].ID
|
||||||
title := "GNU/Linux"
|
title := "GNU/Linux"
|
||||||
context := []string{"home"}
|
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 {
|
if err != nil {
|
||||||
suite.FailNow(err.Error())
|
suite.FailNow(err.Error())
|
||||||
}
|
}
|
||||||
|
@ -236,7 +342,7 @@ func (suite *FiltersTestSuite) TestPutNonexistentFilter() {
|
||||||
id := "not_even_a_real_ULID"
|
id := "not_even_a_real_ULID"
|
||||||
phrase := "GNU/Linux"
|
phrase := "GNU/Linux"
|
||||||
context := []string{"home"}
|
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 {
|
if err != nil {
|
||||||
suite.FailNow(err.Error())
|
suite.FailNow(err.Error())
|
||||||
}
|
}
|
||||||
|
|
|
@ -135,9 +135,21 @@ type FilterCreateRequestV2 struct {
|
||||||
//
|
//
|
||||||
// Example: 86400
|
// Example: 86400
|
||||||
ExpiresInI interface{} `json:"expires_in"`
|
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
|
// swagger:ignore
|
||||||
type FilterKeywordCreateUpdateRequest struct {
|
type FilterKeywordCreateUpdateRequest struct {
|
||||||
|
@ -152,7 +164,7 @@ type FilterKeywordCreateUpdateRequest struct {
|
||||||
WholeWord *bool `form:"whole_word" json:"whole_word" xml:"whole_word"`
|
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
|
// swagger:ignore
|
||||||
type FilterStatusCreateRequest struct {
|
type FilterStatusCreateRequest struct {
|
||||||
|
@ -188,4 +200,57 @@ type FilterUpdateRequestV2 struct {
|
||||||
//
|
//
|
||||||
// Example: 86400
|
// Example: 86400
|
||||||
ExpiresInI interface{} `json:"expires_in"`
|
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"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,6 +63,29 @@ func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, form
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, formKeyword := range form.Keywords {
|
||||||
|
filterKeyword := >smodel.FilterKeyword{
|
||||||
|
ID: id.NewULID(),
|
||||||
|
AccountID: account.ID,
|
||||||
|
FilterID: filter.ID,
|
||||||
|
Filter: filter,
|
||||||
|
Keyword: formKeyword.Keyword,
|
||||||
|
WholeWord: formKeyword.WholeWord,
|
||||||
|
}
|
||||||
|
filter.Keywords = append(filter.Keywords, filterKeyword)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, formStatus := range form.Statuses {
|
||||||
|
filterStatus := >smodel.FilterStatus{
|
||||||
|
ID: id.NewULID(),
|
||||||
|
AccountID: account.ID,
|
||||||
|
FilterID: filter.ID,
|
||||||
|
Filter: filter,
|
||||||
|
StatusID: formStatus.StatusID,
|
||||||
|
}
|
||||||
|
filter.Statuses = append(filter.Statuses, filterStatus)
|
||||||
|
}
|
||||||
|
|
||||||
if err := p.state.DB.PutFilter(ctx, filter); err != nil {
|
if err := p.state.DB.PutFilter(ctx, filter); err != nil {
|
||||||
if errors.Is(err, db.ErrAlreadyExists) {
|
if errors.Is(err, db.ErrAlreadyExists) {
|
||||||
err = errors.New("duplicate title, keyword, or status")
|
err = errors.New("duplicate title, keyword, or status")
|
||||||
|
|
|
@ -27,6 +27,7 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||||
)
|
)
|
||||||
|
@ -39,6 +40,8 @@ func (p *Processor) Update(
|
||||||
filterID string,
|
filterID string,
|
||||||
form *apimodel.FilterUpdateRequestV2,
|
form *apimodel.FilterUpdateRequestV2,
|
||||||
) (*apimodel.FilterV2, gtserror.WithCode) {
|
) (*apimodel.FilterV2, gtserror.WithCode) {
|
||||||
|
var errWithCode gtserror.WithCode
|
||||||
|
|
||||||
// Get the filter by ID, with existing keywords and statuses.
|
// Get the filter by ID, with existing keywords and statuses.
|
||||||
filter, err := p.state.DB.GetFilterByID(ctx, filterID)
|
filter, err := p.state.DB.GetFilterByID(ctx, filterID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -103,13 +106,17 @@ func (p *Processor) Update(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temporarily detach keywords and statuses from filter, since we're not updating them below.
|
filterKeywordColumns, deleteFilterKeywordIDs, errWithCode := applyKeywordChanges(filter, form.Keywords)
|
||||||
filterKeywords := filter.Keywords
|
if err != nil {
|
||||||
filterStatuses := filter.Statuses
|
return nil, errWithCode
|
||||||
filter.Keywords = nil
|
}
|
||||||
filter.Statuses = nil
|
|
||||||
|
|
||||||
if err := p.state.DB.UpdateFilter(ctx, filter, filterColumns, nil, nil, nil); err != nil {
|
deleteFilterStatusIDs, errWithCode := applyStatusChanges(filter, form.Statuses)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errWithCode
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.state.DB.UpdateFilter(ctx, filter, filterColumns, filterKeywordColumns, deleteFilterKeywordIDs, deleteFilterStatusIDs); err != nil {
|
||||||
if errors.Is(err, db.ErrAlreadyExists) {
|
if errors.Is(err, db.ErrAlreadyExists) {
|
||||||
err = errors.New("you already have a filter with this title")
|
err = errors.New("you already have a filter with this title")
|
||||||
return nil, gtserror.NewErrorConflict(err, err.Error())
|
return nil, gtserror.NewErrorConflict(err, err.Error())
|
||||||
|
@ -117,10 +124,6 @@ func (p *Processor) Update(
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-attach keywords and statuses before returning.
|
|
||||||
filter.Keywords = filterKeywords
|
|
||||||
filter.Statuses = filterStatuses
|
|
||||||
|
|
||||||
apiFilter, errWithCode := p.apiFilter(ctx, filter)
|
apiFilter, errWithCode := p.apiFilter(ctx, filter)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
return nil, errWithCode
|
return nil, errWithCode
|
||||||
|
@ -131,3 +134,131 @@ func (p *Processor) Update(
|
||||||
|
|
||||||
return apiFilter, nil
|
return apiFilter, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// applyKeywordChanges applies the provided changes to the filter's keywords in place,
|
||||||
|
// and returns a list of lists of filter columns to update, and a list of filter keyword IDs to delete.
|
||||||
|
func applyKeywordChanges(filter *gtsmodel.Filter, formKeywords []apimodel.FilterKeywordCreateUpdateDeleteRequest) ([][]string, []string, gtserror.WithCode) {
|
||||||
|
if len(formKeywords) == 0 {
|
||||||
|
// Detach currently existing keywords from the filter so we don't change them.
|
||||||
|
filter.Keywords = nil
|
||||||
|
return nil, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteFilterKeywordIDs := []string{}
|
||||||
|
filterKeywordsByID := map[string]*gtsmodel.FilterKeyword{}
|
||||||
|
filterKeywordColumnsByID := map[string][]string{}
|
||||||
|
for _, filterKeyword := range filter.Keywords {
|
||||||
|
filterKeywordsByID[filterKeyword.ID] = filterKeyword
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, formKeyword := range formKeywords {
|
||||||
|
if formKeyword.ID != nil {
|
||||||
|
id := *formKeyword.ID
|
||||||
|
filterKeyword, ok := filterKeywordsByID[id]
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, gtserror.NewErrorNotFound(
|
||||||
|
fmt.Errorf("couldn't find filter keyword '%s' to update or delete", id),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process deletes.
|
||||||
|
if *formKeyword.Destroy {
|
||||||
|
delete(filterKeywordsByID, id)
|
||||||
|
deleteFilterKeywordIDs = append(deleteFilterKeywordIDs, id)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process updates.
|
||||||
|
columns := make([]string, 0, 2)
|
||||||
|
if formKeyword.Keyword != nil {
|
||||||
|
columns = append(columns, "keyword")
|
||||||
|
filterKeyword.Keyword = *formKeyword.Keyword
|
||||||
|
}
|
||||||
|
if formKeyword.WholeWord != nil {
|
||||||
|
columns = append(columns, "whole_word")
|
||||||
|
filterKeyword.WholeWord = formKeyword.WholeWord
|
||||||
|
}
|
||||||
|
filterKeywordColumnsByID[id] = columns
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process creates.
|
||||||
|
filterKeyword := >smodel.FilterKeyword{
|
||||||
|
ID: id.NewULID(),
|
||||||
|
AccountID: filter.AccountID,
|
||||||
|
FilterID: filter.ID,
|
||||||
|
Filter: filter,
|
||||||
|
Keyword: *formKeyword.Keyword,
|
||||||
|
WholeWord: util.Ptr(util.PtrValueOr(formKeyword.WholeWord, false)),
|
||||||
|
}
|
||||||
|
filterKeywordsByID[filterKeyword.ID] = filterKeyword
|
||||||
|
// Don't need to set columns, as we're using all of them.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace the filter's keywords list with our updated version.
|
||||||
|
filterKeywordColumns := [][]string{}
|
||||||
|
filter.Keywords = nil
|
||||||
|
for id, filterKeyword := range filterKeywordsByID {
|
||||||
|
filter.Keywords = append(filter.Keywords, filterKeyword)
|
||||||
|
// Okay to use the nil slice zero value for entries being created instead of updated.
|
||||||
|
filterKeywordColumns = append(filterKeywordColumns, filterKeywordColumnsByID[id])
|
||||||
|
}
|
||||||
|
|
||||||
|
return filterKeywordColumns, deleteFilterKeywordIDs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyKeywordChanges applies the provided changes to the filter's keywords in place,
|
||||||
|
// and returns a list of filter status IDs to delete.
|
||||||
|
func applyStatusChanges(filter *gtsmodel.Filter, formStatuses []apimodel.FilterStatusCreateDeleteRequest) ([]string, gtserror.WithCode) {
|
||||||
|
if len(formStatuses) == 0 {
|
||||||
|
// Detach currently existing statuses from the filter so we don't change them.
|
||||||
|
filter.Statuses = nil
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteFilterStatusIDs := []string{}
|
||||||
|
filterStatusesByID := map[string]*gtsmodel.FilterStatus{}
|
||||||
|
for _, filterStatus := range filter.Statuses {
|
||||||
|
filterStatusesByID[filterStatus.ID] = filterStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, formStatus := range formStatuses {
|
||||||
|
if formStatus.ID != nil {
|
||||||
|
id := *formStatus.ID
|
||||||
|
_, ok := filterStatusesByID[id]
|
||||||
|
if !ok {
|
||||||
|
return nil, gtserror.NewErrorNotFound(
|
||||||
|
fmt.Errorf("couldn't find filter status '%s' to delete", id),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process deletes.
|
||||||
|
if *formStatus.Destroy {
|
||||||
|
delete(filterStatusesByID, id)
|
||||||
|
deleteFilterStatusIDs = append(deleteFilterStatusIDs, id)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter statuses don't have updates.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process creates.
|
||||||
|
filterStatus := >smodel.FilterStatus{
|
||||||
|
ID: id.NewULID(),
|
||||||
|
AccountID: filter.AccountID,
|
||||||
|
FilterID: filter.ID,
|
||||||
|
Filter: filter,
|
||||||
|
StatusID: *formStatus.StatusID,
|
||||||
|
}
|
||||||
|
filterStatusesByID[filterStatus.ID] = filterStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace the filter's keywords list with our updated version.
|
||||||
|
filter.Statuses = nil
|
||||||
|
for _, filterStatus := range filterStatusesByID {
|
||||||
|
filter.Statuses = append(filter.Statuses, filterStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
return deleteFilterStatusIDs, nil
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue