summaryrefslogtreecommitdiff
path: root/internal/processing/filters
diff options
context:
space:
mode:
Diffstat (limited to 'internal/processing/filters')
-rw-r--r--internal/processing/filters/common/common.go184
-rw-r--r--internal/processing/filters/v1/convert.go38
-rw-r--r--internal/processing/filters/v1/create.go82
-rw-r--r--internal/processing/filters/v1/delete.go54
-rw-r--r--internal/processing/filters/v1/filters.go8
-rw-r--r--internal/processing/filters/v1/get.go61
-rw-r--r--internal/processing/filters/v1/update.go189
-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
20 files changed, 950 insertions, 701 deletions
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 := &gtsmodel.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 := &gtsmodel.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 := &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
}