mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-11-30 12:03:34 -06:00
[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:
parent
9d5af6c3dc
commit
996da6e029
82 changed files with 2440 additions and 1722 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
184
internal/processing/filters/common/common.go
Normal file
184
internal/processing/filters/common/common.go
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -20,7 +20,7 @@ package v1
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
|
||||
|
|
@ -28,68 +28,72 @@ import (
|
|||
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/id"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/processing/filters/common"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/typeutils"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
// Create a new filter and filter keyword for the given account, using the provided parameters.
|
||||
// These params should have already been validated by the time they reach this function.
|
||||
func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, form *apimodel.FilterCreateUpdateRequestV1) (*apimodel.FilterV1, gtserror.WithCode) {
|
||||
func (p *Processor) Create(ctx context.Context, requester *gtsmodel.Account, form *apimodel.FilterCreateUpdateRequestV1) (*apimodel.FilterV1, gtserror.WithCode) {
|
||||
var errWithCode gtserror.WithCode
|
||||
|
||||
// Create new wrapping filter.
|
||||
filter := >smodel.Filter{
|
||||
ID: id.NewULID(),
|
||||
AccountID: account.ID,
|
||||
AccountID: requester.ID,
|
||||
Title: form.Phrase,
|
||||
Action: gtsmodel.FilterActionWarn,
|
||||
}
|
||||
|
||||
if *form.Irreversible {
|
||||
// Irreversible = action hide.
|
||||
filter.Action = gtsmodel.FilterActionHide
|
||||
}
|
||||
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 := >smodel.FilterKeyword{
|
||||
ID: id.NewULID(),
|
||||
AccountID: account.ID,
|
||||
FilterID: filter.ID,
|
||||
Filter: filter,
|
||||
Keyword: form.Phrase,
|
||||
WholeWord: util.Ptr(util.PtrOrValue(form.WholeWord, false)),
|
||||
}
|
||||
filter.Keywords = []*gtsmodel.FilterKeyword{filterKeyword}
|
||||
|
||||
if err := p.state.DB.PutFilter(ctx, filter); err != nil {
|
||||
if errors.Is(err, db.ErrAlreadyExists) {
|
||||
err = errors.New("you already have a filter with this title")
|
||||
return nil, gtserror.NewErrorConflict(err, err.Error())
|
||||
}
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
// 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 := >smodel.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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 := >smodel.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 := >smodel.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 := >smodel.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 := >smodel.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 := >smodel.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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 := >smodel.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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 := >smodel.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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 := >smodel.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 := >smodel.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 := >smodel.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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 = >smodel.Filter{
|
||||
filterStatusID = id.NewULID()
|
||||
filterStatus = >smodel.FilterStatus{
|
||||
ID: filterStatusID,
|
||||
FilterID: filterID,
|
||||
StatusID: filteredStatus.ID,
|
||||
}
|
||||
filter = >smodel.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())
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 = >smodel.Filter{
|
||||
filterStatusID = id.NewULID()
|
||||
filterStatus = >smodel.FilterStatus{
|
||||
ID: filterStatusID,
|
||||
FilterID: filterID,
|
||||
StatusID: filteredStatus.ID,
|
||||
}
|
||||
filter = >smodel.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())
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue