mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-12-22 10:12:11 +00:00
[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
|
||||
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:
|
||||
|
@ -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.
|
||||
|
|
|
@ -100,6 +100,30 @@
|
|||
// - 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
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
|
@ -35,7 +36,7 @@
|
|||
"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())
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
package v2
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
@ -68,6 +69,30 @@
|
|||
// 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
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
|
@ -35,7 +36,7 @@
|
|||
"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())
|
||||
}
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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 errors.Is(err, db.ErrAlreadyExists) {
|
||||
err = errors.New("duplicate title, keyword, or status")
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
)
|
||||
|
@ -39,6 +40,8 @@ func (p *Processor) Update(
|
|||
filterID string,
|
||||
form *apimodel.FilterUpdateRequestV2,
|
||||
) (*apimodel.FilterV2, gtserror.WithCode) {
|
||||
var errWithCode gtserror.WithCode
|
||||
|
||||
// Get the filter by ID, with existing keywords and statuses.
|
||||
filter, err := p.state.DB.GetFilterByID(ctx, filterID)
|
||||
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.
|
||||
filterKeywords := filter.Keywords
|
||||
filterStatuses := filter.Statuses
|
||||
filter.Keywords = nil
|
||||
filter.Statuses = nil
|
||||
filterKeywordColumns, deleteFilterKeywordIDs, errWithCode := applyKeywordChanges(filter, form.Keywords)
|
||||
if err != nil {
|
||||
return nil, errWithCode
|
||||
}
|
||||
|
||||
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) {
|
||||
err = errors.New("you already have a filter with this title")
|
||||
return nil, gtserror.NewErrorConflict(err, err.Error())
|
||||
|
@ -117,10 +124,6 @@ func (p *Processor) Update(
|
|||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
// Re-attach keywords and statuses before returning.
|
||||
filter.Keywords = filterKeywords
|
||||
filter.Statuses = filterStatuses
|
||||
|
||||
apiFilter, errWithCode := p.apiFilter(ctx, filter)
|
||||
if errWithCode != nil {
|
||||
return nil, errWithCode
|
||||
|
@ -131,3 +134,131 @@ func (p *Processor) Update(
|
|||
|
||||
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 a new issue