[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:
Vyr Cossont
2024-06-14 01:11:41 -07:00
committed by GitHub
parent ee6e9b2795
commit b789fe2bc7
8 changed files with 656 additions and 40 deletions

View File

@ -63,6 +63,29 @@ func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, form
}
}
for _, formKeyword := range form.Keywords {
filterKeyword := &gtsmodel.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 := &gtsmodel.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")

View File

@ -27,6 +27,7 @@ import (
"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 := &gtsmodel.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 := &gtsmodel.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
}