summaryrefslogtreecommitdiff
path: root/internal/processing
diff options
context:
space:
mode:
Diffstat (limited to 'internal/processing')
-rw-r--r--internal/processing/account/bookmarks.go3
-rw-r--r--internal/processing/account/statuses.go5
-rw-r--r--internal/processing/common/status.go6
-rw-r--r--internal/processing/conversations/conversations.go2
-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
-rw-r--r--internal/processing/processor.go6
-rw-r--r--internal/processing/search/util.go3
-rw-r--r--internal/processing/status/context.go7
-rw-r--r--internal/processing/stream/statusupdate_test.go4
-rw-r--r--internal/processing/timeline/faved.go4
-rw-r--r--internal/processing/timeline/home.go3
-rw-r--r--internal/processing/timeline/home_test.go30
-rw-r--r--internal/processing/timeline/list.go3
-rw-r--r--internal/processing/timeline/notification.go2
-rw-r--r--internal/processing/timeline/public.go5
-rw-r--r--internal/processing/timeline/public_test.go30
-rw-r--r--internal/processing/timeline/tag.go3
-rw-r--r--internal/processing/timeline/timeline.go4
-rw-r--r--internal/processing/workers/fromclientapi_test.go3
-rw-r--r--internal/processing/workers/surfacenotify.go2
-rw-r--r--internal/processing/workers/surfacetimeline.go12
40 files changed, 1015 insertions, 773 deletions
diff --git a/internal/processing/account/bookmarks.go b/internal/processing/account/bookmarks.go
index 329bcf30c..e6f0886f9 100644
--- a/internal/processing/account/bookmarks.go
+++ b/internal/processing/account/bookmarks.go
@@ -23,7 +23,6 @@ import (
apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
"code.superseriousbusiness.org/gotosocial/internal/db"
- statusfilter "code.superseriousbusiness.org/gotosocial/internal/filter/status"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"code.superseriousbusiness.org/gotosocial/internal/log"
@@ -75,7 +74,7 @@ func (p *Processor) BookmarksGet(ctx context.Context, requestingAccount *gtsmode
}
// Convert the status.
- item, err := p.converter.StatusToAPIStatus(ctx, status, requestingAccount, statusfilter.FilterContextNone, nil)
+ item, err := p.converter.StatusToAPIStatus(ctx, status, requestingAccount, gtsmodel.FilterContextNone, nil)
if err != nil {
log.Errorf(ctx, "error converting bookmarked status to api: %s", err)
continue
diff --git a/internal/processing/account/statuses.go b/internal/processing/account/statuses.go
index 0ff9ef7e1..3b56750c5 100644
--- a/internal/processing/account/statuses.go
+++ b/internal/processing/account/statuses.go
@@ -24,7 +24,6 @@ import (
apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
"code.superseriousbusiness.org/gotosocial/internal/db"
- statusfilter "code.superseriousbusiness.org/gotosocial/internal/filter/status"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"code.superseriousbusiness.org/gotosocial/internal/log"
@@ -97,7 +96,7 @@ func (p *Processor) StatusesGet(
return nil, gtserror.NewErrorInternalError(err)
}
- filters, err := p.state.DB.GetFiltersForAccountID(ctx, requestingAccount.ID)
+ filters, err := p.state.DB.GetFiltersByAccountID(ctx, requestingAccount.ID)
if err != nil {
err = gtserror.Newf("couldn't retrieve filters for account %s: %w", requestingAccount.ID, err)
return nil, gtserror.NewErrorInternalError(err)
@@ -105,7 +104,7 @@ func (p *Processor) StatusesGet(
for _, s := range filtered {
// Convert filtered statuses to API statuses.
- item, err := p.converter.StatusToAPIStatus(ctx, s, requestingAccount, statusfilter.FilterContextAccount, filters)
+ item, err := p.converter.StatusToAPIStatus(ctx, s, requestingAccount, gtsmodel.FilterContextAccount, filters)
if err != nil {
log.Errorf(ctx, "error convering to api status: %v", err)
continue
diff --git a/internal/processing/common/status.go b/internal/processing/common/status.go
index 441a58384..83acddc84 100644
--- a/internal/processing/common/status.go
+++ b/internal/processing/common/status.go
@@ -213,7 +213,7 @@ func (p *Processor) GetAPIStatus(
apiStatus, err := p.converter.StatusToAPIStatus(ctx,
target,
requester,
- statusfilter.FilterContextNone,
+ gtsmodel.FilterContextNone,
nil,
)
if err != nil {
@@ -234,7 +234,7 @@ func (p *Processor) GetVisibleAPIStatuses(
ctx context.Context,
requester *gtsmodel.Account,
statuses []*gtsmodel.Status,
- filterContext statusfilter.FilterContext,
+ filterCtx gtsmodel.FilterContext,
filters []*gtsmodel.Filter,
) []apimodel.Status {
@@ -277,7 +277,7 @@ func (p *Processor) GetVisibleAPIStatuses(
apiStatus, err := p.converter.StatusToAPIStatus(ctx,
status,
requester,
- filterContext,
+ filterCtx,
filters,
)
if err != nil && !errors.Is(err, statusfilter.ErrHideStatus) {
diff --git a/internal/processing/conversations/conversations.go b/internal/processing/conversations/conversations.go
index e31f60500..70fafa437 100644
--- a/internal/processing/conversations/conversations.go
+++ b/internal/processing/conversations/conversations.go
@@ -101,7 +101,7 @@ func (p *Processor) getFilters(
ctx context.Context,
requestingAccount *gtsmodel.Account,
) ([]*gtsmodel.Filter, gtserror.WithCode) {
- filters, err := p.state.DB.GetFiltersForAccountID(ctx, requestingAccount.ID)
+ filters, err := p.state.DB.GetFiltersByAccountID(ctx, requestingAccount.ID)
if err != nil {
return nil, gtserror.NewErrorInternalError(
gtserror.Newf(
diff --git a/internal/processing/filters/common/common.go b/internal/processing/filters/common/common.go
new file mode 100644
index 000000000..a119d3bd4
--- /dev/null
+++ b/internal/processing/filters/common/common.go
@@ -0,0 +1,184 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package common
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/http"
+
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
+ "code.superseriousbusiness.org/gotosocial/internal/db"
+ "code.superseriousbusiness.org/gotosocial/internal/gtscontext"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
+ "code.superseriousbusiness.org/gotosocial/internal/state"
+)
+
+type Processor struct{ state *state.State }
+
+func New(state *state.State) *Processor { return &Processor{state} }
+
+// CheckFilterExists calls .GetFilter() with a barebones context to not
+// fetch any sub-models, and not returning the result. this functionally
+// just uses .GetFilter() for the ownership and existence checks.
+func (p *Processor) CheckFilterExists(
+ ctx context.Context,
+ requester *gtsmodel.Account,
+ id string,
+) gtserror.WithCode {
+ _, errWithCode := p.GetFilter(gtscontext.SetBarebones(ctx), requester, id)
+ return errWithCode
+}
+
+// GetFilter fetches the filter with given ID, also checking
+// the given requesting account is the owner of the filter.
+func (p *Processor) GetFilter(
+ ctx context.Context,
+ requester *gtsmodel.Account,
+ id string,
+) (
+ *gtsmodel.Filter,
+ gtserror.WithCode,
+) {
+ // Get the filter from the database with given ID.
+ filter, err := p.state.DB.GetFilterByID(ctx, id)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("error getting filter: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // Check it exists.
+ if filter == nil {
+ const text = "filter not found"
+ return nil, gtserror.NewWithCode(http.StatusNotFound, text)
+ }
+
+ // Check that the requester owns it.
+ if filter.AccountID != requester.ID {
+ const text = "filter not found"
+ err := gtserror.New("filter does not belong to account")
+ return nil, gtserror.NewErrorNotFound(err, text)
+ }
+
+ return filter, nil
+}
+
+// GetFilterStatus fetches the filter status with given ID, also
+// checking the given requesting account is the owner of it.
+func (p *Processor) GetFilterStatus(
+ ctx context.Context,
+ requester *gtsmodel.Account,
+ id string,
+) (
+ *gtsmodel.FilterStatus,
+ *gtsmodel.Filter,
+ gtserror.WithCode,
+) {
+
+ // Get the filter status from the database with given ID.
+ filterStatus, err := p.state.DB.GetFilterStatusByID(ctx, id)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("error getting filter status: %w", err)
+ return nil, nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // Check it even exists.
+ if filterStatus == nil {
+ const text = "filter status not found"
+ return nil, nil, gtserror.NewWithCode(http.StatusNotFound, text)
+ }
+
+ // Get the filter this filter status is
+ // associated with, without sub-models.
+ // (this also checks filter ownership).
+ filter, errWithCode := p.GetFilter(
+ gtscontext.SetBarebones(ctx),
+ requester,
+ filterStatus.FilterID,
+ )
+ if errWithCode != nil {
+ return nil, nil, errWithCode
+ }
+
+ return filterStatus, filter, nil
+}
+
+// GetFilterKeyword fetches the filter keyword with given ID,
+// also checking the given requesting account is the owner of it.
+func (p *Processor) GetFilterKeyword(
+ ctx context.Context,
+ requester *gtsmodel.Account,
+ id string,
+) (
+ *gtsmodel.FilterKeyword,
+ *gtsmodel.Filter,
+ gtserror.WithCode,
+) {
+
+ // Get the filter keyword from the database with given ID.
+ keyword, err := p.state.DB.GetFilterKeywordByID(ctx, id)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("error getting filter keyword: %w", err)
+ return nil, nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // Check it exists.
+ if keyword == nil {
+ const text = "filter keyword not found"
+ return nil, nil, gtserror.NewWithCode(http.StatusNotFound, text)
+ }
+
+ // Get the filter this filter keyword is
+ // associated with, without sub-models.
+ // (this also checks filter ownership).
+ filter, errWithCode := p.GetFilter(
+ gtscontext.SetBarebones(ctx),
+ requester,
+ keyword.FilterID,
+ )
+ if errWithCode != nil {
+ return nil, nil, errWithCode
+ }
+
+ return keyword, filter, nil
+}
+
+// FromAPIContexts converts a slice of frontend API model FilterContext types to our internal FilterContexts bit field.
+func FromAPIContexts(apiContexts []apimodel.FilterContext) (gtsmodel.FilterContexts, gtserror.WithCode) {
+ var contexts gtsmodel.FilterContexts
+ for _, context := range apiContexts {
+ switch context {
+ case apimodel.FilterContextHome:
+ contexts.SetHome()
+ case apimodel.FilterContextNotifications:
+ contexts.SetNotifications()
+ case apimodel.FilterContextPublic:
+ contexts.SetPublic()
+ case apimodel.FilterContextThread:
+ contexts.SetThread()
+ case apimodel.FilterContextAccount:
+ contexts.SetAccount()
+ default:
+ text := fmt.Sprintf("unsupported filter context: %s", context)
+ return 0, gtserror.NewWithCode(http.StatusBadRequest, text)
+ }
+ }
+ return contexts, nil
+}
diff --git a/internal/processing/filters/v1/convert.go b/internal/processing/filters/v1/convert.go
deleted file mode 100644
index 417cf7b7d..000000000
--- a/internal/processing/filters/v1/convert.go
+++ /dev/null
@@ -1,38 +0,0 @@
-// GoToSocial
-// Copyright (C) GoToSocial Authors admin@gotosocial.org
-// SPDX-License-Identifier: AGPL-3.0-or-later
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-package v1
-
-import (
- "context"
- "fmt"
-
- apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
- "code.superseriousbusiness.org/gotosocial/internal/gtserror"
- "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
-)
-
-// apiFilter is a shortcut to return the API v1 filter version of the given
-// filter keyword, or return an appropriate error if conversion fails.
-func (p *Processor) apiFilter(ctx context.Context, filterKeyword *gtsmodel.FilterKeyword) (*apimodel.FilterV1, gtserror.WithCode) {
- apiFilter, err := p.converter.FilterKeywordToAPIFilterV1(ctx, filterKeyword)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting filter keyword to API v1 filter: %w", err))
- }
-
- return apiFilter, nil
-}
diff --git a/internal/processing/filters/v1/create.go b/internal/processing/filters/v1/create.go
index 24517dd7b..b2ec69442 100644
--- a/internal/processing/filters/v1/create.go
+++ b/internal/processing/filters/v1/create.go
@@ -20,7 +20,7 @@ package v1
import (
"context"
"errors"
- "fmt"
+ "net/http"
"time"
apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
@@ -28,68 +28,72 @@ import (
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"code.superseriousbusiness.org/gotosocial/internal/id"
+ "code.superseriousbusiness.org/gotosocial/internal/processing/filters/common"
+ "code.superseriousbusiness.org/gotosocial/internal/typeutils"
"code.superseriousbusiness.org/gotosocial/internal/util"
)
// Create a new filter and filter keyword for the given account, using the provided parameters.
// These params should have already been validated by the time they reach this function.
-func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, form *apimodel.FilterCreateUpdateRequestV1) (*apimodel.FilterV1, gtserror.WithCode) {
+func (p *Processor) Create(ctx context.Context, requester *gtsmodel.Account, form *apimodel.FilterCreateUpdateRequestV1) (*apimodel.FilterV1, gtserror.WithCode) {
+ var errWithCode gtserror.WithCode
+
+ // Create new wrapping filter.
filter := &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
}
diff --git a/internal/processing/processor.go b/internal/processing/processor.go
index b8adb9bb8..22574f1d7 100644
--- a/internal/processing/processor.go
+++ b/internal/processing/processor.go
@@ -34,6 +34,7 @@ import (
"code.superseriousbusiness.org/gotosocial/internal/processing/common"
"code.superseriousbusiness.org/gotosocial/internal/processing/conversations"
"code.superseriousbusiness.org/gotosocial/internal/processing/fedi"
+ filterCommon "code.superseriousbusiness.org/gotosocial/internal/processing/filters/common"
filtersv1 "code.superseriousbusiness.org/gotosocial/internal/processing/filters/v1"
filtersv2 "code.superseriousbusiness.org/gotosocial/internal/processing/filters/v2"
"code.superseriousbusiness.org/gotosocial/internal/processing/interactionrequests"
@@ -224,6 +225,7 @@ func NewProcessor(
processor.account = account.New(&common, state, converter, mediaManager, federator, visFilter, parseMentionFunc)
processor.media = media.New(&common, state, converter, federator, mediaManager, federator.TransportController())
processor.stream = stream.New(state, oauthServer)
+ filterCommon := filterCommon.New(state)
// Instantiate the rest of the sub
// processors + pin them to this struct.
@@ -232,8 +234,8 @@ func NewProcessor(
processor.application = application.New(state, converter)
processor.conversations = conversations.New(state, converter, visFilter, muteFilter)
processor.fedi = fedi.New(state, &common, converter, federator, visFilter)
- processor.filtersv1 = filtersv1.New(state, converter, &processor.stream)
- processor.filtersv2 = filtersv2.New(state, converter, &processor.stream)
+ processor.filtersv1 = filtersv1.New(state, converter, filterCommon, &processor.stream)
+ processor.filtersv2 = filtersv2.New(state, converter, filterCommon, &processor.stream)
processor.interactionRequests = interactionrequests.New(&common, state, converter)
processor.list = list.New(state, converter)
processor.markers = markers.New(state, converter)
diff --git a/internal/processing/search/util.go b/internal/processing/search/util.go
index 97eb813db..b4568722d 100644
--- a/internal/processing/search/util.go
+++ b/internal/processing/search/util.go
@@ -21,7 +21,6 @@ import (
"context"
apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
- statusfilter "code.superseriousbusiness.org/gotosocial/internal/filter/status"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"code.superseriousbusiness.org/gotosocial/internal/log"
@@ -114,7 +113,7 @@ func (p *Processor) packageStatuses(
continue
}
- apiStatus, err := p.converter.StatusToAPIStatus(ctx, status, requestingAccount, statusfilter.FilterContextNone, nil)
+ apiStatus, err := p.converter.StatusToAPIStatus(ctx, status, requestingAccount, gtsmodel.FilterContextNone, nil)
if err != nil {
log.Debugf(ctx, "skipping status %s because it couldn't be converted to its api representation: %s", status.ID, err)
continue
diff --git a/internal/processing/status/context.go b/internal/processing/status/context.go
index 6f3e7a4fd..531dff1d6 100644
--- a/internal/processing/status/context.go
+++ b/internal/processing/status/context.go
@@ -24,7 +24,6 @@ import (
"strings"
apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
- statusfilter "code.superseriousbusiness.org/gotosocial/internal/filter/status"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
)
@@ -278,7 +277,7 @@ func (p *Processor) ContextGet(
) (*apimodel.ThreadContext, gtserror.WithCode) {
// Retrieve filters as they affect
// what should be shown to requester.
- filters, err := p.state.DB.GetFiltersForAccountID(
+ filters, err := p.state.DB.GetFiltersByAccountID(
ctx, // Populate filters.
requester.ID,
)
@@ -305,7 +304,7 @@ func (p *Processor) ContextGet(
apiContext.Ancestors = p.c.GetVisibleAPIStatuses(ctx,
requester,
threadContext.ancestors,
- statusfilter.FilterContextThread,
+ gtsmodel.FilterContextThread,
filters,
)
@@ -313,7 +312,7 @@ func (p *Processor) ContextGet(
apiContext.Descendants = p.c.GetVisibleAPIStatuses(ctx,
requester,
threadContext.descendants,
- statusfilter.FilterContextThread,
+ gtsmodel.FilterContextThread,
filters,
)
diff --git a/internal/processing/stream/statusupdate_test.go b/internal/processing/stream/statusupdate_test.go
index 8fc4bcfe8..74e7a4933 100644
--- a/internal/processing/stream/statusupdate_test.go
+++ b/internal/processing/stream/statusupdate_test.go
@@ -22,7 +22,7 @@ import (
"encoding/json"
"testing"
- statusfilter "code.superseriousbusiness.org/gotosocial/internal/filter/status"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"code.superseriousbusiness.org/gotosocial/internal/stream"
"code.superseriousbusiness.org/gotosocial/internal/typeutils"
"github.com/stretchr/testify/suite"
@@ -39,7 +39,7 @@ func (suite *StatusUpdateTestSuite) TestStreamNotification() {
suite.NoError(errWithCode)
editedStatus := suite.testStatuses["remote_account_1_status_1"]
- apiStatus, err := typeutils.NewConverter(&suite.state).StatusToAPIStatus(suite.T().Context(), editedStatus, account, statusfilter.FilterContextNotifications, nil)
+ apiStatus, err := typeutils.NewConverter(&suite.state).StatusToAPIStatus(suite.T().Context(), editedStatus, account, gtsmodel.FilterContextNotifications, nil)
suite.NoError(err)
suite.streamProcessor.StatusUpdate(suite.T().Context(), account, apiStatus, stream.TimelineHome)
diff --git a/internal/processing/timeline/faved.go b/internal/processing/timeline/faved.go
index 84788a8fa..c1b44fa92 100644
--- a/internal/processing/timeline/faved.go
+++ b/internal/processing/timeline/faved.go
@@ -25,8 +25,8 @@ import (
apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util"
"code.superseriousbusiness.org/gotosocial/internal/db"
- statusfilter "code.superseriousbusiness.org/gotosocial/internal/filter/status"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"code.superseriousbusiness.org/gotosocial/internal/log"
"code.superseriousbusiness.org/gotosocial/internal/util"
)
@@ -56,7 +56,7 @@ func (p *Processor) FavedTimelineGet(ctx context.Context, authed *apiutil.Auth,
continue
}
- apiStatus, err := p.converter.StatusToAPIStatus(ctx, s, authed.Account, statusfilter.FilterContextNone, nil)
+ apiStatus, err := p.converter.StatusToAPIStatus(ctx, s, authed.Account, gtsmodel.FilterContextNone, nil)
if err != nil {
log.Errorf(ctx, "error convering to api status: %v", err)
continue
diff --git a/internal/processing/timeline/home.go b/internal/processing/timeline/home.go
index ba74b770c..3089f52fc 100644
--- a/internal/processing/timeline/home.go
+++ b/internal/processing/timeline/home.go
@@ -22,7 +22,6 @@ import (
"net/url"
apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
- statusfilter "code.superseriousbusiness.org/gotosocial/internal/filter/status"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"code.superseriousbusiness.org/gotosocial/internal/log"
@@ -77,7 +76,7 @@ func (p *Processor) HomeTimelineGet(
pageQuery,
// Status filter context.
- statusfilter.FilterContextHome,
+ gtsmodel.FilterContextHome,
// Database load function.
func(pg *paging.Page) (statuses []*gtsmodel.Status, err error) {
diff --git a/internal/processing/timeline/home_test.go b/internal/processing/timeline/home_test.go
index 184d361f0..2d0c912f8 100644
--- a/internal/processing/timeline/home_test.go
+++ b/internal/processing/timeline/home_test.go
@@ -24,7 +24,6 @@ import (
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"code.superseriousbusiness.org/gotosocial/internal/id"
"code.superseriousbusiness.org/gotosocial/internal/paging"
- "code.superseriousbusiness.org/gotosocial/internal/util"
"github.com/stretchr/testify/suite"
)
@@ -49,24 +48,20 @@ func (suite *HomeTestSuite) TestHomeTimelineGetHideFiltered() {
filteredStatus = suite.testStatuses["admin_account_status_2"]
filteredStatusFound = false
filterID = id.NewULID()
- filter = &gtsmodel.Filter{
+ filterStatusID = id.NewULID()
+ filterStatus = &gtsmodel.FilterStatus{
+ ID: filterStatusID,
+ FilterID: filterID,
+ StatusID: filteredStatus.ID,
+ }
+ filter = &gtsmodel.Filter{
ID: filterID,
AccountID: requester.ID,
Title: "timeline filtering test",
Action: gtsmodel.FilterActionHide,
- Statuses: []*gtsmodel.FilterStatus{
- {
- ID: id.NewULID(),
- AccountID: requester.ID,
- FilterID: filterID,
- StatusID: filteredStatus.ID,
- },
- },
- ContextHome: util.Ptr(true),
- ContextNotifications: util.Ptr(false),
- ContextPublic: util.Ptr(false),
- ContextThread: util.Ptr(false),
- ContextAccount: util.Ptr(false),
+ Statuses: []*gtsmodel.FilterStatus{filterStatus},
+ StatusIDs: []string{filterStatusID},
+ Contexts: gtsmodel.FilterContexts(gtsmodel.FilterContextHome),
}
)
@@ -95,6 +90,11 @@ func (suite *HomeTestSuite) TestHomeTimelineGetHideFiltered() {
// Clear the timeline to drop all cached statuses.
suite.state.Caches.Timelines.Home.Clear(requester.ID)
+ // Create the filter status associated with the main filter.
+ if err := suite.db.PutFilterStatus(ctx, filterStatus); err != nil {
+ suite.FailNow(err.Error())
+ }
+
// Create a filter to hide one status on the timeline.
if err := suite.db.PutFilter(ctx, filter); err != nil {
suite.FailNow(err.Error())
diff --git a/internal/processing/timeline/list.go b/internal/processing/timeline/list.go
index c8e6bc5f1..265cd5ca2 100644
--- a/internal/processing/timeline/list.go
+++ b/internal/processing/timeline/list.go
@@ -23,7 +23,6 @@ import (
apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
"code.superseriousbusiness.org/gotosocial/internal/db"
- statusfilter "code.superseriousbusiness.org/gotosocial/internal/filter/status"
"code.superseriousbusiness.org/gotosocial/internal/gtscontext"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
@@ -88,7 +87,7 @@ func (p *Processor) ListTimelineGet(
nil,
// Status filter context.
- statusfilter.FilterContextHome,
+ gtsmodel.FilterContextHome,
// Database load function.
func(pg *paging.Page) (statuses []*gtsmodel.Status, err error) {
diff --git a/internal/processing/timeline/notification.go b/internal/processing/timeline/notification.go
index ad60fd90c..143145bb9 100644
--- a/internal/processing/timeline/notification.go
+++ b/internal/processing/timeline/notification.go
@@ -59,7 +59,7 @@ func (p *Processor) NotificationsGet(
return util.EmptyPageableResponse(), nil
}
- filters, err := p.state.DB.GetFiltersForAccountID(ctx, requester.ID)
+ filters, err := p.state.DB.GetFiltersByAccountID(ctx, requester.ID)
if err != nil {
err = gtserror.Newf("error getting account %s filters: %w", requester.ID, err)
return nil, gtserror.NewErrorInternalError(err)
diff --git a/internal/processing/timeline/public.go b/internal/processing/timeline/public.go
index cfb58201d..d724bfaa1 100644
--- a/internal/processing/timeline/public.go
+++ b/internal/processing/timeline/public.go
@@ -21,7 +21,6 @@ import (
"context"
apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
- statusfilter "code.superseriousbusiness.org/gotosocial/internal/filter/status"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"code.superseriousbusiness.org/gotosocial/internal/log"
@@ -79,7 +78,7 @@ func (p *Processor) publicTimelineGet(
localOnlyFalse,
// Status filter context.
- statusfilter.FilterContextPublic,
+ gtsmodel.FilterContextPublic,
// Database load function.
func(pg *paging.Page) (statuses []*gtsmodel.Status, err error) {
@@ -148,7 +147,7 @@ func (p *Processor) localTimelineGet(
localOnlyTrue,
// Status filter context.
- statusfilter.FilterContextPublic,
+ gtsmodel.FilterContextPublic,
// Database load function.
func(pg *paging.Page) (statuses []*gtsmodel.Status, err error) {
diff --git a/internal/processing/timeline/public_test.go b/internal/processing/timeline/public_test.go
index 5aa09f138..3320a45da 100644
--- a/internal/processing/timeline/public_test.go
+++ b/internal/processing/timeline/public_test.go
@@ -24,7 +24,6 @@ import (
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"code.superseriousbusiness.org/gotosocial/internal/id"
"code.superseriousbusiness.org/gotosocial/internal/paging"
- "code.superseriousbusiness.org/gotosocial/internal/util"
"github.com/stretchr/testify/suite"
)
@@ -110,24 +109,20 @@ func (suite *PublicTestSuite) TestPublicTimelineGetHideFiltered() {
filteredStatus = suite.testStatuses["admin_account_status_2"]
filteredStatusFound = false
filterID = id.NewULID()
- filter = &gtsmodel.Filter{
+ filterStatusID = id.NewULID()
+ filterStatus = &gtsmodel.FilterStatus{
+ ID: filterStatusID,
+ FilterID: filterID,
+ StatusID: filteredStatus.ID,
+ }
+ filter = &gtsmodel.Filter{
ID: filterID,
AccountID: requester.ID,
Title: "timeline filtering test",
Action: gtsmodel.FilterActionHide,
- Statuses: []*gtsmodel.FilterStatus{
- {
- ID: id.NewULID(),
- AccountID: requester.ID,
- FilterID: filterID,
- StatusID: filteredStatus.ID,
- },
- },
- ContextHome: util.Ptr(false),
- ContextNotifications: util.Ptr(false),
- ContextPublic: util.Ptr(true),
- ContextThread: util.Ptr(false),
- ContextAccount: util.Ptr(false),
+ Statuses: []*gtsmodel.FilterStatus{filterStatus},
+ StatusIDs: []string{filterStatusID},
+ Contexts: gtsmodel.FilterContexts(gtsmodel.FilterContextPublic),
}
)
@@ -153,6 +148,11 @@ func (suite *PublicTestSuite) TestPublicTimelineGetHideFiltered() {
suite.FailNow("precondition failed: status we would filter isn't present in unfiltered timeline")
}
+ // Create the filter status associated with the main filter.
+ if err := suite.db.PutFilterStatus(ctx, filterStatus); err != nil {
+ suite.FailNow(err.Error())
+ }
+
// Create a filter to hide one status on the timeline.
if err := suite.db.PutFilter(ctx, filter); err != nil {
suite.FailNow(err.Error())
diff --git a/internal/processing/timeline/tag.go b/internal/processing/timeline/tag.go
index 88333d343..995f9f8cc 100644
--- a/internal/processing/timeline/tag.go
+++ b/internal/processing/timeline/tag.go
@@ -24,7 +24,6 @@ import (
apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
"code.superseriousbusiness.org/gotosocial/internal/db"
- statusfilter "code.superseriousbusiness.org/gotosocial/internal/filter/status"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"code.superseriousbusiness.org/gotosocial/internal/log"
@@ -87,7 +86,7 @@ func (p *Processor) TagTimelineGet(
nil,
// Status filter context.
- statusfilter.FilterContextPublic,
+ gtsmodel.FilterContextPublic,
// Database load function.
func(pg *paging.Page) (statuses []*gtsmodel.Status, err error) {
diff --git a/internal/processing/timeline/timeline.go b/internal/processing/timeline/timeline.go
index a86702d42..a37785879 100644
--- a/internal/processing/timeline/timeline.go
+++ b/internal/processing/timeline/timeline.go
@@ -70,7 +70,7 @@ func (p *Processor) getStatusTimeline(
page *paging.Page,
pagePath string,
pageQuery url.Values,
- filterCtx statusfilter.FilterContext,
+ filterCtx gtsmodel.FilterContext,
loadPage func(*paging.Page) (statuses []*gtsmodel.Status, err error),
filter func(*gtsmodel.Status) (delete bool),
postFilter func(*gtsmodel.Status) (remove bool),
@@ -83,7 +83,7 @@ func (p *Processor) getStatusTimeline(
if requester != nil {
// Fetch all filters relevant for requesting account.
- filters, err = p.state.DB.GetFiltersForAccountID(ctx,
+ filters, err = p.state.DB.GetFiltersByAccountID(ctx,
requester.ID,
)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
diff --git a/internal/processing/workers/fromclientapi_test.go b/internal/processing/workers/fromclientapi_test.go
index 3f6964259..1c30c11be 100644
--- a/internal/processing/workers/fromclientapi_test.go
+++ b/internal/processing/workers/fromclientapi_test.go
@@ -27,7 +27,6 @@ import (
"code.superseriousbusiness.org/gotosocial/internal/ap"
"code.superseriousbusiness.org/gotosocial/internal/config"
"code.superseriousbusiness.org/gotosocial/internal/db"
- statusfilter "code.superseriousbusiness.org/gotosocial/internal/filter/status"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"code.superseriousbusiness.org/gotosocial/internal/id"
"code.superseriousbusiness.org/gotosocial/internal/messages"
@@ -213,7 +212,7 @@ func (suite *FromClientAPITestSuite) statusJSON(
ctx,
status,
requestingAccount,
- statusfilter.FilterContextNone,
+ gtsmodel.FilterContextNone,
nil,
)
if err != nil {
diff --git a/internal/processing/workers/surfacenotify.go b/internal/processing/workers/surfacenotify.go
index 044315349..b11fb103e 100644
--- a/internal/processing/workers/surfacenotify.go
+++ b/internal/processing/workers/surfacenotify.go
@@ -743,7 +743,7 @@ func (s *Surface) Notify(
}
}
- filters, err := s.State.DB.GetFiltersForAccountID(ctx, targetAccount.ID)
+ filters, err := s.State.DB.GetFiltersByAccountID(ctx, targetAccount.ID)
if err != nil {
return gtserror.Newf("couldn't retrieve filters for account %s: %w", targetAccount.ID, err)
}
diff --git a/internal/processing/workers/surfacetimeline.go b/internal/processing/workers/surfacetimeline.go
index 7ef5fee87..7f9bcd596 100644
--- a/internal/processing/workers/surfacetimeline.go
+++ b/internal/processing/workers/surfacetimeline.go
@@ -180,7 +180,7 @@ func (s *Surface) timelineAndNotifyStatusForFollowers(
follow.Account,
status,
stream.TimelineHome,
- statusfilter.FilterContextHome,
+ gtsmodel.FilterContextHome,
filters,
); homeTimelined {
@@ -275,7 +275,7 @@ func (s *Surface) listTimelineStatusForFollow(
follow.Account,
status,
stream.TimelineList+":"+list.ID, // key streamType to this specific list
- statusfilter.FilterContextHome,
+ gtsmodel.FilterContextHome,
filters,
)
@@ -288,7 +288,7 @@ func (s *Surface) listTimelineStatusForFollow(
// getFiltersAndMutes returns an account's filters and mutes.
func (s *Surface) getFilters(ctx context.Context, accountID string) ([]*gtsmodel.Filter, error) {
- filters, err := s.State.DB.GetFiltersForAccountID(ctx, accountID)
+ filters, err := s.State.DB.GetFiltersByAccountID(ctx, accountID)
if err != nil {
return nil, gtserror.Newf("couldn't retrieve filters for account %s: %w", accountID, err)
}
@@ -369,7 +369,7 @@ func (s *Surface) timelineStatus(
account *gtsmodel.Account,
status *gtsmodel.Status,
streamType string,
- filterCtx statusfilter.FilterContext,
+ filterCtx gtsmodel.FilterContext,
filters []*gtsmodel.Filter,
) bool {
@@ -436,7 +436,7 @@ func (s *Surface) timelineAndNotifyStatusForTagFollowers(
tagFollowerAccount,
status,
stream.TimelineHome,
- statusfilter.FilterContextHome,
+ gtsmodel.FilterContextHome,
filters,
)
}
@@ -731,7 +731,7 @@ func (s *Surface) timelineStreamStatusUpdate(
apiStatus, err := s.Converter.StatusToAPIStatus(ctx,
status,
account,
- statusfilter.FilterContextHome,
+ gtsmodel.FilterContextHome,
filters,
)