[performance] filter model and database table improvements (#4277)

- removes unnecessary fields / columns (created_at, updated_at)
- replaces filter.context_* columns with singular filter.contexts bit field which should save both struct memory and database space
- replaces filter.action string with integer enum type which should save both struct memory and database space
- adds links from filter to filter_* tables with Filter{}.KeywordIDs and Filter{}.StatusIDs fields (this also means we now have those ID slices cached, which reduces some lookups)
- removes account_id fields from filter_* tables, since there's a more direct connection between filter and filter_* tables, and filter.account_id already exists
- refactors a bunch of the filter processor logic to save on code repetition, factor in the above changes, fix a few bugs with missed error returns and bring it more in-line with some of our newer code

Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4277
Co-authored-by: kim <grufwub@gmail.com>
Co-committed-by: kim <grufwub@gmail.com>
This commit is contained in:
kim 2025-06-24 17:24:34 +02:00 committed by tobi
commit 996da6e029
82 changed files with 2440 additions and 1722 deletions

View file

@ -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

View file

@ -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

View file

@ -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) {

View file

@ -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(

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}
if form.ExpiresIn != nil && *form.ExpiresIn != 0 {
filter.ExpiresAt = time.Now().Add(time.Second * time.Duration(*form.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),
)
}
} else {
// Default action = action warn.
filter.Action = gtsmodel.FilterActionWarn
}
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)
// 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)
}
apiFilter, errWithCode := p.apiFilter(ctx, filterKeyword)
// Parse contexts filter applies in from incoming request form data.
filter.Contexts, errWithCode = common.FromAPIContexts(form.Context)
if errWithCode != nil {
return nil, errWithCode
}
// Send a filters changed event.
p.stream.FiltersChanged(ctx, account)
// Create new keyword attached to filter.
filterKeyword := &gtsmodel.FilterKeyword{
ID: id.NewULID(),
FilterID: filter.ID,
Keyword: form.Phrase,
WholeWord: util.Ptr(util.PtrOrValue(form.WholeWord, false)),
}
return apiFilter, nil
// Attach the new keyword to filter before insert.
filter.Keywords = append(filter.Keywords, filterKeyword)
filter.KeywordIDs = append(filter.KeywordIDs, filterKeyword.ID)
// 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)
}
// Stream a filters changed event to WS.
p.stream.FiltersChanged(ctx, requester)
// Return as converted frontend filter keyword model.
return typeutils.FilterKeywordToAPIFilterV1(filter, filterKeyword), nil
}

View file

@ -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
}

View file

@ -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,

View file

@ -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)
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
}
if filterKeyword.AccountID != account.ID {
return nil, gtserror.NewErrorNotFound(nil)
}
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.

View file

@ -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))
// Filter columns that
// we're going to update.
var filterCols []string
var keywordCols []string
// We only want to update the relevant filter keyword.
filter.Keywords = []*gtsmodel.FilterKeyword{filterKeyword}
filter.Statuses = nil
filterKeyword.Filter = filter
// 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
}
filterColumns := []string{
"title",
"action",
"expires_at",
"context_home",
"context_notifications",
"context_public",
"context_thread",
"context_account",
// Check for changed action.
if action != filter.Action {
filterCols = append(filterCols, "action")
filter.Action = action
}
filterKeywordColumns := [][]string{
{
"keyword",
"whole_word",
},
// Check for changed filter expiry time.
if !expiresAt.Equal(filter.ExpiresAt) {
filterCols = append(filterCols, "expires_at")
filter.ExpiresAt = expiresAt
}
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 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
}

View file

@ -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
}

View file

@ -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))
}
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),
)
}
}
for _, formKeyword := range form.Keywords {
filterKeyword := &gtsmodel.FilterKeyword{
ID: id.NewULID(),
AccountID: account.ID,
FilterID: filter.ID,
Filter: filter,
Keyword: formKeyword.Keyword,
WholeWord: formKeyword.WholeWord,
}
filter.Keywords = append(filter.Keywords, filterKeyword)
// 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 _, formStatus := range form.Statuses {
filterStatus := &gtsmodel.FilterStatus{
ID: id.NewULID(),
AccountID: account.ID,
FilterID: filter.ID,
Filter: filter,
StatusID: formStatus.StatusID,
}
filter.Statuses = append(filter.Statuses, filterStatus)
}
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)
}
apiFilter, errWithCode := p.apiFilter(ctx, filter)
// 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)
}
// Create new attached filter keywords.
for _, keyword := range form.Keywords {
filterKeyword := &gtsmodel.FilterKeyword{
ID: id.NewULID(),
FilterID: filter.ID,
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)
}
// Create new attached filter statuses.
for _, status := range form.Statuses {
filterStatus := &gtsmodel.FilterStatus{
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)
}
// Insert the new filter model 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, 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
}

View file

@ -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)
// Get the filter with given ID, also checking ownership.
filter, errWithCode := p.c.GetFilter(ctx, requester, filterID)
if errWithCode != nil {
return errWithCode
}
// 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),
)
}
// 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
}

View file

@ -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,

View file

@ -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.

View file

@ -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)
}
// Send a filters changed event.
p.stream.FiltersChanged(ctx, account)
// Now update the filter it is attached to with new keyword.
filter.KeywordIDs = append(filter.KeywordIDs, filterKeyword.ID)
filter.Keywords = append(filter.Keywords, filterKeyword)
return p.converter.FilterKeywordToAPIFilterKeyword(ctx, filterKeyword), nil
// 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)
}
// Stream a filters changed event to WS.
p.stream.FiltersChanged(ctx, requester)
return typeutils.FilterKeywordToAPIFilterKeyword(filterKeyword), nil
}

View file

@ -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 the filter keyword.
if err := p.state.DB.DeleteFilterKeywordByID(ctx, filterKeyword.ID); err != nil {
// 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)
}
// Send a filters changed event.
p.stream.FiltersChanged(ctx, account)
// 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)
}
// Stream a filters changed event to WS.
p.stream.FiltersChanged(ctx, requester)
return nil
}

View file

@ -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)
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
}
if filterKeyword.AccountID != account.ID {
return nil, gtserror.NewErrorNotFound(
fmt.Errorf("filter keyword %s doesn't belong to account %s", filterKeyword.ID, account.ID),
)
}
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.

View file

@ -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
}

View file

@ -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)
}
// Send a filters changed event.
p.stream.FiltersChanged(ctx, account)
// Now update the filter it is attached to with new status.
filter.StatusIDs = append(filter.StatusIDs, filterStatus.ID)
filter.Statuses = append(filter.Statuses, filterStatus)
return p.converter.FilterStatusToAPIFilterStatus(ctx, filterStatus), nil
// 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)
}
// Stream a filters changed event to WS.
p.stream.FiltersChanged(ctx, requester)
return typeutils.FilterStatusToAPIFilterStatus(filterStatus), nil
}

View file

@ -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 the filter status.
if err := p.state.DB.DeleteFilterStatusByID(ctx, filterStatus.ID); err != nil {
// 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)
}
// Send a filters changed event.
p.stream.FiltersChanged(ctx, account)
// 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)
}
// Stream a filters changed event to WS.
p.stream.FiltersChanged(ctx, requester)
return nil
}

View file

@ -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)
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
}
if filterStatus.AccountID != account.ID {
return nil, gtserror.NewErrorNotFound(
fmt.Errorf("filter status %s doesn't belong to account %s", filterStatus.ID, account.ID),
)
}
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.

View file

@ -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),
)
}
// Filter columns that we're going to update.
filterColumns := []string{}
// Apply filter changes.
if form.Title != nil {
filterColumns = append(filterColumns, "title")
filter.Title = *form.Title
}
if form.FilterAction != nil {
filterColumns = append(filterColumns, "action")
filter.Action = typeutils.APIFilterActionToFilterAction(*form.FilterAction)
}
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))
}
}
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),
)
}
}
}
filterKeywordColumns, deleteFilterKeywordIDs, errWithCode := applyKeywordChanges(filter, form.Keywords)
if err != nil {
return nil, errWithCode
}
deleteFilterStatusIDs, errWithCode := applyStatusChanges(filter, form.Statuses)
if err != nil {
return nil, errWithCode
}
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)
}
apiFilter, errWithCode := p.apiFilter(ctx, filter)
// Get the filter with given ID, also checking ownership.
filter, errWithCode := p.c.GetFilter(ctx, requester, filterID)
if errWithCode != nil {
return nil, errWithCode
}
// Send a filters changed event.
p.stream.FiltersChanged(ctx, account)
// Filter columns that
// we're going to update.
cols := make([]string, 0, 6)
return apiFilter, nil
// Check for title change.
if form.Title != nil {
cols = append(cols, "title")
filter.Title = *form.Title
}
// Check action type change.
if form.FilterAction != nil {
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 {
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 {
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
}
}
// 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")
}
// 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 {
// Attached statuses have changed.
cols = append(cols, "statuses")
}
// Perform all the deferred database queries.
errWithCode = performTxs(keywordQs, statusQs)
if errWithCode != nil {
return nil, errWithCode
}
// Update the filter model in the database with determined cols.
switch err := p.state.DB.UpdateFilter(ctx, filter, cols...); {
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)
}
// Stream a filters changed event to WS.
p.stream.FiltersChanged(ctx, requester)
// Return as converted frontend filter model.
return typeutils.FilterToAPIFilterV2(filter), nil
}
// 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
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
}
deleteFilterKeywordIDs := []string{}
filterKeywordsByID := map[string]*gtsmodel.FilterKeyword{}
filterKeywordColumnsByID := map[string][]string{}
for _, filterKeyword := range filter.Keywords {
filterKeywordsByID[filterKeyword.ID] = filterKeyword
}
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
}
if formKeyword.WholeWord != nil {
columns = append(columns, "whole_word")
filterKeyword.WholeWord = formKeyword.WholeWord
// Check for changes to wholeword flag.
if val := request.WholeWord; val != nil {
cols = append(cols, "whole_word")
filterKeyword.WholeWord = val
}
filterKeywordColumnsByID[id] = columns
// 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
})
}
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
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
}
deleteFilterStatusIDs := []string{}
filterStatusesByID := map[string]*gtsmodel.FilterStatus{}
for _, filterStatus := range filter.Statuses {
filterStatusesByID[filterStatus.ID] = filterStatus
}
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.
filterStatus := &gtsmodel.FilterStatus{
ID: id.NewULID(),
AccountID: filter.AccountID,
FilterID: filter.ID,
Filter: filter,
StatusID: *formStatus.StatusID,
// Check for valid request.
if request.StatusID == nil {
const text = "missing status"
return deferred, gtserror.NewWithCode(http.StatusBadRequest, text)
}
filterStatusesByID[filterStatus.ID] = filterStatus
}
// Replace the filter's keywords list with our updated version.
filter.Statuses = nil
for _, filterStatus := range filterStatusesByID {
// Create new filter status for insert.
filterStatus := &gtsmodel.FilterStatus{
ID: id.NewULID(),
FilterID: filter.ID,
StatusID: *request.StatusID,
}
// 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 deleteFilterStatusIDs, 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 nil
}

View file

@ -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)

View file

@ -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

View file

@ -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,
)

View file

@ -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)

View file

@ -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

View file

@ -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) {

View file

@ -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())

View file

@ -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) {

View file

@ -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)

View file

@ -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) {

View file

@ -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())

View file

@ -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) {

View file

@ -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) {

View file

@ -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 {

View file

@ -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)
}

View file

@ -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,
)