diff options
Diffstat (limited to 'internal/processing/filters/v2')
| -rw-r--r-- | internal/processing/filters/v2/convert.go | 38 | ||||
| -rw-r--r-- | internal/processing/filters/v2/create.go | 94 | ||||
| -rw-r--r-- | internal/processing/filters/v2/delete.go | 29 | ||||
| -rw-r--r-- | internal/processing/filters/v2/filters.go | 8 | ||||
| -rw-r--r-- | internal/processing/filters/v2/get.go | 53 | ||||
| -rw-r--r-- | internal/processing/filters/v2/keywordcreate.go | 57 | ||||
| -rw-r--r-- | internal/processing/filters/v2/keyworddelete.go | 37 | ||||
| -rw-r--r-- | internal/processing/filters/v2/keywordget.go | 66 | ||||
| -rw-r--r-- | internal/processing/filters/v2/keywordupdate.go | 47 | ||||
| -rw-r--r-- | internal/processing/filters/v2/statuscreate.go | 63 | ||||
| -rw-r--r-- | internal/processing/filters/v2/statusdelete.go | 37 | ||||
| -rw-r--r-- | internal/processing/filters/v2/statusget.go | 66 | ||||
| -rw-r--r-- | internal/processing/filters/v2/update.go | 440 |
13 files changed, 559 insertions, 476 deletions
diff --git a/internal/processing/filters/v2/convert.go b/internal/processing/filters/v2/convert.go deleted file mode 100644 index 590edd04b..000000000 --- a/internal/processing/filters/v2/convert.go +++ /dev/null @@ -1,38 +0,0 @@ -// GoToSocial -// Copyright (C) GoToSocial Authors admin@gotosocial.org -// SPDX-License-Identifier: AGPL-3.0-or-later -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <http://www.gnu.org/licenses/>. - -package v2 - -import ( - "context" - "fmt" - - apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model" - "code.superseriousbusiness.org/gotosocial/internal/gtserror" - "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" -) - -// apiFilter is a shortcut to return the API v2 filter version of the given -// filter, or return an appropriate error if conversion fails. -func (p *Processor) apiFilter(ctx context.Context, filterKeyword *gtsmodel.Filter) (*apimodel.FilterV2, gtserror.WithCode) { - apiFilter, err := p.converter.FilterToAPIFilterV2(ctx, filterKeyword) - if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting filter to API v2 filter: %w", err)) - } - - return apiFilter, nil -} diff --git a/internal/processing/filters/v2/create.go b/internal/processing/filters/v2/create.go index c221e1539..d77c23424 100644 --- a/internal/processing/filters/v2/create.go +++ b/internal/processing/filters/v2/create.go @@ -20,7 +20,7 @@ package v2 import ( "context" "errors" - "fmt" + "net/http" "time" apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model" @@ -28,79 +28,85 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/gtserror" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/id" + "code.superseriousbusiness.org/gotosocial/internal/processing/filters/common" "code.superseriousbusiness.org/gotosocial/internal/typeutils" - "code.superseriousbusiness.org/gotosocial/internal/util" ) // Create a new filter for the given account, using the provided parameters. // These params should have already been validated by the time they reach this function. func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, form *apimodel.FilterCreateRequestV2) (*apimodel.FilterV2, gtserror.WithCode) { + var errWithCode gtserror.WithCode + + // Create new filter model. filter := >smodel.Filter{ ID: id.NewULID(), AccountID: account.ID, Title: form.Title, - Action: typeutils.APIFilterActionToFilterAction(*form.FilterAction), } - if form.ExpiresIn != nil && *form.ExpiresIn != 0 { - filter.ExpiresAt = time.Now().Add(time.Second * time.Duration(*form.ExpiresIn)) + + // Parse filter action from form and set on filter, checking for validity. + filter.Action = typeutils.APIFilterActionToFilterAction(*form.FilterAction) + if filter.Action == 0 { + const text = "invalid filter action" + return nil, gtserror.NewWithCode(http.StatusBadRequest, text) } - for _, context := range form.Context { - switch context { - case apimodel.FilterContextHome: - filter.ContextHome = util.Ptr(true) - case apimodel.FilterContextNotifications: - filter.ContextNotifications = util.Ptr(true) - case apimodel.FilterContextPublic: - filter.ContextPublic = util.Ptr(true) - case apimodel.FilterContextThread: - filter.ContextThread = util.Ptr(true) - case apimodel.FilterContextAccount: - filter.ContextAccount = util.Ptr(true) - default: - return nil, gtserror.NewErrorUnprocessableEntity( - fmt.Errorf("unsupported filter context '%s'", context), - ) - } + + // Parse contexts filter applies in from incoming request form data. + filter.Contexts, errWithCode = common.FromAPIContexts(form.Context) + if errWithCode != nil { + return nil, errWithCode + } + + // Check form for valid expiry and set on filter. + if form.ExpiresIn != nil && *form.ExpiresIn > 0 { + expiresIn := time.Duration(*form.ExpiresIn) * time.Second + filter.ExpiresAt = time.Now().Add(expiresIn) } - for _, formKeyword := range form.Keywords { + // Create new attached filter keywords. + for _, keyword := range form.Keywords { filterKeyword := >smodel.FilterKeyword{ ID: id.NewULID(), - AccountID: account.ID, FilterID: filter.ID, - Filter: filter, - Keyword: formKeyword.Keyword, - WholeWord: formKeyword.WholeWord, + Keyword: keyword.Keyword, + WholeWord: keyword.WholeWord, } + + // Append the new filter key word to filter itself. filter.Keywords = append(filter.Keywords, filterKeyword) + filter.KeywordIDs = append(filter.KeywordIDs, filterKeyword.ID) } - for _, formStatus := range form.Statuses { + // Create new attached filter statuses. + for _, status := range form.Statuses { filterStatus := >smodel.FilterStatus{ - ID: id.NewULID(), - AccountID: account.ID, - FilterID: filter.ID, - Filter: filter, - StatusID: formStatus.StatusID, + ID: id.NewULID(), + FilterID: filter.ID, + StatusID: status.StatusID, } + + // Append the new filter status to filter itself. filter.Statuses = append(filter.Statuses, filterStatus) + filter.StatusIDs = append(filter.StatusIDs, filterStatus.ID) } - if err := p.state.DB.PutFilter(ctx, filter); err != nil { - if errors.Is(err, db.ErrAlreadyExists) { - err = errors.New("duplicate title, keyword, or status") - return nil, gtserror.NewErrorConflict(err, err.Error()) - } - return nil, gtserror.NewErrorInternalError(err) - } + // Insert the new filter model into the database. + switch err := p.state.DB.PutFilter(ctx, filter); { + case err == nil: + // no issue - apiFilter, errWithCode := p.apiFilter(ctx, filter) - if errWithCode != nil { - return nil, errWithCode + case errors.Is(err, db.ErrAlreadyExists): + const text = "duplicate title, keyword or status" + return nil, gtserror.NewWithCode(http.StatusConflict, text) + + default: + err := gtserror.Newf("error inserting filter: %w", err) + return nil, gtserror.NewErrorInternalError(err) } // Send a filters changed event. p.stream.FiltersChanged(ctx, account) - return apiFilter, nil + // Return as converted frontend filter model. + return typeutils.FilterToAPIFilterV2(filter), nil } diff --git a/internal/processing/filters/v2/delete.go b/internal/processing/filters/v2/delete.go index b6a4c6321..ca3ade431 100644 --- a/internal/processing/filters/v2/delete.go +++ b/internal/processing/filters/v2/delete.go @@ -19,38 +19,33 @@ package v2 import ( "context" - "fmt" "code.superseriousbusiness.org/gotosocial/internal/gtserror" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" ) -// Delete an existing filter and all its attached keywords and statuses for the given account. +// Delete an existing filter and all its attached +// keywords and statuses for the given account. func (p *Processor) Delete( ctx context.Context, - account *gtsmodel.Account, + requester *gtsmodel.Account, filterID string, ) gtserror.WithCode { - // Get the filter for this keyword. - filter, err := p.state.DB.GetFilterByID(ctx, filterID) - if err != nil { - return gtserror.NewErrorNotFound(err) - } - // Check that the account owns it. - if filter.AccountID != account.ID { - return gtserror.NewErrorNotFound( - fmt.Errorf("filter %s doesn't belong to account %s", filter.ID, account.ID), - ) + // Get the filter with given ID, also checking ownership. + filter, errWithCode := p.c.GetFilter(ctx, requester, filterID) + if errWithCode != nil { + return errWithCode } - // Delete the entire filter. - if err := p.state.DB.DeleteFilterByID(ctx, filter.ID); err != nil { + // Delete filter from the database with all associated models. + if err := p.state.DB.DeleteFilter(ctx, filter); err != nil { + err := gtserror.Newf("error deleting filter: %w", err) return gtserror.NewErrorInternalError(err) } - // Send a filters changed event. - p.stream.FiltersChanged(ctx, account) + // Stream a filters changed event to WS. + p.stream.FiltersChanged(ctx, requester) return nil } diff --git a/internal/processing/filters/v2/filters.go b/internal/processing/filters/v2/filters.go index 82fef36b6..8c0ade1ca 100644 --- a/internal/processing/filters/v2/filters.go +++ b/internal/processing/filters/v2/filters.go @@ -18,19 +18,25 @@ package v2 import ( + "code.superseriousbusiness.org/gotosocial/internal/processing/filters/common" "code.superseriousbusiness.org/gotosocial/internal/processing/stream" "code.superseriousbusiness.org/gotosocial/internal/state" "code.superseriousbusiness.org/gotosocial/internal/typeutils" ) type Processor struct { + // embedded common logic + c *common.Processor + state *state.State converter *typeutils.Converter stream *stream.Processor } -func New(state *state.State, converter *typeutils.Converter, stream *stream.Processor) Processor { +func New(state *state.State, converter *typeutils.Converter, common *common.Processor, stream *stream.Processor) Processor { return Processor{ + c: common, + state: state, converter: converter, stream: stream, diff --git a/internal/processing/filters/v2/get.go b/internal/processing/filters/v2/get.go index 7240d1ba3..4cdf9e8ee 100644 --- a/internal/processing/filters/v2/get.go +++ b/internal/processing/filters/v2/get.go @@ -19,56 +19,43 @@ package v2 import ( "context" - "errors" - "fmt" "slices" "strings" apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model" - "code.superseriousbusiness.org/gotosocial/internal/db" "code.superseriousbusiness.org/gotosocial/internal/gtserror" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" + "code.superseriousbusiness.org/gotosocial/internal/typeutils" ) // Get looks up a filter by ID and returns it with keywords and statuses. -func (p *Processor) Get(ctx context.Context, account *gtsmodel.Account, filterID string) (*apimodel.FilterV2, gtserror.WithCode) { - filter, err := p.state.DB.GetFilterByID(ctx, filterID) - if err != nil { - if errors.Is(err, db.ErrNoEntries) { - return nil, gtserror.NewErrorNotFound(err) - } - return nil, gtserror.NewErrorInternalError(err) +func (p *Processor) Get(ctx context.Context, requester *gtsmodel.Account, filterID string) (*apimodel.FilterV2, gtserror.WithCode) { + filter, errWithCode := p.c.GetFilter(ctx, requester, filterID) + if errWithCode != nil { + return nil, errWithCode } - if filter.AccountID != account.ID { - return nil, gtserror.NewErrorNotFound( - fmt.Errorf("filter %s doesn't belong to account %s", filter.ID, account.ID), - ) - } - - return p.apiFilter(ctx, filter) + return typeutils.FilterToAPIFilterV2(filter), nil } // GetAll looks up all filters for the current account and returns them with keywords and statuses. -func (p *Processor) GetAll(ctx context.Context, account *gtsmodel.Account) ([]*apimodel.FilterV2, gtserror.WithCode) { - filters, err := p.state.DB.GetFiltersForAccountID( - ctx, - account.ID, - ) +func (p *Processor) GetAll(ctx context.Context, requester *gtsmodel.Account) ([]*apimodel.FilterV2, gtserror.WithCode) { + + // Get all filters belonging to this requester from the database. + filters, err := p.state.DB.GetFiltersByAccountID(ctx, requester.ID) if err != nil { - if errors.Is(err, db.ErrNoEntries) { - return nil, nil - } + err := gtserror.Newf("error getting account filters: %w", err) return nil, gtserror.NewErrorInternalError(err) } - apiFilters := make([]*apimodel.FilterV2, 0, len(filters)) - for _, filter := range filters { - apiFilter, errWithCode := p.apiFilter(ctx, filter) - if errWithCode != nil { - return nil, errWithCode - } - - apiFilters = append(apiFilters, apiFilter) + // Convert all these filters to frontend API models. + apiFilters := make([]*apimodel.FilterV2, len(filters)) + if len(apiFilters) != len(filters) { + // bound check eliminiation compiler-hint + panic(gtserror.New("BCE")) + } + for i, filter := range filters { + apiFilter := typeutils.FilterToAPIFilterV2(filter) + apiFilters[i] = apiFilter } // Sort them by ID so that they're in a stable order. diff --git a/internal/processing/filters/v2/keywordcreate.go b/internal/processing/filters/v2/keywordcreate.go index 89ada34f4..da91d5fd3 100644 --- a/internal/processing/filters/v2/keywordcreate.go +++ b/internal/processing/filters/v2/keywordcreate.go @@ -20,51 +20,60 @@ package v2 import ( "context" "errors" - "fmt" + "net/http" apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model" "code.superseriousbusiness.org/gotosocial/internal/db" - "code.superseriousbusiness.org/gotosocial/internal/gtscontext" "code.superseriousbusiness.org/gotosocial/internal/gtserror" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/id" + "code.superseriousbusiness.org/gotosocial/internal/typeutils" ) // KeywordCreate adds a filter keyword to an existing filter for the given account, using the provided parameters. // These params should have already been normalized and validated by the time they reach this function. -func (p *Processor) KeywordCreate(ctx context.Context, account *gtsmodel.Account, filterID string, form *apimodel.FilterKeywordCreateUpdateRequest) (*apimodel.FilterKeyword, gtserror.WithCode) { - // Check that the filter is owned by the given account. - filter, err := p.state.DB.GetFilterByID(gtscontext.SetBarebones(ctx), filterID) - if err != nil { - if errors.Is(err, db.ErrNoEntries) { - return nil, gtserror.NewErrorNotFound(err) - } - return nil, gtserror.NewErrorInternalError(err) - } - if filter.AccountID != account.ID { - return nil, gtserror.NewErrorNotFound( - fmt.Errorf("filter %s doesn't belong to account %s", filter.ID, account.ID), - ) +func (p *Processor) KeywordCreate(ctx context.Context, requester *gtsmodel.Account, filterID string, form *apimodel.FilterKeywordCreateUpdateRequest) (*apimodel.FilterKeyword, gtserror.WithCode) { + + // Get the filter with given ID, also checking ownership. + filter, errWithCode := p.c.GetFilter(ctx, requester, filterID) + if errWithCode != nil { + return nil, errWithCode } + // Create new filter keyword model. filterKeyword := >smodel.FilterKeyword{ ID: id.NewULID(), - AccountID: account.ID, FilterID: filter.ID, Keyword: form.Keyword, WholeWord: form.WholeWord, } - if err := p.state.DB.PutFilterKeyword(ctx, filterKeyword); err != nil { - if errors.Is(err, db.ErrAlreadyExists) { - err = errors.New("duplicate keyword") - return nil, gtserror.NewErrorConflict(err, err.Error()) - } + // Insert the new filter keyword model into the database. + switch err := p.state.DB.PutFilterKeyword(ctx, filterKeyword); { + case err == nil: + // no issue + + case errors.Is(err, db.ErrAlreadyExists): + const text = "duplicate keyword" + return nil, gtserror.NewWithCode(http.StatusConflict, text) + + default: + err := gtserror.Newf("error inserting filter keyword: %w", err) + return nil, gtserror.NewErrorInternalError(err) + } + + // Now update the filter it is attached to with new keyword. + filter.KeywordIDs = append(filter.KeywordIDs, filterKeyword.ID) + filter.Keywords = append(filter.Keywords, filterKeyword) + + // Update the existing filter model in the database (only the needed col). + if err := p.state.DB.UpdateFilter(ctx, filter, "keywords"); err != nil { + err := gtserror.Newf("error updating filter: %w", err) return nil, gtserror.NewErrorInternalError(err) } - // Send a filters changed event. - p.stream.FiltersChanged(ctx, account) + // Stream a filters changed event to WS. + p.stream.FiltersChanged(ctx, requester) - return p.converter.FilterKeywordToAPIFilterKeyword(ctx, filterKeyword), nil + return typeutils.FilterKeywordToAPIFilterKeyword(filterKeyword), nil } diff --git a/internal/processing/filters/v2/keyworddelete.go b/internal/processing/filters/v2/keyworddelete.go index 390c7b2cf..a0ec887e3 100644 --- a/internal/processing/filters/v2/keyworddelete.go +++ b/internal/processing/filters/v2/keyworddelete.go @@ -19,7 +19,7 @@ package v2 import ( "context" - "fmt" + "slices" "code.superseriousbusiness.org/gotosocial/internal/gtserror" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" @@ -28,29 +28,34 @@ import ( // KeywordDelete deletes an existing filter keyword from a filter. func (p *Processor) KeywordDelete( ctx context.Context, - account *gtsmodel.Account, - filterID string, + requester *gtsmodel.Account, + filterKeywordID string, ) gtserror.WithCode { - // Get the filter keyword. - filterKeyword, err := p.state.DB.GetFilterKeywordByID(ctx, filterID) - if err != nil { - return gtserror.NewErrorNotFound(err) + // Get filter keyword with given ID, also checking ownership to requester. + _, filter, errWithCode := p.c.GetFilterKeyword(ctx, requester, filterKeywordID) + if errWithCode != nil { + return errWithCode } - // Check that the account owns it. - if filterKeyword.AccountID != account.ID { - return gtserror.NewErrorNotFound( - fmt.Errorf("filter keyword %s doesn't belong to account %s", filterKeyword.ID, account.ID), - ) + // Delete this one filter keyword from the database, now ownership is confirmed. + if err := p.state.DB.DeleteFilterKeywordsByIDs(ctx, filterKeywordID); err != nil { + err := gtserror.Newf("error deleting filter keyword: %w", err) + return gtserror.NewErrorInternalError(err) } - // Delete the filter keyword. - if err := p.state.DB.DeleteFilterKeywordByID(ctx, filterKeyword.ID); err != nil { + // Delete this filter keyword from the slice of IDs attached to filter. + filter.KeywordIDs = slices.DeleteFunc(filter.KeywordIDs, func(id string) bool { + return filterKeywordID == id + }) + + // Update filter in the database now the keyword has been unattached. + if err := p.state.DB.UpdateFilter(ctx, filter, "keywords"); err != nil { + err := gtserror.Newf("error updating filter: %w", err) return gtserror.NewErrorInternalError(err) } - // Send a filters changed event. - p.stream.FiltersChanged(ctx, account) + // Stream a filters changed event to WS. + p.stream.FiltersChanged(ctx, requester) return nil } diff --git a/internal/processing/filters/v2/keywordget.go b/internal/processing/filters/v2/keywordget.go index e824b2e57..3cf120ed8 100644 --- a/internal/processing/filters/v2/keywordget.go +++ b/internal/processing/filters/v2/keywordget.go @@ -20,7 +20,6 @@ package v2 import ( "context" "errors" - "fmt" "slices" "strings" @@ -29,54 +28,47 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/gtscontext" "code.superseriousbusiness.org/gotosocial/internal/gtserror" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" + "code.superseriousbusiness.org/gotosocial/internal/typeutils" ) // KeywordGet looks up a filter keyword by ID. -func (p *Processor) KeywordGet(ctx context.Context, account *gtsmodel.Account, filterKeywordID string) (*apimodel.FilterKeyword, gtserror.WithCode) { - filterKeyword, err := p.state.DB.GetFilterKeywordByID(ctx, filterKeywordID) - if err != nil { - if errors.Is(err, db.ErrNoEntries) { - return nil, gtserror.NewErrorNotFound(err) - } - return nil, gtserror.NewErrorInternalError(err) - } - if filterKeyword.AccountID != account.ID { - return nil, gtserror.NewErrorNotFound( - fmt.Errorf("filter keyword %s doesn't belong to account %s", filterKeyword.ID, account.ID), - ) +func (p *Processor) KeywordGet(ctx context.Context, requester *gtsmodel.Account, filterKeywordID string) (*apimodel.FilterKeyword, gtserror.WithCode) { + filterKeyword, _, errWithCode := p.c.GetFilterKeyword(ctx, requester, filterKeywordID) + if errWithCode != nil { + return nil, errWithCode } - - return p.converter.FilterKeywordToAPIFilterKeyword(ctx, filterKeyword), nil + return typeutils.FilterKeywordToAPIFilterKeyword(filterKeyword), nil } // KeywordsGetForFilterID looks up all filter keywords for the given filter. -func (p *Processor) KeywordsGetForFilterID(ctx context.Context, account *gtsmodel.Account, filterID string) ([]*apimodel.FilterKeyword, gtserror.WithCode) { - // Check that the filter is owned by the given account. - filter, err := p.state.DB.GetFilterByID(gtscontext.SetBarebones(ctx), filterID) - if err != nil { - if errors.Is(err, db.ErrNoEntries) { - return nil, gtserror.NewErrorNotFound(err) - } - return nil, gtserror.NewErrorInternalError(err) - } - if filter.AccountID != account.ID { - return nil, gtserror.NewErrorNotFound(nil) - } +func (p *Processor) KeywordsGetForFilterID(ctx context.Context, requester *gtsmodel.Account, filterID string) ([]*apimodel.FilterKeyword, gtserror.WithCode) { - filterKeywords, err := p.state.DB.GetFilterKeywordsForFilterID( - ctx, - filter.ID, + // Get the filter with given ID (but + // without any sub-models attached). + filter, errWithCode := p.c.GetFilter( + gtscontext.SetBarebones(ctx), + requester, + filterID, ) - if err != nil { - if errors.Is(err, db.ErrNoEntries) { - return nil, nil - } + if errWithCode != nil { + return nil, errWithCode + } + + // Fetch all associated filter keywords to the determined existent filter. + filterKeywords, err := p.state.DB.GetFilterKeywordsByIDs(ctx, filter.KeywordIDs) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + err := gtserror.Newf("error getting filter keywords: %w", err) return nil, gtserror.NewErrorInternalError(err) } - apiFilterKeywords := make([]*apimodel.FilterKeyword, 0, len(filterKeywords)) - for _, filterKeyword := range filterKeywords { - apiFilterKeywords = append(apiFilterKeywords, p.converter.FilterKeywordToAPIFilterKeyword(ctx, filterKeyword)) + // Convert all of the filter keyword models from internal to frontend form. + apiFilterKeywords := make([]*apimodel.FilterKeyword, len(filterKeywords)) + if len(apiFilterKeywords) != len(filterKeywords) { + // bound check eliminiation compiler-hint + panic(gtserror.New("BCE")) + } + for i, filterKeyword := range filterKeywords { + apiFilterKeywords[i] = typeutils.FilterKeywordToAPIFilterKeyword(filterKeyword) } // Sort them by ID so that they're in a stable order. diff --git a/internal/processing/filters/v2/keywordupdate.go b/internal/processing/filters/v2/keywordupdate.go index 4c0a54b83..9d1e5bd0c 100644 --- a/internal/processing/filters/v2/keywordupdate.go +++ b/internal/processing/filters/v2/keywordupdate.go @@ -20,50 +20,51 @@ package v2 import ( "context" "errors" - "fmt" + "net/http" apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model" "code.superseriousbusiness.org/gotosocial/internal/db" - "code.superseriousbusiness.org/gotosocial/internal/gtscontext" "code.superseriousbusiness.org/gotosocial/internal/gtserror" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" + "code.superseriousbusiness.org/gotosocial/internal/typeutils" ) // KeywordUpdate updates an existing filter keyword for the given account, using the provided parameters. // These params should have already been validated by the time they reach this function. func (p *Processor) KeywordUpdate( ctx context.Context, - account *gtsmodel.Account, + requester *gtsmodel.Account, filterKeywordID string, form *apimodel.FilterKeywordCreateUpdateRequest, ) (*apimodel.FilterKeyword, gtserror.WithCode) { - // Get the filter keyword by ID. - filterKeyword, err := p.state.DB.GetFilterKeywordByID(gtscontext.SetBarebones(ctx), filterKeywordID) - if err != nil { - if errors.Is(err, db.ErrNoEntries) { - return nil, gtserror.NewErrorNotFound(err) - } - return nil, gtserror.NewErrorInternalError(err) - } - if filterKeyword.AccountID != account.ID { - return nil, gtserror.NewErrorNotFound( - fmt.Errorf("filter keyword %s doesn't belong to account %s", filterKeyword.ID, account.ID), - ) + + // Get the filter keyword with given ID, also checking ownership to requester. + filterKeyword, _, errWithCode := p.c.GetFilterKeyword(ctx, requester, filterKeywordID) + if errWithCode != nil { + return nil, errWithCode } + // Update the keyword model fields. filterKeyword.Keyword = form.Keyword filterKeyword.WholeWord = form.WholeWord - if err := p.state.DB.UpdateFilterKeyword(ctx, filterKeyword, "keyword", "whole_word"); err != nil { - if errors.Is(err, db.ErrAlreadyExists) { - err = errors.New("duplicate keyword") - return nil, gtserror.NewErrorConflict(err, err.Error()) - } + // Update existing filter keyword model in the database, (only necessary cols). + switch err := p.state.DB.UpdateFilterKeyword(ctx, filterKeyword, []string{ + "keyword", "whole_word"}...); { + case err == nil: + // no issue + + case errors.Is(err, db.ErrAlreadyExists): + const text = "duplicate keyword" + return nil, gtserror.NewWithCode(http.StatusConflict, text) + + default: + err := gtserror.Newf("error inserting filter keyword: %w", err) return nil, gtserror.NewErrorInternalError(err) } - // Send a filters changed event. - p.stream.FiltersChanged(ctx, account) + // Stream a filters changed event to WS. + p.stream.FiltersChanged(ctx, requester) - return p.converter.FilterKeywordToAPIFilterKeyword(ctx, filterKeyword), nil + return typeutils.FilterKeywordToAPIFilterKeyword(filterKeyword), nil } diff --git a/internal/processing/filters/v2/statuscreate.go b/internal/processing/filters/v2/statuscreate.go index 927986c69..1acab448c 100644 --- a/internal/processing/filters/v2/statuscreate.go +++ b/internal/processing/filters/v2/statuscreate.go @@ -20,50 +20,59 @@ package v2 import ( "context" "errors" - "fmt" + "net/http" apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model" "code.superseriousbusiness.org/gotosocial/internal/db" - "code.superseriousbusiness.org/gotosocial/internal/gtscontext" "code.superseriousbusiness.org/gotosocial/internal/gtserror" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/id" + "code.superseriousbusiness.org/gotosocial/internal/typeutils" ) // StatusCreate adds a filter status to an existing filter for the given account, using the provided parameters. // These params should have already been validated by the time they reach this function. -func (p *Processor) StatusCreate(ctx context.Context, account *gtsmodel.Account, filterID string, form *apimodel.FilterStatusCreateRequest) (*apimodel.FilterStatus, gtserror.WithCode) { - // Check that the filter is owned by the given account. - filter, err := p.state.DB.GetFilterByID(gtscontext.SetBarebones(ctx), filterID) - if err != nil { - if errors.Is(err, db.ErrNoEntries) { - return nil, gtserror.NewErrorNotFound(err) - } - return nil, gtserror.NewErrorInternalError(err) - } - if filter.AccountID != account.ID { - return nil, gtserror.NewErrorNotFound( - fmt.Errorf("filter %s doesn't belong to account %s", filter.ID, account.ID), - ) +func (p *Processor) StatusCreate(ctx context.Context, requester *gtsmodel.Account, filterID string, form *apimodel.FilterStatusCreateRequest) (*apimodel.FilterStatus, gtserror.WithCode) { + + // Get the filter with given ID, also checking ownership. + filter, errWithCode := p.c.GetFilter(ctx, requester, filterID) + if errWithCode != nil { + return nil, errWithCode } + // Create new filter status model. filterStatus := >smodel.FilterStatus{ - ID: id.NewULID(), - AccountID: account.ID, - FilterID: filter.ID, - StatusID: form.StatusID, + ID: id.NewULID(), + FilterID: filter.ID, + StatusID: form.StatusID, } - if err := p.state.DB.PutFilterStatus(ctx, filterStatus); err != nil { - if errors.Is(err, db.ErrAlreadyExists) { - err = errors.New("duplicate status") - return nil, gtserror.NewErrorConflict(err, err.Error()) - } + // Insert the new filter status model into the database. + switch err := p.state.DB.PutFilterStatus(ctx, filterStatus); { + case err == nil: + // no issue + + case errors.Is(err, db.ErrAlreadyExists): + const text = "duplicate status" + return nil, gtserror.NewWithCode(http.StatusConflict, text) + + default: + err := gtserror.Newf("error inserting filter status: %w", err) + return nil, gtserror.NewErrorInternalError(err) + } + + // Now update the filter it is attached to with new status. + filter.StatusIDs = append(filter.StatusIDs, filterStatus.ID) + filter.Statuses = append(filter.Statuses, filterStatus) + + // Update the existing filter model in the database (only the needed col). + if err := p.state.DB.UpdateFilter(ctx, filter, "statuses"); err != nil { + err := gtserror.Newf("error updating filter: %w", err) return nil, gtserror.NewErrorInternalError(err) } - // Send a filters changed event. - p.stream.FiltersChanged(ctx, account) + // Stream a filters changed event to WS. + p.stream.FiltersChanged(ctx, requester) - return p.converter.FilterStatusToAPIFilterStatus(ctx, filterStatus), nil + return typeutils.FilterStatusToAPIFilterStatus(filterStatus), nil } diff --git a/internal/processing/filters/v2/statusdelete.go b/internal/processing/filters/v2/statusdelete.go index e25f7279e..4309bac1a 100644 --- a/internal/processing/filters/v2/statusdelete.go +++ b/internal/processing/filters/v2/statusdelete.go @@ -19,7 +19,7 @@ package v2 import ( "context" - "fmt" + "slices" "code.superseriousbusiness.org/gotosocial/internal/gtserror" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" @@ -28,29 +28,34 @@ import ( // StatusDelete deletes an existing filter status from a filter. func (p *Processor) StatusDelete( ctx context.Context, - account *gtsmodel.Account, - filterID string, + requester *gtsmodel.Account, + filterStatusID string, ) gtserror.WithCode { - // Get the filter status. - filterStatus, err := p.state.DB.GetFilterStatusByID(ctx, filterID) - if err != nil { - return gtserror.NewErrorNotFound(err) + // Get filter status with given ID, also checking ownership to requester. + _, filter, errWithCode := p.c.GetFilterStatus(ctx, requester, filterStatusID) + if errWithCode != nil { + return errWithCode } - // Check that the account owns it. - if filterStatus.AccountID != account.ID { - return gtserror.NewErrorNotFound( - fmt.Errorf("filter status %s doesn't belong to account %s", filterStatus.ID, account.ID), - ) + // Delete this one filter status from the database, now ownership is confirmed. + if err := p.state.DB.DeleteFilterStatusesByIDs(ctx, filterStatusID); err != nil { + err := gtserror.Newf("error deleting filter status: %w", err) + return gtserror.NewErrorInternalError(err) } - // Delete the filter status. - if err := p.state.DB.DeleteFilterStatusByID(ctx, filterStatus.ID); err != nil { + // Delete this filter keyword from the slice of IDs attached to filter. + filter.StatusIDs = slices.DeleteFunc(filter.StatusIDs, func(id string) bool { + return filterStatusID == id + }) + + // Update filter in the database now the status has been unattached. + if err := p.state.DB.UpdateFilter(ctx, filter, "statuses"); err != nil { + err := gtserror.Newf("error updating filter: %w", err) return gtserror.NewErrorInternalError(err) } - // Send a filters changed event. - p.stream.FiltersChanged(ctx, account) + // Stream a filters changed event to WS. + p.stream.FiltersChanged(ctx, requester) return nil } diff --git a/internal/processing/filters/v2/statusget.go b/internal/processing/filters/v2/statusget.go index 06a56d271..7aa51f830 100644 --- a/internal/processing/filters/v2/statusget.go +++ b/internal/processing/filters/v2/statusget.go @@ -20,7 +20,6 @@ package v2 import ( "context" "errors" - "fmt" "slices" "strings" @@ -29,54 +28,47 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/gtscontext" "code.superseriousbusiness.org/gotosocial/internal/gtserror" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" + "code.superseriousbusiness.org/gotosocial/internal/typeutils" ) // StatusGet looks up a filter status by ID. -func (p *Processor) StatusGet(ctx context.Context, account *gtsmodel.Account, filterStatusID string) (*apimodel.FilterStatus, gtserror.WithCode) { - filterStatus, err := p.state.DB.GetFilterStatusByID(ctx, filterStatusID) - if err != nil { - if errors.Is(err, db.ErrNoEntries) { - return nil, gtserror.NewErrorNotFound(err) - } - return nil, gtserror.NewErrorInternalError(err) - } - if filterStatus.AccountID != account.ID { - return nil, gtserror.NewErrorNotFound( - fmt.Errorf("filter status %s doesn't belong to account %s", filterStatus.ID, account.ID), - ) +func (p *Processor) StatusGet(ctx context.Context, requester *gtsmodel.Account, filterStatusID string) (*apimodel.FilterStatus, gtserror.WithCode) { + filterStatus, _, errWithCode := p.c.GetFilterStatus(ctx, requester, filterStatusID) + if errWithCode != nil { + return nil, errWithCode } - - return p.converter.FilterStatusToAPIFilterStatus(ctx, filterStatus), nil + return typeutils.FilterStatusToAPIFilterStatus(filterStatus), nil } // StatusesGetForFilterID looks up all filter statuses for the given filter. -func (p *Processor) StatusesGetForFilterID(ctx context.Context, account *gtsmodel.Account, filterID string) ([]*apimodel.FilterStatus, gtserror.WithCode) { - // Check that the filter is owned by the given account. - filter, err := p.state.DB.GetFilterByID(gtscontext.SetBarebones(ctx), filterID) - if err != nil { - if errors.Is(err, db.ErrNoEntries) { - return nil, gtserror.NewErrorNotFound(err) - } - return nil, gtserror.NewErrorInternalError(err) - } - if filter.AccountID != account.ID { - return nil, gtserror.NewErrorNotFound(nil) - } +func (p *Processor) StatusesGetForFilterID(ctx context.Context, requester *gtsmodel.Account, filterID string) ([]*apimodel.FilterStatus, gtserror.WithCode) { - filterStatuses, err := p.state.DB.GetFilterStatusesForFilterID( - ctx, - filter.ID, + // Get the filter with given ID (but + // without any sub-models attached). + filter, errWithCode := p.c.GetFilter( + gtscontext.SetBarebones(ctx), + requester, + filterID, ) - if err != nil { - if errors.Is(err, db.ErrNoEntries) { - return nil, nil - } + if errWithCode != nil { + return nil, errWithCode + } + + // Fetch all associated filter statuses to the determined existent filter. + filterStatuses, err := p.state.DB.GetFilterStatusesByIDs(ctx, filter.StatusIDs) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + err := gtserror.Newf("error getting filter statuses: %w", err) return nil, gtserror.NewErrorInternalError(err) } - apiFilterStatuses := make([]*apimodel.FilterStatus, 0, len(filterStatuses)) - for _, filterStatus := range filterStatuses { - apiFilterStatuses = append(apiFilterStatuses, p.converter.FilterStatusToAPIFilterStatus(ctx, filterStatus)) + // Convert all of the filter status models from internal to frontend form. + apiFilterStatuses := make([]*apimodel.FilterStatus, len(filterStatuses)) + if len(apiFilterStatuses) != len(filterStatuses) { + // bound check eliminiation compiler-hint + panic(gtserror.New("BCE")) + } + for i, filterStatus := range filterStatuses { + apiFilterStatuses[i] = typeutils.FilterStatusToAPIFilterStatus(filterStatus) } // Sort them by ID so that they're in a stable order. diff --git a/internal/processing/filters/v2/update.go b/internal/processing/filters/v2/update.go index 9d38cac66..96a43612f 100644 --- a/internal/processing/filters/v2/update.go +++ b/internal/processing/filters/v2/update.go @@ -20,7 +20,8 @@ package v2 import ( "context" "errors" - "fmt" + "net/http" + "slices" "time" apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model" @@ -28,243 +29,356 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/gtserror" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/id" + "code.superseriousbusiness.org/gotosocial/internal/processing/filters/common" "code.superseriousbusiness.org/gotosocial/internal/typeutils" - "code.superseriousbusiness.org/gotosocial/internal/util" ) // Update an existing filter for the given account, using the provided parameters. // These params should have already been validated by the time they reach this function. func (p *Processor) Update( ctx context.Context, - account *gtsmodel.Account, + requester *gtsmodel.Account, 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 { - if errors.Is(err, db.ErrNoEntries) { - return nil, gtserror.NewErrorNotFound(err) - } - return nil, gtserror.NewErrorInternalError(err) - } - if filter.AccountID != account.ID { - return nil, gtserror.NewErrorNotFound( - fmt.Errorf("filter %s doesn't belong to account %s", filter.ID, account.ID), - ) + // Get the filter with given ID, also checking ownership. + filter, errWithCode := p.c.GetFilter(ctx, requester, filterID) + if errWithCode != nil { + return nil, errWithCode } - // Filter columns that we're going to update. - filterColumns := []string{} + // Filter columns that + // we're going to update. + cols := make([]string, 0, 6) - // Apply filter changes. + // Check for title change. if form.Title != nil { - filterColumns = append(filterColumns, "title") + cols = append(cols, "title") filter.Title = *form.Title } + + // Check action type change. if form.FilterAction != nil { - filterColumns = append(filterColumns, "action") + cols = append(cols, "action") + + // Parse filter action from form and set on filter, checking for validity. filter.Action = typeutils.APIFilterActionToFilterAction(*form.FilterAction) + if filter.Action == 0 { + const text = "invalid filter action" + return nil, gtserror.NewWithCode(http.StatusBadRequest, text) + } } + + // Check expiry change. if form.ExpiresIn != nil { - expiresIn := *form.ExpiresIn - filterColumns = append(filterColumns, "expires_at") - if expiresIn == 0 { - // Unset the expiration date. - filter.ExpiresAt = time.Time{} - } else { - // Update the expiration date. - filter.ExpiresAt = time.Now().Add(time.Second * time.Duration(expiresIn)) + cols = append(cols, "expires_at") + filter.ExpiresAt = time.Time{} + + // Check form for valid + // expiry and set on filter. + if *form.ExpiresIn > 0 { + expiresIn := time.Duration(*form.ExpiresIn) * time.Second + filter.ExpiresAt = time.Now().Add(expiresIn) } } + + // Check context change. if form.Context != nil { - filterColumns = append(filterColumns, - "context_home", - "context_notifications", - "context_public", - "context_thread", - "context_account", - ) - filter.ContextHome = util.Ptr(false) - filter.ContextNotifications = util.Ptr(false) - filter.ContextPublic = util.Ptr(false) - filter.ContextThread = util.Ptr(false) - filter.ContextAccount = util.Ptr(false) - for _, context := range *form.Context { - switch context { - case apimodel.FilterContextHome: - filter.ContextHome = util.Ptr(true) - case apimodel.FilterContextNotifications: - filter.ContextNotifications = util.Ptr(true) - case apimodel.FilterContextPublic: - filter.ContextPublic = util.Ptr(true) - case apimodel.FilterContextThread: - filter.ContextThread = util.Ptr(true) - case apimodel.FilterContextAccount: - filter.ContextAccount = util.Ptr(true) - default: - return nil, gtserror.NewErrorUnprocessableEntity( - fmt.Errorf("unsupported filter context '%s'", context), - ) - } + cols = append(cols, "contexts") + + // Parse contexts filter applies in from incoming request form data. + filter.Contexts, errWithCode = common.FromAPIContexts(*form.Context) + if errWithCode != nil { + return nil, errWithCode } } - filterKeywordColumns, deleteFilterKeywordIDs, errWithCode := applyKeywordChanges(filter, form.Keywords) - if err != nil { + // Check for any changes to attached keywords on filter. + keywordQs, errWithCode := p.updateFilterKeywords(ctx, + filter, form.Keywords) + if errWithCode != nil { return nil, errWithCode + } else if len(keywordQs.create) > 0 || len(keywordQs.delete) > 0 { + + // Attached keywords have changed. + cols = append(cols, "keywords") } - deleteFilterStatusIDs, errWithCode := applyStatusChanges(filter, form.Statuses) - if err != nil { + // Check for any changes to attached statuses on filter. + statusQs, errWithCode := p.updateFilterStatuses(ctx, + filter, form.Statuses) + if errWithCode != nil { return nil, errWithCode - } + } else if len(statusQs.create) > 0 || len(statusQs.delete) > 0 { - 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()) - } - return nil, gtserror.NewErrorInternalError(err) + // Attached statuses have changed. + cols = append(cols, "statuses") } - apiFilter, errWithCode := p.apiFilter(ctx, filter) + // Perform all the deferred database queries. + errWithCode = performTxs(keywordQs, statusQs) if errWithCode != nil { return nil, errWithCode } - // Send a filters changed event. - p.stream.FiltersChanged(ctx, account) + // Update the filter model in the database with determined cols. + switch err := p.state.DB.UpdateFilter(ctx, filter, cols...); { + case err == nil: + // no issue - return apiFilter, nil -} + case errors.Is(err, db.ErrAlreadyExists): + const text = "duplicate title" + return nil, gtserror.NewWithCode(http.StatusConflict, text) -// 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 + default: + err := gtserror.Newf("error updating filter: %w", err) + return nil, gtserror.NewErrorInternalError(err) } - deleteFilterKeywordIDs := []string{} - filterKeywordsByID := map[string]*gtsmodel.FilterKeyword{} - filterKeywordColumnsByID := map[string][]string{} - for _, filterKeyword := range filter.Keywords { - filterKeywordsByID[filterKeyword.ID] = filterKeyword + // Stream a filters changed event to WS. + p.stream.FiltersChanged(ctx, requester) + + // Return as converted frontend filter model. + return typeutils.FilterToAPIFilterV2(filter), nil +} + +func (p *Processor) updateFilterKeywords(ctx context.Context, filter *gtsmodel.Filter, form []apimodel.FilterKeywordCreateUpdateDeleteRequest) (deferredQs, gtserror.WithCode) { + if len(form) == 0 { + // No keyword changes. + return deferredQs{}, nil } - 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), - ) + var deferred deferredQs + for _, request := range form { + if request.ID != nil { + // Look by ID for keyword attached to filter. + idx := slices.IndexFunc(filter.Keywords, + func(f *gtsmodel.FilterKeyword) bool { + return f.ID == (*request.ID) + }) + if idx == -1 { + const text = "filter keyword not found" + return deferred, gtserror.NewWithCode(http.StatusNotFound, text) } - // Process deletes. - if *formKeyword.Destroy { - delete(filterKeywordsByID, id) - deleteFilterKeywordIDs = append(deleteFilterKeywordIDs, id) + // If this is a delete, update filter's id list. + if request.Destroy != nil && *request.Destroy { + filter.Keywords = slices.Delete(filter.Keywords, idx, idx+1) + filter.KeywordIDs = slices.Delete(filter.KeywordIDs, idx, idx+1) + + // Append database delete to funcs for later processing by caller. + deferred.delete = append(deferred.delete, func() gtserror.WithCode { + if err := p.state.DB.DeleteFilterKeywordsByIDs(ctx, *request.ID); // + err != nil { + err := gtserror.Newf("error deleting filter keyword: %w", err) + return gtserror.NewErrorInternalError(err) + } + return nil + }) continue } - // Process updates. - columns := make([]string, 0, 2) - if formKeyword.Keyword != nil { - columns = append(columns, "keyword") - filterKeyword.Keyword = *formKeyword.Keyword + // Get the filter keyword at index. + filterKeyword := filter.Keywords[idx] + + // Filter keywords database + // columns we need to update. + cols := make([]string, 0, 2) + + // Check for changes to keyword string. + if val := request.Keyword; val != nil { + cols = append(cols, "keyword") + filterKeyword.Keyword = *val + } + + // Check for changes to wholeword flag. + if val := request.WholeWord; val != nil { + cols = append(cols, "whole_word") + filterKeyword.WholeWord = val } - if formKeyword.WholeWord != nil { - columns = append(columns, "whole_word") - filterKeyword.WholeWord = formKeyword.WholeWord + + // Verify that this is valid regular expression. + if err := filterKeyword.Compile(); err != nil { + const text = "invalid regular expression" + err := gtserror.Newf("invalid regular expression: %w", err) + return deferred, gtserror.NewWithCodeSafe( + http.StatusBadRequest, + err, text, + ) + } + + if len(cols) > 0 { + // Append database update to funcs for later processing by caller. + deferred.update = append(deferred.update, func() gtserror.WithCode { + if err := p.state.DB.UpdateFilterKeyword(ctx, filterKeyword, cols...); // + err != nil { + if errors.Is(err, db.ErrAlreadyExists) { + const text = "duplicate keyword" + return gtserror.NewWithCode(http.StatusConflict, text) + } + err := gtserror.Newf("error updating filter keyword: %w", err) + return gtserror.NewErrorInternalError(err) + } + return nil + }) } - filterKeywordColumnsByID[id] = columns + continue } - // Process creates. + // Check for valid request. + if request.Keyword == nil { + const text = "missing keyword" + return deferred, gtserror.NewWithCode(http.StatusBadRequest, text) + } + + // Create new filter keyword for insert. filterKeyword := >smodel.FilterKeyword{ ID: id.NewULID(), - AccountID: filter.AccountID, FilterID: filter.ID, - Filter: filter, - Keyword: *formKeyword.Keyword, - WholeWord: util.Ptr(util.PtrOrValue(formKeyword.WholeWord, false)), + Keyword: *request.Keyword, + WholeWord: request.WholeWord, } - 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 { + // Verify that this is valid regular expression. + if err := filterKeyword.Compile(); err != nil { + const text = "invalid regular expression" + err := gtserror.Newf("invalid regular expression: %w", err) + return deferred, gtserror.NewWithCodeSafe( + http.StatusBadRequest, + err, text, + ) + } + + // Append new filter keyword to filter and list of IDs. 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]) + filter.KeywordIDs = append(filter.KeywordIDs, filterKeyword.ID) + + // Append database insert to funcs for later processing by caller. + deferred.create = append(deferred.create, func() gtserror.WithCode { + if err := p.state.DB.PutFilterKeyword(ctx, filterKeyword); // + err != nil { + if errors.Is(err, db.ErrAlreadyExists) { + const text = "duplicate keyword" + return gtserror.NewWithCode(http.StatusConflict, text) + } + err := gtserror.Newf("error inserting filter keyword: %w", err) + return gtserror.NewErrorInternalError(err) + } + return nil + }) } - return filterKeywordColumns, deleteFilterKeywordIDs, nil + return deferred, 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 +func (p *Processor) updateFilterStatuses(ctx context.Context, filter *gtsmodel.Filter, form []apimodel.FilterStatusCreateDeleteRequest) (deferredQs, gtserror.WithCode) { + if len(form) == 0 { + // No keyword changes. + return deferredQs{}, nil } - 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), - ) + var deferred deferredQs + for _, request := range form { + if request.ID != nil { + // Look by ID for status attached to filter. + idx := slices.IndexFunc(filter.Statuses, + func(f *gtsmodel.FilterStatus) bool { + return f.ID == *request.ID + }) + if idx == -1 { + const text = "filter status not found" + return deferred, gtserror.NewWithCode(http.StatusNotFound, text) } - // Process deletes. - if *formStatus.Destroy { - delete(filterStatusesByID, id) - deleteFilterStatusIDs = append(deleteFilterStatusIDs, id) - continue - } + // If this is a delete, update filter's id list. + if request.Destroy != nil && *request.Destroy { + filter.Statuses = slices.Delete(filter.Statuses, idx, idx+1) + filter.StatusIDs = slices.Delete(filter.StatusIDs, idx, idx+1) - // Filter statuses don't have updates. + // Append database delete to funcs for later processing by caller. + deferred.delete = append(deferred.delete, func() gtserror.WithCode { + if err := p.state.DB.DeleteFilterStatusesByIDs(ctx, *request.ID); // + err != nil { + err := gtserror.Newf("error deleting filter status: %w", err) + return gtserror.NewErrorInternalError(err) + } + return nil + }) + } continue } - // Process creates. + // Check for valid request. + if request.StatusID == nil { + const text = "missing status" + return deferred, gtserror.NewWithCode(http.StatusBadRequest, text) + } + + // Create new filter status for insert. filterStatus := >smodel.FilterStatus{ - ID: id.NewULID(), - AccountID: filter.AccountID, - FilterID: filter.ID, - Filter: filter, - StatusID: *formStatus.StatusID, + ID: id.NewULID(), + FilterID: filter.ID, + StatusID: *request.StatusID, } - filterStatusesByID[filterStatus.ID] = filterStatus - } - // Replace the filter's keywords list with our updated version. - filter.Statuses = nil - for _, filterStatus := range filterStatusesByID { + // Append new filter status to filter and list of IDs. filter.Statuses = append(filter.Statuses, filterStatus) + filter.StatusIDs = append(filter.StatusIDs, filterStatus.ID) + + // Append database insert to funcs for later processing by caller. + deferred.create = append(deferred.create, func() gtserror.WithCode { + if err := p.state.DB.PutFilterStatus(ctx, filterStatus); // + err != nil { + if errors.Is(err, db.ErrAlreadyExists) { + const text = "duplicate status" + return gtserror.NewWithCode(http.StatusConflict, text) + } + err := gtserror.Newf("error inserting filter status: %w", err) + return gtserror.NewErrorInternalError(err) + } + return nil + }) + } + + return deferred, nil +} + +// deferredQs stores selection of +// deferred database queries. +type deferredQs struct { + create []func() gtserror.WithCode + update []func() gtserror.WithCode + delete []func() gtserror.WithCode +} + +// performTx performs the passed deferredQs functions, +// prioritising create / update operations before deletes. +func performTxs(queries ...deferredQs) gtserror.WithCode { + + // Perform create / update + // operations before anything. + for _, q := range queries { + for _, create := range q.create { + if errWithCode := create(); errWithCode != nil { + return errWithCode + } + } + for _, update := range q.update { + if errWithCode := update(); errWithCode != nil { + return errWithCode + } + } + } + + // Perform deletes last. + for _, q := range queries { + for _, delete := range q.delete { + if errWithCode := delete(); errWithCode != nil { + return errWithCode + } + } } - return deleteFilterStatusIDs, nil + return nil } |
