diff options
Diffstat (limited to 'internal/processing')
40 files changed, 1015 insertions, 773 deletions
diff --git a/internal/processing/account/bookmarks.go b/internal/processing/account/bookmarks.go index 329bcf30c..e6f0886f9 100644 --- a/internal/processing/account/bookmarks.go +++ b/internal/processing/account/bookmarks.go @@ -23,7 +23,6 @@ import ( apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model" "code.superseriousbusiness.org/gotosocial/internal/db" - statusfilter "code.superseriousbusiness.org/gotosocial/internal/filter/status" "code.superseriousbusiness.org/gotosocial/internal/gtserror" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/log" @@ -75,7 +74,7 @@ func (p *Processor) BookmarksGet(ctx context.Context, requestingAccount *gtsmode } // Convert the status. - item, err := p.converter.StatusToAPIStatus(ctx, status, requestingAccount, statusfilter.FilterContextNone, nil) + item, err := p.converter.StatusToAPIStatus(ctx, status, requestingAccount, gtsmodel.FilterContextNone, nil) if err != nil { log.Errorf(ctx, "error converting bookmarked status to api: %s", err) continue diff --git a/internal/processing/account/statuses.go b/internal/processing/account/statuses.go index 0ff9ef7e1..3b56750c5 100644 --- a/internal/processing/account/statuses.go +++ b/internal/processing/account/statuses.go @@ -24,7 +24,6 @@ import ( apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model" "code.superseriousbusiness.org/gotosocial/internal/db" - statusfilter "code.superseriousbusiness.org/gotosocial/internal/filter/status" "code.superseriousbusiness.org/gotosocial/internal/gtserror" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/log" @@ -97,7 +96,7 @@ func (p *Processor) StatusesGet( return nil, gtserror.NewErrorInternalError(err) } - filters, err := p.state.DB.GetFiltersForAccountID(ctx, requestingAccount.ID) + filters, err := p.state.DB.GetFiltersByAccountID(ctx, requestingAccount.ID) if err != nil { err = gtserror.Newf("couldn't retrieve filters for account %s: %w", requestingAccount.ID, err) return nil, gtserror.NewErrorInternalError(err) @@ -105,7 +104,7 @@ func (p *Processor) StatusesGet( for _, s := range filtered { // Convert filtered statuses to API statuses. - item, err := p.converter.StatusToAPIStatus(ctx, s, requestingAccount, statusfilter.FilterContextAccount, filters) + item, err := p.converter.StatusToAPIStatus(ctx, s, requestingAccount, gtsmodel.FilterContextAccount, filters) if err != nil { log.Errorf(ctx, "error convering to api status: %v", err) continue diff --git a/internal/processing/common/status.go b/internal/processing/common/status.go index 441a58384..83acddc84 100644 --- a/internal/processing/common/status.go +++ b/internal/processing/common/status.go @@ -213,7 +213,7 @@ func (p *Processor) GetAPIStatus( apiStatus, err := p.converter.StatusToAPIStatus(ctx, target, requester, - statusfilter.FilterContextNone, + gtsmodel.FilterContextNone, nil, ) if err != nil { @@ -234,7 +234,7 @@ func (p *Processor) GetVisibleAPIStatuses( ctx context.Context, requester *gtsmodel.Account, statuses []*gtsmodel.Status, - filterContext statusfilter.FilterContext, + filterCtx gtsmodel.FilterContext, filters []*gtsmodel.Filter, ) []apimodel.Status { @@ -277,7 +277,7 @@ func (p *Processor) GetVisibleAPIStatuses( apiStatus, err := p.converter.StatusToAPIStatus(ctx, status, requester, - filterContext, + filterCtx, filters, ) if err != nil && !errors.Is(err, statusfilter.ErrHideStatus) { diff --git a/internal/processing/conversations/conversations.go b/internal/processing/conversations/conversations.go index e31f60500..70fafa437 100644 --- a/internal/processing/conversations/conversations.go +++ b/internal/processing/conversations/conversations.go @@ -101,7 +101,7 @@ func (p *Processor) getFilters( ctx context.Context, requestingAccount *gtsmodel.Account, ) ([]*gtsmodel.Filter, gtserror.WithCode) { - filters, err := p.state.DB.GetFiltersForAccountID(ctx, requestingAccount.ID) + filters, err := p.state.DB.GetFiltersByAccountID(ctx, requestingAccount.ID) if err != nil { return nil, gtserror.NewErrorInternalError( gtserror.Newf( diff --git a/internal/processing/filters/common/common.go b/internal/processing/filters/common/common.go new file mode 100644 index 000000000..a119d3bd4 --- /dev/null +++ b/internal/processing/filters/common/common.go @@ -0,0 +1,184 @@ +// 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 common + +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/state" +) + +type Processor struct{ state *state.State } + +func New(state *state.State) *Processor { return &Processor{state} } + +// CheckFilterExists calls .GetFilter() with a barebones context to not +// fetch any sub-models, and not returning the result. this functionally +// just uses .GetFilter() for the ownership and existence checks. +func (p *Processor) CheckFilterExists( + ctx context.Context, + requester *gtsmodel.Account, + id string, +) gtserror.WithCode { + _, errWithCode := p.GetFilter(gtscontext.SetBarebones(ctx), requester, id) + return errWithCode +} + +// GetFilter fetches the filter with given ID, also checking +// the given requesting account is the owner of the filter. +func (p *Processor) GetFilter( + ctx context.Context, + requester *gtsmodel.Account, + id string, +) ( + *gtsmodel.Filter, + gtserror.WithCode, +) { + // Get the filter from the database with given ID. + filter, err := p.state.DB.GetFilterByID(ctx, id) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + err := gtserror.Newf("error getting filter: %w", err) + return nil, gtserror.NewErrorInternalError(err) + } + + // Check it exists. + if filter == nil { + const text = "filter not found" + return nil, gtserror.NewWithCode(http.StatusNotFound, text) + } + + // Check that the requester owns it. + if filter.AccountID != requester.ID { + const text = "filter not found" + err := gtserror.New("filter does not belong to account") + return nil, gtserror.NewErrorNotFound(err, text) + } + + return filter, nil +} + +// GetFilterStatus fetches the filter status with given ID, also +// checking the given requesting account is the owner of it. +func (p *Processor) GetFilterStatus( + ctx context.Context, + requester *gtsmodel.Account, + id string, +) ( + *gtsmodel.FilterStatus, + *gtsmodel.Filter, + gtserror.WithCode, +) { + + // Get the filter status from the database with given ID. + filterStatus, err := p.state.DB.GetFilterStatusByID(ctx, id) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + err := gtserror.Newf("error getting filter status: %w", err) + return nil, nil, gtserror.NewErrorInternalError(err) + } + + // Check it even exists. + if filterStatus == nil { + const text = "filter status not found" + return nil, nil, gtserror.NewWithCode(http.StatusNotFound, text) + } + + // Get the filter this filter status is + // associated with, without sub-models. + // (this also checks filter ownership). + filter, errWithCode := p.GetFilter( + gtscontext.SetBarebones(ctx), + requester, + filterStatus.FilterID, + ) + if errWithCode != nil { + return nil, nil, errWithCode + } + + return filterStatus, filter, nil +} + +// GetFilterKeyword fetches the filter keyword with given ID, +// also checking the given requesting account is the owner of it. +func (p *Processor) GetFilterKeyword( + ctx context.Context, + requester *gtsmodel.Account, + id string, +) ( + *gtsmodel.FilterKeyword, + *gtsmodel.Filter, + gtserror.WithCode, +) { + + // Get the filter keyword from the database with given ID. + keyword, err := p.state.DB.GetFilterKeywordByID(ctx, id) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + err := gtserror.Newf("error getting filter keyword: %w", err) + return nil, nil, gtserror.NewErrorInternalError(err) + } + + // Check it exists. + if keyword == nil { + const text = "filter keyword not found" + return nil, nil, gtserror.NewWithCode(http.StatusNotFound, text) + } + + // Get the filter this filter keyword is + // associated with, without sub-models. + // (this also checks filter ownership). + filter, errWithCode := p.GetFilter( + gtscontext.SetBarebones(ctx), + requester, + keyword.FilterID, + ) + if errWithCode != nil { + return nil, nil, errWithCode + } + + return keyword, filter, nil +} + +// FromAPIContexts converts a slice of frontend API model FilterContext types to our internal FilterContexts bit field. +func FromAPIContexts(apiContexts []apimodel.FilterContext) (gtsmodel.FilterContexts, gtserror.WithCode) { + var contexts gtsmodel.FilterContexts + for _, context := range apiContexts { + switch context { + case apimodel.FilterContextHome: + contexts.SetHome() + case apimodel.FilterContextNotifications: + contexts.SetNotifications() + case apimodel.FilterContextPublic: + contexts.SetPublic() + case apimodel.FilterContextThread: + contexts.SetThread() + case apimodel.FilterContextAccount: + contexts.SetAccount() + default: + text := fmt.Sprintf("unsupported filter context: %s", context) + return 0, gtserror.NewWithCode(http.StatusBadRequest, text) + } + } + return contexts, nil +} diff --git a/internal/processing/filters/v1/convert.go b/internal/processing/filters/v1/convert.go deleted file mode 100644 index 417cf7b7d..000000000 --- a/internal/processing/filters/v1/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 v1 - -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 v1 filter version of the given -// filter keyword, or return an appropriate error if conversion fails. -func (p *Processor) apiFilter(ctx context.Context, filterKeyword *gtsmodel.FilterKeyword) (*apimodel.FilterV1, gtserror.WithCode) { - apiFilter, err := p.converter.FilterKeywordToAPIFilterV1(ctx, filterKeyword) - if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting filter keyword to API v1 filter: %w", err)) - } - - return apiFilter, nil -} diff --git a/internal/processing/filters/v1/create.go b/internal/processing/filters/v1/create.go index 24517dd7b..b2ec69442 100644 --- a/internal/processing/filters/v1/create.go +++ b/internal/processing/filters/v1/create.go @@ -20,7 +20,7 @@ package v1 import ( "context" "errors" - "fmt" + "net/http" "time" apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model" @@ -28,68 +28,72 @@ 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 and 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) Create(ctx context.Context, account *gtsmodel.Account, form *apimodel.FilterCreateUpdateRequestV1) (*apimodel.FilterV1, gtserror.WithCode) { +func (p *Processor) Create(ctx context.Context, requester *gtsmodel.Account, form *apimodel.FilterCreateUpdateRequestV1) (*apimodel.FilterV1, gtserror.WithCode) { + var errWithCode gtserror.WithCode + + // Create new wrapping filter. filter := >smodel.Filter{ ID: id.NewULID(), - AccountID: account.ID, + AccountID: requester.ID, Title: form.Phrase, - Action: gtsmodel.FilterActionWarn, } + if *form.Irreversible { + // Irreversible = action hide. filter.Action = gtsmodel.FilterActionHide + } else { + // Default action = action warn. + filter.Action = gtsmodel.FilterActionWarn } - if form.ExpiresIn != nil && *form.ExpiresIn != 0 { - filter.ExpiresAt = time.Now().Add(time.Second * time.Duration(*form.ExpiresIn)) + + // 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 _, 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 } + // Create new keyword attached to filter. filterKeyword := >smodel.FilterKeyword{ ID: id.NewULID(), - AccountID: account.ID, FilterID: filter.ID, - Filter: filter, Keyword: form.Phrase, WholeWord: util.Ptr(util.PtrOrValue(form.WholeWord, false)), } - filter.Keywords = []*gtsmodel.FilterKeyword{filterKeyword} - if err := p.state.DB.PutFilter(ctx, filter); 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) - } + // Attach the new keyword to filter before insert. + filter.Keywords = append(filter.Keywords, filterKeyword) + filter.KeywordIDs = append(filter.KeywordIDs, filterKeyword.ID) - apiFilter, errWithCode := p.apiFilter(ctx, filterKeyword) - if errWithCode != nil { - return nil, errWithCode + // Insert newly created filter into the database. + switch err := p.state.DB.PutFilter(ctx, filter); { + case err == nil: + // no issue + + case errors.Is(err, db.ErrAlreadyExists): + const text = "duplicate title" + 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) + // Stream a filters changed event to WS. + p.stream.FiltersChanged(ctx, requester) - return apiFilter, nil + // Return as converted frontend filter keyword model. + return typeutils.FilterKeywordToAPIFilterV1(filter, filterKeyword), nil } diff --git a/internal/processing/filters/v1/delete.go b/internal/processing/filters/v1/delete.go index 6a081ff04..cab8b185d 100644 --- a/internal/processing/filters/v1/delete.go +++ b/internal/processing/filters/v1/delete.go @@ -19,52 +19,52 @@ package v1 import ( "context" - "errors" + "slices" - "code.superseriousbusiness.org/gotosocial/internal/db" - "code.superseriousbusiness.org/gotosocial/internal/gtscontext" "code.superseriousbusiness.org/gotosocial/internal/gtserror" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" ) -// Delete an existing filter keyword and (if empty afterwards) filter for the given account. +// Delete an existing filter keyword and (if empty +// afterwards) filter for the given account. func (p *Processor) Delete( ctx context.Context, - account *gtsmodel.Account, + requester *gtsmodel.Account, filterKeywordID string, ) gtserror.WithCode { - // Get enough of the filter keyword that we can look up its filter ID. - filterKeyword, err := p.state.DB.GetFilterKeywordByID(gtscontext.SetBarebones(ctx), filterKeywordID) - if err != nil { - if errors.Is(err, db.ErrNoEntries) { - return gtserror.NewErrorNotFound(err) - } - return gtserror.NewErrorInternalError(err) - } - if filterKeyword.AccountID != account.ID { - return gtserror.NewErrorNotFound(nil) - } - - // Get the filter for this keyword. - filter, err := p.state.DB.GetFilterByID(ctx, filterKeyword.FilterID) - if err != nil { - return gtserror.NewErrorNotFound(err) + // Get the filter keyword with given ID, and associated filter, also checking ownership. + filterKeyword, filter, errWithCode := p.c.GetFilterKeyword(ctx, requester, filterKeywordID) + if errWithCode != nil { + return errWithCode } if len(filter.Keywords) > 1 || len(filter.Statuses) > 0 { - // The filter has other keywords or statuses. Delete only the requested filter keyword. - if err := p.state.DB.DeleteFilterKeywordByID(ctx, filterKeyword.ID); err != nil { + // The filter has other keywords or statuses, just delete the one filter keyword. + if err := p.state.DB.DeleteFilterKeywordsByIDs(ctx, filterKeyword.ID); err != nil { + err := gtserror.Newf("error deleting filter keyword: %w", err) + return gtserror.NewErrorInternalError(err) + } + + // Delete this filter keyword from the slice of IDs attached to filter. + filter.KeywordIDs = slices.DeleteFunc(filter.KeywordIDs, func(id string) bool { + return filterKeyword.ID == 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) } } else { - // Delete the entire filter. - if err := p.state.DB.DeleteFilterByID(ctx, filter.ID); err != nil { + // Delete the filter and this keyword that is attached to it. + 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/v1/filters.go b/internal/processing/filters/v1/filters.go index 89b509912..bcbbd70c0 100644 --- a/internal/processing/filters/v1/filters.go +++ b/internal/processing/filters/v1/filters.go @@ -18,19 +18,25 @@ package v1 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/v1/get.go b/internal/processing/filters/v1/get.go index ad35e6272..bdde123e9 100644 --- a/internal/processing/filters/v1/get.go +++ b/internal/processing/filters/v1/get.go @@ -25,47 +25,58 @@ import ( 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" ) // Get looks up a filter keyword by ID and returns it as a v1 filter. -func (p *Processor) Get(ctx context.Context, account *gtsmodel.Account, filterKeywordID string) (*apimodel.FilterV1, 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(nil) +func (p *Processor) Get(ctx context.Context, requester *gtsmodel.Account, filterKeywordID string) (*apimodel.FilterV1, gtserror.WithCode) { + filterKeyword, filter, errWithCode := p.c.GetFilterKeyword(ctx, requester, filterKeywordID) + if errWithCode != nil { + return nil, errWithCode } - - return p.apiFilter(ctx, filterKeyword) + return typeutils.FilterKeywordToAPIFilterV1(filter, filterKeyword), nil } // GetAll looks up all filter keywords for the current account and returns them as v1 filters. -func (p *Processor) GetAll(ctx context.Context, account *gtsmodel.Account) ([]*apimodel.FilterV1, gtserror.WithCode) { - filters, err := p.state.DB.GetFilterKeywordsForAccountID( - ctx, - account.ID, +func (p *Processor) GetAll(ctx context.Context, requester *gtsmodel.Account) ([]*apimodel.FilterV1, gtserror.WithCode) { + var totalKeywords int + + // Get a list of all filters owned by this account, + // (without any sub-models attached, done later). + filters, err := p.state.DB.GetFiltersByAccountID( + gtscontext.SetBarebones(ctx), + requester.ID, ) - if err != nil { - if errors.Is(err, db.ErrNoEntries) { - return nil, nil - } + if err != nil && !errors.Is(err, db.ErrNoEntries) { + err := gtserror.Newf("error getting filters: %w", err) return nil, gtserror.NewErrorInternalError(err) } - apiFilters := make([]*apimodel.FilterV1, 0, len(filters)) + // Get a total count of all expected + // keywords for slice preallocation. for _, filter := range filters { - apiFilter, errWithCode := p.apiFilter(ctx, filter) - if errWithCode != nil { - return nil, errWithCode + totalKeywords += len(filter.KeywordIDs) + } + + // Create a slice to store converted V1 frontend models. + apiFilters := make([]*apimodel.FilterV1, 0, totalKeywords) + + for _, filter := range filters { + // For each of the fetched filters, fetch all of their associated keywords. + keywords, err := p.state.DB.GetFilterKeywordsByIDs(ctx, filter.KeywordIDs) + if err != nil { + err := gtserror.Newf("error getting filter keywords: %w", err) + return nil, gtserror.NewErrorInternalError(err) } - apiFilters = append(apiFilters, apiFilter) + // Convert each keyword to frontend. + for _, keyword := range keywords { + apiFilter := typeutils.FilterKeywordToAPIFilterV1(filter, keyword) + apiFilters = append(apiFilters, apiFilter) + } } // Sort them by ID so that they're in a stable order. diff --git a/internal/processing/filters/v1/update.go b/internal/processing/filters/v1/update.go index 8b50c3fcf..7e25e6fde 100644 --- a/internal/processing/filters/v1/update.go +++ b/internal/processing/filters/v1/update.go @@ -21,77 +21,59 @@ import ( "context" "errors" "fmt" + "net/http" "strings" "time" 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/util" + "code.superseriousbusiness.org/gotosocial/internal/processing/filters/common" + "code.superseriousbusiness.org/gotosocial/internal/typeutils" ) // Update an existing filter and 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) Update( ctx context.Context, - account *gtsmodel.Account, + requester *gtsmodel.Account, filterKeywordID string, form *apimodel.FilterCreateUpdateRequestV1, ) (*apimodel.FilterV1, gtserror.WithCode) { - // Get enough of the filter keyword that we can look up its filter 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(nil) + // Get the filter keyword with given ID, and associated filter, also checking ownership. + filterKeyword, filter, errWithCode := p.c.GetFilterKeyword(ctx, requester, filterKeywordID) + if errWithCode != nil { + return nil, errWithCode } - // Get the filter for this keyword. - filter, err := p.state.DB.GetFilterByID(ctx, filterKeyword.FilterID) - if err != nil { - if errors.Is(err, db.ErrNoEntries) { - return nil, gtserror.NewErrorNotFound(err) - } - return nil, gtserror.NewErrorInternalError(err) - } + var title string + var action gtsmodel.FilterAction + var contexts gtsmodel.FilterContexts + var expiresAt time.Time + var wholeword bool + + // Get filter title. + title = form.Phrase - title := form.Phrase - action := gtsmodel.FilterActionWarn if *form.Irreversible { + // Irreversible = action hide. action = gtsmodel.FilterActionHide + } else { + // Default action = action warn. + action = gtsmodel.FilterActionWarn } - expiresAt := time.Time{} - if form.ExpiresIn != nil && *form.ExpiresIn != 0 { - expiresAt = time.Now().Add(time.Second * time.Duration(*form.ExpiresIn)) + + // Check form for valid expiry and set on filter. + if form.ExpiresIn != nil && *form.ExpiresIn > 0 { + expiresIn := time.Duration(*form.ExpiresIn) * time.Second + expiresAt = time.Now().Add(expiresIn) } - contextHome := false - contextNotifications := false - contextPublic := false - contextThread := false - contextAccount := false - for _, context := range form.Context { - switch context { - case apimodel.FilterContextHome: - contextHome = true - case apimodel.FilterContextNotifications: - contextNotifications = true - case apimodel.FilterContextPublic: - contextPublic = true - case apimodel.FilterContextThread: - contextThread = true - case apimodel.FilterContextAccount: - contextAccount = true - default: - return nil, gtserror.NewErrorUnprocessableEntity( - fmt.Errorf("unsupported filter context '%s'", context), - ) - } + + // Parse contexts filter applies in from incoming form data. + contexts, errWithCode = common.FromAPIContexts(form.Context) + if errWithCode != nil { + return nil, errWithCode } // v1 filter APIs can't change certain fields for a filter with multiple keywords or any statuses, @@ -108,11 +90,7 @@ func (p *Processor) Update( if expiresAt != filter.ExpiresAt { forbiddenFields = append(forbiddenFields, "expires_in") } - if contextHome != util.PtrOrValue(filter.ContextHome, false) || - contextNotifications != util.PtrOrValue(filter.ContextNotifications, false) || - contextPublic != util.PtrOrValue(filter.ContextPublic, false) || - contextThread != util.PtrOrValue(filter.ContextThread, false) || - contextAccount != util.PtrOrValue(filter.ContextAccount, false) { + if contexts != filter.Contexts { forbiddenFields = append(forbiddenFields, "context") } if len(forbiddenFields) > 0 { @@ -122,54 +100,75 @@ func (p *Processor) Update( } } - // Now that we've checked that the changes are legal, apply them to the filter and keyword. - filter.Title = title - filter.Action = action - filter.ExpiresAt = expiresAt - filter.ContextHome = &contextHome - filter.ContextNotifications = &contextNotifications - filter.ContextPublic = &contextPublic - filter.ContextThread = &contextThread - filter.ContextAccount = &contextAccount - filterKeyword.Keyword = form.Phrase - filterKeyword.WholeWord = util.Ptr(util.PtrOrValue(form.WholeWord, false)) - - // We only want to update the relevant filter keyword. - filter.Keywords = []*gtsmodel.FilterKeyword{filterKeyword} - filter.Statuses = nil - filterKeyword.Filter = filter - - filterColumns := []string{ - "title", - "action", - "expires_at", - "context_home", - "context_notifications", - "context_public", - "context_thread", - "context_account", + // Filter columns that + // we're going to update. + var filterCols []string + var keywordCols []string + + // Check for changed filter title / filter keyword phrase. + if title != filter.Title || title != filterKeyword.Keyword { + keywordCols = append(keywordCols, "keyword") + filterCols = append(filterCols, "title") + filterKeyword.Keyword = title + filter.Title = title } - filterKeywordColumns := [][]string{ - { - "keyword", - "whole_word", - }, + + // Check for changed action. + if action != filter.Action { + filterCols = append(filterCols, "action") + filter.Action = action } - if err := p.state.DB.UpdateFilter(ctx, filter, filterColumns, filterKeywordColumns, nil, nil); 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()) - } + + // Check for changed filter expiry time. + if !expiresAt.Equal(filter.ExpiresAt) { + filterCols = append(filterCols, "expires_at") + filter.ExpiresAt = expiresAt + } + + // Check for changed filter context. + if contexts != filter.Contexts { + filterCols = append(filterCols, "contexts") + filter.Contexts = contexts + } + + // Check for changed wholeword flag. + if form.WholeWord != nil && + *form.WholeWord != *filterKeyword.WholeWord { + keywordCols = append(keywordCols, "whole_word") + filterKeyword.WholeWord = &wholeword + } + + // Update filter keyword model in the database with determined changed cols. + switch err := p.state.DB.UpdateFilterKeyword(ctx, filterKeyword, keywordCols...); { + 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 updating filter: %w", err) return nil, gtserror.NewErrorInternalError(err) } - apiFilter, errWithCode := p.apiFilter(ctx, filterKeyword) - if errWithCode != nil { - return nil, errWithCode + // Update filter model in the database with determined changed cols. + switch err := p.state.DB.UpdateFilter(ctx, filter, filterCols...); { + case err == nil: + // no issue + + case errors.Is(err, db.ErrAlreadyExists): + const text = "duplicate title" + return nil, gtserror.NewWithCode(http.StatusConflict, text) + + default: + 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 apiFilter, nil + // Return as converted frontend filter keyword model. + return typeutils.FilterKeywordToAPIFilterV1(filter, filterKeyword), nil } 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 } diff --git a/internal/processing/processor.go b/internal/processing/processor.go index b8adb9bb8..22574f1d7 100644 --- a/internal/processing/processor.go +++ b/internal/processing/processor.go @@ -34,6 +34,7 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/processing/common" "code.superseriousbusiness.org/gotosocial/internal/processing/conversations" "code.superseriousbusiness.org/gotosocial/internal/processing/fedi" + filterCommon "code.superseriousbusiness.org/gotosocial/internal/processing/filters/common" filtersv1 "code.superseriousbusiness.org/gotosocial/internal/processing/filters/v1" filtersv2 "code.superseriousbusiness.org/gotosocial/internal/processing/filters/v2" "code.superseriousbusiness.org/gotosocial/internal/processing/interactionrequests" @@ -224,6 +225,7 @@ func NewProcessor( processor.account = account.New(&common, state, converter, mediaManager, federator, visFilter, parseMentionFunc) processor.media = media.New(&common, state, converter, federator, mediaManager, federator.TransportController()) processor.stream = stream.New(state, oauthServer) + filterCommon := filterCommon.New(state) // Instantiate the rest of the sub // processors + pin them to this struct. @@ -232,8 +234,8 @@ func NewProcessor( processor.application = application.New(state, converter) processor.conversations = conversations.New(state, converter, visFilter, muteFilter) processor.fedi = fedi.New(state, &common, converter, federator, visFilter) - processor.filtersv1 = filtersv1.New(state, converter, &processor.stream) - processor.filtersv2 = filtersv2.New(state, converter, &processor.stream) + processor.filtersv1 = filtersv1.New(state, converter, filterCommon, &processor.stream) + processor.filtersv2 = filtersv2.New(state, converter, filterCommon, &processor.stream) processor.interactionRequests = interactionrequests.New(&common, state, converter) processor.list = list.New(state, converter) processor.markers = markers.New(state, converter) diff --git a/internal/processing/search/util.go b/internal/processing/search/util.go index 97eb813db..b4568722d 100644 --- a/internal/processing/search/util.go +++ b/internal/processing/search/util.go @@ -21,7 +21,6 @@ import ( "context" apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model" - statusfilter "code.superseriousbusiness.org/gotosocial/internal/filter/status" "code.superseriousbusiness.org/gotosocial/internal/gtserror" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/log" @@ -114,7 +113,7 @@ func (p *Processor) packageStatuses( continue } - apiStatus, err := p.converter.StatusToAPIStatus(ctx, status, requestingAccount, statusfilter.FilterContextNone, nil) + apiStatus, err := p.converter.StatusToAPIStatus(ctx, status, requestingAccount, gtsmodel.FilterContextNone, nil) if err != nil { log.Debugf(ctx, "skipping status %s because it couldn't be converted to its api representation: %s", status.ID, err) continue diff --git a/internal/processing/status/context.go b/internal/processing/status/context.go index 6f3e7a4fd..531dff1d6 100644 --- a/internal/processing/status/context.go +++ b/internal/processing/status/context.go @@ -24,7 +24,6 @@ import ( "strings" apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model" - statusfilter "code.superseriousbusiness.org/gotosocial/internal/filter/status" "code.superseriousbusiness.org/gotosocial/internal/gtserror" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" ) @@ -278,7 +277,7 @@ func (p *Processor) ContextGet( ) (*apimodel.ThreadContext, gtserror.WithCode) { // Retrieve filters as they affect // what should be shown to requester. - filters, err := p.state.DB.GetFiltersForAccountID( + filters, err := p.state.DB.GetFiltersByAccountID( ctx, // Populate filters. requester.ID, ) @@ -305,7 +304,7 @@ func (p *Processor) ContextGet( apiContext.Ancestors = p.c.GetVisibleAPIStatuses(ctx, requester, threadContext.ancestors, - statusfilter.FilterContextThread, + gtsmodel.FilterContextThread, filters, ) @@ -313,7 +312,7 @@ func (p *Processor) ContextGet( apiContext.Descendants = p.c.GetVisibleAPIStatuses(ctx, requester, threadContext.descendants, - statusfilter.FilterContextThread, + gtsmodel.FilterContextThread, filters, ) diff --git a/internal/processing/stream/statusupdate_test.go b/internal/processing/stream/statusupdate_test.go index 8fc4bcfe8..74e7a4933 100644 --- a/internal/processing/stream/statusupdate_test.go +++ b/internal/processing/stream/statusupdate_test.go @@ -22,7 +22,7 @@ import ( "encoding/json" "testing" - statusfilter "code.superseriousbusiness.org/gotosocial/internal/filter/status" + "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/stream" "code.superseriousbusiness.org/gotosocial/internal/typeutils" "github.com/stretchr/testify/suite" @@ -39,7 +39,7 @@ func (suite *StatusUpdateTestSuite) TestStreamNotification() { suite.NoError(errWithCode) editedStatus := suite.testStatuses["remote_account_1_status_1"] - apiStatus, err := typeutils.NewConverter(&suite.state).StatusToAPIStatus(suite.T().Context(), editedStatus, account, statusfilter.FilterContextNotifications, nil) + apiStatus, err := typeutils.NewConverter(&suite.state).StatusToAPIStatus(suite.T().Context(), editedStatus, account, gtsmodel.FilterContextNotifications, nil) suite.NoError(err) suite.streamProcessor.StatusUpdate(suite.T().Context(), account, apiStatus, stream.TimelineHome) diff --git a/internal/processing/timeline/faved.go b/internal/processing/timeline/faved.go index 84788a8fa..c1b44fa92 100644 --- a/internal/processing/timeline/faved.go +++ b/internal/processing/timeline/faved.go @@ -25,8 +25,8 @@ import ( apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model" apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util" "code.superseriousbusiness.org/gotosocial/internal/db" - statusfilter "code.superseriousbusiness.org/gotosocial/internal/filter/status" "code.superseriousbusiness.org/gotosocial/internal/gtserror" + "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/log" "code.superseriousbusiness.org/gotosocial/internal/util" ) @@ -56,7 +56,7 @@ func (p *Processor) FavedTimelineGet(ctx context.Context, authed *apiutil.Auth, continue } - apiStatus, err := p.converter.StatusToAPIStatus(ctx, s, authed.Account, statusfilter.FilterContextNone, nil) + apiStatus, err := p.converter.StatusToAPIStatus(ctx, s, authed.Account, gtsmodel.FilterContextNone, nil) if err != nil { log.Errorf(ctx, "error convering to api status: %v", err) continue diff --git a/internal/processing/timeline/home.go b/internal/processing/timeline/home.go index ba74b770c..3089f52fc 100644 --- a/internal/processing/timeline/home.go +++ b/internal/processing/timeline/home.go @@ -22,7 +22,6 @@ import ( "net/url" apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model" - statusfilter "code.superseriousbusiness.org/gotosocial/internal/filter/status" "code.superseriousbusiness.org/gotosocial/internal/gtserror" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/log" @@ -77,7 +76,7 @@ func (p *Processor) HomeTimelineGet( pageQuery, // Status filter context. - statusfilter.FilterContextHome, + gtsmodel.FilterContextHome, // Database load function. func(pg *paging.Page) (statuses []*gtsmodel.Status, err error) { diff --git a/internal/processing/timeline/home_test.go b/internal/processing/timeline/home_test.go index 184d361f0..2d0c912f8 100644 --- a/internal/processing/timeline/home_test.go +++ b/internal/processing/timeline/home_test.go @@ -24,7 +24,6 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/id" "code.superseriousbusiness.org/gotosocial/internal/paging" - "code.superseriousbusiness.org/gotosocial/internal/util" "github.com/stretchr/testify/suite" ) @@ -49,24 +48,20 @@ func (suite *HomeTestSuite) TestHomeTimelineGetHideFiltered() { filteredStatus = suite.testStatuses["admin_account_status_2"] filteredStatusFound = false filterID = id.NewULID() - filter = >smodel.Filter{ + filterStatusID = id.NewULID() + filterStatus = >smodel.FilterStatus{ + ID: filterStatusID, + FilterID: filterID, + StatusID: filteredStatus.ID, + } + filter = >smodel.Filter{ ID: filterID, AccountID: requester.ID, Title: "timeline filtering test", Action: gtsmodel.FilterActionHide, - Statuses: []*gtsmodel.FilterStatus{ - { - ID: id.NewULID(), - AccountID: requester.ID, - FilterID: filterID, - StatusID: filteredStatus.ID, - }, - }, - ContextHome: util.Ptr(true), - ContextNotifications: util.Ptr(false), - ContextPublic: util.Ptr(false), - ContextThread: util.Ptr(false), - ContextAccount: util.Ptr(false), + Statuses: []*gtsmodel.FilterStatus{filterStatus}, + StatusIDs: []string{filterStatusID}, + Contexts: gtsmodel.FilterContexts(gtsmodel.FilterContextHome), } ) @@ -95,6 +90,11 @@ func (suite *HomeTestSuite) TestHomeTimelineGetHideFiltered() { // Clear the timeline to drop all cached statuses. suite.state.Caches.Timelines.Home.Clear(requester.ID) + // Create the filter status associated with the main filter. + if err := suite.db.PutFilterStatus(ctx, filterStatus); err != nil { + suite.FailNow(err.Error()) + } + // Create a filter to hide one status on the timeline. if err := suite.db.PutFilter(ctx, filter); err != nil { suite.FailNow(err.Error()) diff --git a/internal/processing/timeline/list.go b/internal/processing/timeline/list.go index c8e6bc5f1..265cd5ca2 100644 --- a/internal/processing/timeline/list.go +++ b/internal/processing/timeline/list.go @@ -23,7 +23,6 @@ import ( apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model" "code.superseriousbusiness.org/gotosocial/internal/db" - statusfilter "code.superseriousbusiness.org/gotosocial/internal/filter/status" "code.superseriousbusiness.org/gotosocial/internal/gtscontext" "code.superseriousbusiness.org/gotosocial/internal/gtserror" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" @@ -88,7 +87,7 @@ func (p *Processor) ListTimelineGet( nil, // Status filter context. - statusfilter.FilterContextHome, + gtsmodel.FilterContextHome, // Database load function. func(pg *paging.Page) (statuses []*gtsmodel.Status, err error) { diff --git a/internal/processing/timeline/notification.go b/internal/processing/timeline/notification.go index ad60fd90c..143145bb9 100644 --- a/internal/processing/timeline/notification.go +++ b/internal/processing/timeline/notification.go @@ -59,7 +59,7 @@ func (p *Processor) NotificationsGet( return util.EmptyPageableResponse(), nil } - filters, err := p.state.DB.GetFiltersForAccountID(ctx, requester.ID) + filters, err := p.state.DB.GetFiltersByAccountID(ctx, requester.ID) if err != nil { err = gtserror.Newf("error getting account %s filters: %w", requester.ID, err) return nil, gtserror.NewErrorInternalError(err) diff --git a/internal/processing/timeline/public.go b/internal/processing/timeline/public.go index cfb58201d..d724bfaa1 100644 --- a/internal/processing/timeline/public.go +++ b/internal/processing/timeline/public.go @@ -21,7 +21,6 @@ import ( "context" apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model" - statusfilter "code.superseriousbusiness.org/gotosocial/internal/filter/status" "code.superseriousbusiness.org/gotosocial/internal/gtserror" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/log" @@ -79,7 +78,7 @@ func (p *Processor) publicTimelineGet( localOnlyFalse, // Status filter context. - statusfilter.FilterContextPublic, + gtsmodel.FilterContextPublic, // Database load function. func(pg *paging.Page) (statuses []*gtsmodel.Status, err error) { @@ -148,7 +147,7 @@ func (p *Processor) localTimelineGet( localOnlyTrue, // Status filter context. - statusfilter.FilterContextPublic, + gtsmodel.FilterContextPublic, // Database load function. func(pg *paging.Page) (statuses []*gtsmodel.Status, err error) { diff --git a/internal/processing/timeline/public_test.go b/internal/processing/timeline/public_test.go index 5aa09f138..3320a45da 100644 --- a/internal/processing/timeline/public_test.go +++ b/internal/processing/timeline/public_test.go @@ -24,7 +24,6 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/id" "code.superseriousbusiness.org/gotosocial/internal/paging" - "code.superseriousbusiness.org/gotosocial/internal/util" "github.com/stretchr/testify/suite" ) @@ -110,24 +109,20 @@ func (suite *PublicTestSuite) TestPublicTimelineGetHideFiltered() { filteredStatus = suite.testStatuses["admin_account_status_2"] filteredStatusFound = false filterID = id.NewULID() - filter = >smodel.Filter{ + filterStatusID = id.NewULID() + filterStatus = >smodel.FilterStatus{ + ID: filterStatusID, + FilterID: filterID, + StatusID: filteredStatus.ID, + } + filter = >smodel.Filter{ ID: filterID, AccountID: requester.ID, Title: "timeline filtering test", Action: gtsmodel.FilterActionHide, - Statuses: []*gtsmodel.FilterStatus{ - { - ID: id.NewULID(), - AccountID: requester.ID, - FilterID: filterID, - StatusID: filteredStatus.ID, - }, - }, - ContextHome: util.Ptr(false), - ContextNotifications: util.Ptr(false), - ContextPublic: util.Ptr(true), - ContextThread: util.Ptr(false), - ContextAccount: util.Ptr(false), + Statuses: []*gtsmodel.FilterStatus{filterStatus}, + StatusIDs: []string{filterStatusID}, + Contexts: gtsmodel.FilterContexts(gtsmodel.FilterContextPublic), } ) @@ -153,6 +148,11 @@ func (suite *PublicTestSuite) TestPublicTimelineGetHideFiltered() { suite.FailNow("precondition failed: status we would filter isn't present in unfiltered timeline") } + // Create the filter status associated with the main filter. + if err := suite.db.PutFilterStatus(ctx, filterStatus); err != nil { + suite.FailNow(err.Error()) + } + // Create a filter to hide one status on the timeline. if err := suite.db.PutFilter(ctx, filter); err != nil { suite.FailNow(err.Error()) diff --git a/internal/processing/timeline/tag.go b/internal/processing/timeline/tag.go index 88333d343..995f9f8cc 100644 --- a/internal/processing/timeline/tag.go +++ b/internal/processing/timeline/tag.go @@ -24,7 +24,6 @@ import ( apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model" "code.superseriousbusiness.org/gotosocial/internal/db" - statusfilter "code.superseriousbusiness.org/gotosocial/internal/filter/status" "code.superseriousbusiness.org/gotosocial/internal/gtserror" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/log" @@ -87,7 +86,7 @@ func (p *Processor) TagTimelineGet( nil, // Status filter context. - statusfilter.FilterContextPublic, + gtsmodel.FilterContextPublic, // Database load function. func(pg *paging.Page) (statuses []*gtsmodel.Status, err error) { diff --git a/internal/processing/timeline/timeline.go b/internal/processing/timeline/timeline.go index a86702d42..a37785879 100644 --- a/internal/processing/timeline/timeline.go +++ b/internal/processing/timeline/timeline.go @@ -70,7 +70,7 @@ func (p *Processor) getStatusTimeline( page *paging.Page, pagePath string, pageQuery url.Values, - filterCtx statusfilter.FilterContext, + filterCtx gtsmodel.FilterContext, loadPage func(*paging.Page) (statuses []*gtsmodel.Status, err error), filter func(*gtsmodel.Status) (delete bool), postFilter func(*gtsmodel.Status) (remove bool), @@ -83,7 +83,7 @@ func (p *Processor) getStatusTimeline( if requester != nil { // Fetch all filters relevant for requesting account. - filters, err = p.state.DB.GetFiltersForAccountID(ctx, + filters, err = p.state.DB.GetFiltersByAccountID(ctx, requester.ID, ) if err != nil && !errors.Is(err, db.ErrNoEntries) { diff --git a/internal/processing/workers/fromclientapi_test.go b/internal/processing/workers/fromclientapi_test.go index 3f6964259..1c30c11be 100644 --- a/internal/processing/workers/fromclientapi_test.go +++ b/internal/processing/workers/fromclientapi_test.go @@ -27,7 +27,6 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/ap" "code.superseriousbusiness.org/gotosocial/internal/config" "code.superseriousbusiness.org/gotosocial/internal/db" - statusfilter "code.superseriousbusiness.org/gotosocial/internal/filter/status" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/id" "code.superseriousbusiness.org/gotosocial/internal/messages" @@ -213,7 +212,7 @@ func (suite *FromClientAPITestSuite) statusJSON( ctx, status, requestingAccount, - statusfilter.FilterContextNone, + gtsmodel.FilterContextNone, nil, ) if err != nil { diff --git a/internal/processing/workers/surfacenotify.go b/internal/processing/workers/surfacenotify.go index 044315349..b11fb103e 100644 --- a/internal/processing/workers/surfacenotify.go +++ b/internal/processing/workers/surfacenotify.go @@ -743,7 +743,7 @@ func (s *Surface) Notify( } } - filters, err := s.State.DB.GetFiltersForAccountID(ctx, targetAccount.ID) + filters, err := s.State.DB.GetFiltersByAccountID(ctx, targetAccount.ID) if err != nil { return gtserror.Newf("couldn't retrieve filters for account %s: %w", targetAccount.ID, err) } diff --git a/internal/processing/workers/surfacetimeline.go b/internal/processing/workers/surfacetimeline.go index 7ef5fee87..7f9bcd596 100644 --- a/internal/processing/workers/surfacetimeline.go +++ b/internal/processing/workers/surfacetimeline.go @@ -180,7 +180,7 @@ func (s *Surface) timelineAndNotifyStatusForFollowers( follow.Account, status, stream.TimelineHome, - statusfilter.FilterContextHome, + gtsmodel.FilterContextHome, filters, ); homeTimelined { @@ -275,7 +275,7 @@ func (s *Surface) listTimelineStatusForFollow( follow.Account, status, stream.TimelineList+":"+list.ID, // key streamType to this specific list - statusfilter.FilterContextHome, + gtsmodel.FilterContextHome, filters, ) @@ -288,7 +288,7 @@ func (s *Surface) listTimelineStatusForFollow( // getFiltersAndMutes returns an account's filters and mutes. func (s *Surface) getFilters(ctx context.Context, accountID string) ([]*gtsmodel.Filter, error) { - filters, err := s.State.DB.GetFiltersForAccountID(ctx, accountID) + filters, err := s.State.DB.GetFiltersByAccountID(ctx, accountID) if err != nil { return nil, gtserror.Newf("couldn't retrieve filters for account %s: %w", accountID, err) } @@ -369,7 +369,7 @@ func (s *Surface) timelineStatus( account *gtsmodel.Account, status *gtsmodel.Status, streamType string, - filterCtx statusfilter.FilterContext, + filterCtx gtsmodel.FilterContext, filters []*gtsmodel.Filter, ) bool { @@ -436,7 +436,7 @@ func (s *Surface) timelineAndNotifyStatusForTagFollowers( tagFollowerAccount, status, stream.TimelineHome, - statusfilter.FilterContextHome, + gtsmodel.FilterContextHome, filters, ) } @@ -731,7 +731,7 @@ func (s *Surface) timelineStreamStatusUpdate( apiStatus, err := s.Converter.StatusToAPIStatus(ctx, status, account, - statusfilter.FilterContextHome, + gtsmodel.FilterContextHome, filters, ) |
