summaryrefslogtreecommitdiff
path: root/internal/processing/filters/v2
diff options
context:
space:
mode:
authorLibravatar kim <grufwub@gmail.com>2025-06-24 17:24:34 +0200
committerLibravatar tobi <kipvandenbos@noreply.codeberg.org>2025-06-24 17:24:34 +0200
commit996da6e0291b158093d917ca76933584f464d668 (patch)
tree38c12b20f76076f08ef5c8b8715ba3d8629fa0fb /internal/processing/filters/v2
parent[bugfix] update the default configuration to not set a db type or address, to... (diff)
downloadgotosocial-996da6e0291b158093d917ca76933584f464d668.tar.xz
[performance] filter model and database table improvements (#4277)
- removes unnecessary fields / columns (created_at, updated_at) - replaces filter.context_* columns with singular filter.contexts bit field which should save both struct memory and database space - replaces filter.action string with integer enum type which should save both struct memory and database space - adds links from filter to filter_* tables with Filter{}.KeywordIDs and Filter{}.StatusIDs fields (this also means we now have those ID slices cached, which reduces some lookups) - removes account_id fields from filter_* tables, since there's a more direct connection between filter and filter_* tables, and filter.account_id already exists - refactors a bunch of the filter processor logic to save on code repetition, factor in the above changes, fix a few bugs with missed error returns and bring it more in-line with some of our newer code Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4277 Co-authored-by: kim <grufwub@gmail.com> Co-committed-by: kim <grufwub@gmail.com>
Diffstat (limited to 'internal/processing/filters/v2')
-rw-r--r--internal/processing/filters/v2/convert.go38
-rw-r--r--internal/processing/filters/v2/create.go94
-rw-r--r--internal/processing/filters/v2/delete.go29
-rw-r--r--internal/processing/filters/v2/filters.go8
-rw-r--r--internal/processing/filters/v2/get.go53
-rw-r--r--internal/processing/filters/v2/keywordcreate.go57
-rw-r--r--internal/processing/filters/v2/keyworddelete.go37
-rw-r--r--internal/processing/filters/v2/keywordget.go66
-rw-r--r--internal/processing/filters/v2/keywordupdate.go47
-rw-r--r--internal/processing/filters/v2/statuscreate.go63
-rw-r--r--internal/processing/filters/v2/statusdelete.go37
-rw-r--r--internal/processing/filters/v2/statusget.go66
-rw-r--r--internal/processing/filters/v2/update.go440
13 files changed, 559 insertions, 476 deletions
diff --git a/internal/processing/filters/v2/convert.go b/internal/processing/filters/v2/convert.go
deleted file mode 100644
index 590edd04b..000000000
--- a/internal/processing/filters/v2/convert.go
+++ /dev/null
@@ -1,38 +0,0 @@
-// GoToSocial
-// Copyright (C) GoToSocial Authors admin@gotosocial.org
-// SPDX-License-Identifier: AGPL-3.0-or-later
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-package v2
-
-import (
- "context"
- "fmt"
-
- apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
- "code.superseriousbusiness.org/gotosocial/internal/gtserror"
- "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
-)
-
-// apiFilter is a shortcut to return the API v2 filter version of the given
-// filter, or return an appropriate error if conversion fails.
-func (p *Processor) apiFilter(ctx context.Context, filterKeyword *gtsmodel.Filter) (*apimodel.FilterV2, gtserror.WithCode) {
- apiFilter, err := p.converter.FilterToAPIFilterV2(ctx, filterKeyword)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting filter to API v2 filter: %w", err))
- }
-
- return apiFilter, nil
-}
diff --git a/internal/processing/filters/v2/create.go b/internal/processing/filters/v2/create.go
index c221e1539..d77c23424 100644
--- a/internal/processing/filters/v2/create.go
+++ b/internal/processing/filters/v2/create.go
@@ -20,7 +20,7 @@ package v2
import (
"context"
"errors"
- "fmt"
+ "net/http"
"time"
apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
@@ -28,79 +28,85 @@ import (
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"code.superseriousbusiness.org/gotosocial/internal/id"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/filters/common"
"code.superseriousbusiness.org/gotosocial/internal/typeutils"
- "code.superseriousbusiness.org/gotosocial/internal/util"
)
// Create a new filter for the given account, using the provided parameters.
// These params should have already been validated by the time they reach this function.
func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, form *apimodel.FilterCreateRequestV2) (*apimodel.FilterV2, gtserror.WithCode) {
+ var errWithCode gtserror.WithCode
+
+ // Create new filter model.
filter := &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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
}