diff options
Diffstat (limited to 'internal/processing/filters/v1')
| -rw-r--r-- | internal/processing/filters/v1/convert.go | 38 | ||||
| -rw-r--r-- | internal/processing/filters/v1/create.go | 82 | ||||
| -rw-r--r-- | internal/processing/filters/v1/delete.go | 54 | ||||
| -rw-r--r-- | internal/processing/filters/v1/filters.go | 8 | ||||
| -rw-r--r-- | internal/processing/filters/v1/get.go | 61 | ||||
| -rw-r--r-- | internal/processing/filters/v1/update.go | 189 |
6 files changed, 207 insertions, 225 deletions
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 } |
