diff --git a/cmd/gotosocial/action/server/server.go b/cmd/gotosocial/action/server/server.go index 30e4c28d0..9bbb4bf09 100644 --- a/cmd/gotosocial/action/server/server.go +++ b/cmd/gotosocial/action/server/server.go @@ -43,6 +43,7 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/filter/interaction" "code.superseriousbusiness.org/gotosocial/internal/filter/mutes" "code.superseriousbusiness.org/gotosocial/internal/filter/spam" + "code.superseriousbusiness.org/gotosocial/internal/filter/status" "code.superseriousbusiness.org/gotosocial/internal/filter/visibility" "code.superseriousbusiness.org/gotosocial/internal/gtserror" "code.superseriousbusiness.org/gotosocial/internal/httpclient" @@ -271,6 +272,7 @@ var Start action.GTSAction = func(ctx context.Context) error { visFilter := visibility.NewFilter(state) muteFilter := mutes.NewFilter(state) intFilter := interaction.NewFilter(state) + statusFilter := status.NewFilter(state) spamFilter := spam.NewFilter(state) federatingDB := federatingdb.New(state, typeConverter, visFilter, intFilter, spamFilter) transportController := transport.NewController(state, federatingDB, client) @@ -352,6 +354,7 @@ var Start action.GTSAction = func(ctx context.Context) error { visFilter, muteFilter, intFilter, + statusFilter, ) // Schedule background cleaning tasks. diff --git a/internal/api/wellknown/webfinger/webfingerget_test.go b/internal/api/wellknown/webfinger/webfingerget_test.go index 2bdc0f461..395512472 100644 --- a/internal/api/wellknown/webfinger/webfingerget_test.go +++ b/internal/api/wellknown/webfinger/webfingerget_test.go @@ -34,6 +34,7 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/config" "code.superseriousbusiness.org/gotosocial/internal/filter/interaction" "code.superseriousbusiness.org/gotosocial/internal/filter/mutes" + "code.superseriousbusiness.org/gotosocial/internal/filter/status" "code.superseriousbusiness.org/gotosocial/internal/filter/visibility" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/processing" @@ -101,6 +102,7 @@ func (suite *WebfingerGetTestSuite) funkifyAccountDomain(host string, accountDom visibility.NewFilter(&suite.state), mutes.NewFilter(&suite.state), interaction.NewFilter(&suite.state), + status.NewFilter(&suite.state), ) suite.webfingerModule = webfinger.New(suite.processor) diff --git a/internal/filter/status/status.go b/internal/filter/status/status.go index 5f997129d..e38131ae3 100644 --- a/internal/filter/status/status.go +++ b/internal/filter/status/status.go @@ -25,6 +25,7 @@ import ( apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model" "code.superseriousbusiness.org/gotosocial/internal/cache" + "code.superseriousbusiness.org/gotosocial/internal/gtscontext" "code.superseriousbusiness.org/gotosocial/internal/gtserror" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" ) @@ -159,6 +160,31 @@ func (f *Filter) getStatusFilterResults( return results, nil } + // Check if status is boost. + if status.BoostOfID != "" { + if status.BoostOf == nil { + var err error + + // Ensure original status is loaded on boost. + status.BoostOf, err = f.state.DB.GetStatusByID( + gtscontext.SetBarebones(ctx), + status.BoostOfID, + ) + if err != nil { + return results, gtserror.Newf("error getting boosted status of %s: %w", status.URI, err) + } + } + + // From here look at details + // for original boosted status. + status = status.BoostOf + } + + // For proper status filtering we need all fields populated. + if err := f.state.DB.PopulateStatus(ctx, status); err != nil { + return results, gtserror.Newf("error populating status: %w", err) + } + // Get the string fields status is // filterable on for keyword matching. fields := getFilterableFields(status) @@ -169,11 +195,6 @@ func (f *Filter) getStatusFilterResults( return results, gtserror.Newf("error getting account filters: %w", err) } - // For proper status filtering we need all fields populated. - if err := f.state.DB.PopulateStatus(ctx, status); err != nil { - return results, gtserror.Newf("error populating status: %w", err) - } - // Generate result for each filter. for _, filter := range filters { diff --git a/internal/filter/status/status_test.go b/internal/filter/status/status_test.go new file mode 100644 index 000000000..e81b7d34e --- /dev/null +++ b/internal/filter/status/status_test.go @@ -0,0 +1,201 @@ +// 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 . + +package status_test + +import ( + "testing" + + apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model" + "code.superseriousbusiness.org/gotosocial/internal/filter/status" + "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" + "code.superseriousbusiness.org/gotosocial/internal/id" + "code.superseriousbusiness.org/gotosocial/internal/state" + "code.superseriousbusiness.org/gotosocial/internal/typeutils" + "code.superseriousbusiness.org/gotosocial/testrig" + "github.com/stretchr/testify/suite" +) + +type StatusFilterTestSuite struct { + suite.Suite + + state state.State + filter *status.Filter + converter *typeutils.Converter + + testAccounts map[string]*gtsmodel.Account + testFilters map[string]*gtsmodel.Filter + testStatuses map[string]*gtsmodel.Status +} + +func (suite *StatusFilterTestSuite) SetupTest() { + suite.state.Caches.Init() + + testrig.InitTestConfig() + testrig.InitTestLog() + + db := testrig.NewTestDB(&suite.state) + suite.state.DB = db + + suite.filter = status.NewFilter(&suite.state) + + suite.converter = typeutils.NewConverter(&suite.state) + + suite.testAccounts = testrig.NewTestAccounts() + suite.testFilters = testrig.NewTestFilters() + suite.testStatuses = testrig.NewTestStatuses() + + testrig.StandardDBSetup(suite.state.DB, nil) +} + +func (suite *StatusFilterTestSuite) TearDownTest() { + testrig.StandardDBTeardown(suite.state.DB) + testrig.StopWorkers(&suite.state) +} + +func (suite *StatusFilterTestSuite) TestHideFilteredStatus() { + filtered, hide, err := suite.testFilterStatus(gtsmodel.FilterActionHide, false) + suite.NoError(err) + suite.True(hide) + suite.Empty(filtered) +} + +func (suite *StatusFilterTestSuite) TestWarnFilteredStatus() { + filtered, hide, err := suite.testFilterStatus(gtsmodel.FilterActionWarn, false) + suite.NoError(err) + suite.False(hide) + suite.NotEmpty(filtered) +} + +func (suite *StatusFilterTestSuite) TestHideFilteredBoost() { + filtered, hide, err := suite.testFilterStatus(gtsmodel.FilterActionHide, true) + suite.NoError(err) + suite.True(hide) + suite.Empty(filtered) +} + +func (suite *StatusFilterTestSuite) TestWarnFilteredBoost() { + filtered, hide, err := suite.testFilterStatus(gtsmodel.FilterActionWarn, true) + suite.NoError(err) + suite.False(hide) + suite.NotEmpty(filtered) +} + +func (suite *StatusFilterTestSuite) TestHashtagWholewordStatusFiltered() { + suite.testFilteredStatusWithHashtag(true, false) +} + +func (suite *StatusFilterTestSuite) TestHashtagWholewordBoostFiltered() { + suite.testFilteredStatusWithHashtag(true, true) +} + +func (suite *StatusFilterTestSuite) TestHashtagAnywhereStatusFiltered() { + suite.testFilteredStatusWithHashtag(false, false) +} + +func (suite *StatusFilterTestSuite) TestHashtagAnywhereBoostFiltered() { + suite.testFilteredStatusWithHashtag(false, true) +} + +func (suite *StatusFilterTestSuite) testFilterStatus(action gtsmodel.FilterAction, boost bool) ([]apimodel.FilterResult, bool, error) { + ctx := suite.T().Context() + + status := suite.testStatuses["admin_account_status_1"] + status.Content += " fnord" + status.Text += " fnord" + + if boost { + // Modify a fixture boost into a boost of the above status. + boost := suite.testStatuses["admin_account_status_4"] + boost.BoostOf = status + boost.BoostOfID = status.ID + status = boost + } + + requester := suite.testAccounts["local_account_1"] + + filter := suite.testFilters["local_account_1_filter_1"] + filter.Action = action + + err := suite.state.DB.UpdateFilter(ctx, filter, "action") + suite.NoError(err) + + return suite.filter.StatusFilterResultsInContext(ctx, + requester, + status, + gtsmodel.FilterContextHome, + ) +} + +func (suite *StatusFilterTestSuite) testFilteredStatusWithHashtag(wholeword, boost bool) { + ctx := suite.T().Context() + + status := new(gtsmodel.Status) + *status = *suite.testStatuses["admin_account_status_1"] + status.Content = `

doggo doggin' it

#dogsofmastodon

` + + if boost { + boost, err := suite.converter.StatusToBoost( + suite.T().Context(), + status, + suite.testAccounts["admin_account"], + "", + ) + suite.NoError(err) + status = boost + } + + var err error + + requester := suite.testAccounts["local_account_1"] + + filter := >smodel.Filter{ + ID: id.NewULID(), + Title: id.NewULID(), + AccountID: requester.ID, + Action: gtsmodel.FilterActionWarn, + Contexts: gtsmodel.FilterContexts(gtsmodel.FilterContextHome), + } + + filterKeyword := >smodel.FilterKeyword{ + ID: id.NewULID(), + FilterID: filter.ID, + Keyword: "#dogsofmastodon", + WholeWord: &wholeword, + } + + filter.KeywordIDs = []string{filterKeyword.ID} + + err = suite.state.DB.PutFilterKeyword(ctx, filterKeyword) + suite.NoError(err) + + err = suite.state.DB.PutFilter(ctx, filter) + suite.NoError(err) + + filtered, hide, err := suite.filter.StatusFilterResultsInContext(ctx, + requester, + status, + gtsmodel.FilterContextHome, + ) + suite.NoError(err) + suite.False(hide) + suite.NotEmpty(filtered) +} + +func TestStatusFilterTestSuite(t *testing.T) { + suite.Run(t, new(StatusFilterTestSuite)) +} diff --git a/internal/processing/account/account.go b/internal/processing/account/account.go index 1e8be372f..e94b7e844 100644 --- a/internal/processing/account/account.go +++ b/internal/processing/account/account.go @@ -19,6 +19,7 @@ package account import ( "code.superseriousbusiness.org/gotosocial/internal/federation" + "code.superseriousbusiness.org/gotosocial/internal/filter/status" "code.superseriousbusiness.org/gotosocial/internal/filter/visibility" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/media" @@ -39,6 +40,7 @@ type Processor struct { converter *typeutils.Converter mediaManager *media.Manager visFilter *visibility.Filter + statusFilter *status.Filter formatter *text.Formatter federator *federation.Federator parseMention gtsmodel.ParseMentionFunc @@ -53,6 +55,7 @@ func New( mediaManager *media.Manager, federator *federation.Federator, visFilter *visibility.Filter, + statusFilter *status.Filter, parseMention gtsmodel.ParseMentionFunc, ) Processor { return Processor{ @@ -61,6 +64,7 @@ func New( converter: converter, mediaManager: mediaManager, visFilter: visFilter, + statusFilter: statusFilter, formatter: text.NewFormatter(state.DB), federator: federator, parseMention: parseMention, diff --git a/internal/processing/account/account_test.go b/internal/processing/account/account_test.go index d4fb6ddfb..b322ee771 100644 --- a/internal/processing/account/account_test.go +++ b/internal/processing/account/account_test.go @@ -26,6 +26,7 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/email" "code.superseriousbusiness.org/gotosocial/internal/federation" "code.superseriousbusiness.org/gotosocial/internal/filter/mutes" + "code.superseriousbusiness.org/gotosocial/internal/filter/status" "code.superseriousbusiness.org/gotosocial/internal/filter/visibility" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/media" @@ -107,8 +108,9 @@ func (suite *AccountStandardTestSuite) SetupTest() { visFilter := visibility.NewFilter(&suite.state) mutesFilter := mutes.NewFilter(&suite.state) - common := common.New(&suite.state, suite.mediaManager, suite.tc, suite.federator, visFilter, mutesFilter) - suite.accountProcessor = account.New(&common, &suite.state, suite.tc, suite.mediaManager, suite.federator, visFilter, processing.GetParseMentionFunc(&suite.state, suite.federator)) + statusFilter := status.NewFilter(&suite.state) + common := common.New(&suite.state, suite.mediaManager, suite.tc, suite.federator, visFilter, mutesFilter, statusFilter) + suite.accountProcessor = account.New(&common, &suite.state, suite.tc, suite.mediaManager, suite.federator, visFilter, statusFilter, processing.GetParseMentionFunc(&suite.state, suite.federator)) testrig.StandardDBSetup(suite.db, nil) testrig.StandardStorageSetup(suite.storage, "../../../testrig/media") } diff --git a/internal/processing/account/bookmarks.go b/internal/processing/account/bookmarks.go index 7a0ff9915..468c6ad62 100644 --- a/internal/processing/account/bookmarks.go +++ b/internal/processing/account/bookmarks.go @@ -74,7 +74,7 @@ func (p *Processor) BookmarksGet(ctx context.Context, requestingAccount *gtsmode } // Convert the status. - item, err := p.converter.StatusToAPIStatus(ctx, status, requestingAccount, gtsmodel.FilterContextNone) + item, err := p.converter.StatusToAPIStatus(ctx, status, requestingAccount) if err != nil { log.Errorf(ctx, "error converting bookmarked status to api: %s", err) continue diff --git a/internal/processing/account/statuses.go b/internal/processing/account/statuses.go index f0024d489..e55c1e81c 100644 --- a/internal/processing/account/statuses.go +++ b/internal/processing/account/statuses.go @@ -96,13 +96,33 @@ func (p *Processor) StatusesGet( return nil, gtserror.NewErrorInternalError(err) } - for _, s := range filtered { + for _, status := range filtered { + // ... + filtered, hide, err := p.statusFilter.StatusFilterResultsInContext(ctx, + requestingAccount, + status, + gtsmodel.FilterContextAccount, + ) + if err != nil { + log.Errorf(ctx, "error filtering status: %v", err) + continue + } + + if hide { + // Don't show. + continue + } + // Convert filtered statuses to API statuses. - item, err := p.converter.StatusToAPIStatus(ctx, s, requestingAccount, gtsmodel.FilterContextAccount) + item, err := p.converter.StatusToAPIStatus(ctx, status, requestingAccount) if err != nil { log.Errorf(ctx, "error convering to api status: %v", err) continue } + + // Set any filter results. + item.Filtered = filtered + items = append(items, item) } diff --git a/internal/processing/admin/admin_test.go b/internal/processing/admin/admin_test.go index 8f2eb23f2..857afcae1 100644 --- a/internal/processing/admin/admin_test.go +++ b/internal/processing/admin/admin_test.go @@ -25,6 +25,7 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/federation" "code.superseriousbusiness.org/gotosocial/internal/filter/interaction" "code.superseriousbusiness.org/gotosocial/internal/filter/mutes" + "code.superseriousbusiness.org/gotosocial/internal/filter/status" "code.superseriousbusiness.org/gotosocial/internal/filter/visibility" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/media" @@ -116,6 +117,7 @@ func (suite *AdminStandardTestSuite) SetupTest() { visibility.NewFilter(&suite.state), mutes.NewFilter(&suite.state), interaction.NewFilter(&suite.state), + status.NewFilter(&suite.state), ) testrig.StartWorkers(&suite.state, suite.processor.Workers()) diff --git a/internal/processing/common/common.go b/internal/processing/common/common.go index bebbdffea..2b3adb9a0 100644 --- a/internal/processing/common/common.go +++ b/internal/processing/common/common.go @@ -20,6 +20,7 @@ package common import ( "code.superseriousbusiness.org/gotosocial/internal/federation" "code.superseriousbusiness.org/gotosocial/internal/filter/mutes" + "code.superseriousbusiness.org/gotosocial/internal/filter/status" "code.superseriousbusiness.org/gotosocial/internal/filter/visibility" "code.superseriousbusiness.org/gotosocial/internal/media" "code.superseriousbusiness.org/gotosocial/internal/state" @@ -30,12 +31,13 @@ import ( // common to multiple logical domains of the // processing subsection of the codebase. type Processor struct { - state *state.State - media *media.Manager - converter *typeutils.Converter - federator *federation.Federator - visFilter *visibility.Filter - muteFilter *mutes.Filter + state *state.State + media *media.Manager + converter *typeutils.Converter + federator *federation.Federator + visFilter *visibility.Filter + muteFilter *mutes.Filter + statusFilter *status.Filter } // New returns a new Processor instance. @@ -46,13 +48,15 @@ func New( federator *federation.Federator, visFilter *visibility.Filter, muteFilter *mutes.Filter, + statusFilter *status.Filter, ) Processor { return Processor{ - state: state, - media: media, - converter: converter, - federator: federator, - visFilter: visFilter, - muteFilter: muteFilter, + state: state, + media: media, + converter: converter, + federator: federator, + visFilter: visFilter, + muteFilter: muteFilter, + statusFilter: statusFilter, } } diff --git a/internal/processing/common/status.go b/internal/processing/common/status.go index f5f230e98..2bcf89a02 100644 --- a/internal/processing/common/status.go +++ b/internal/processing/common/status.go @@ -27,7 +27,6 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/gtserror" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/log" - "code.superseriousbusiness.org/gotosocial/internal/typeutils" ) // GetOwnStatus fetches the given status with ID, @@ -213,7 +212,6 @@ func (p *Processor) GetAPIStatus( apiStatus, err := p.converter.StatusToAPIStatus(ctx, target, requester, - gtsmodel.FilterContextNone, ) if err != nil { err := gtserror.Newf("error converting: %w", err) @@ -271,22 +269,33 @@ func (p *Processor) GetVisibleAPIStatuses( continue } + // Check whether status is filtered in context by requesting account. + filtered, hide, err := p.statusFilter.StatusFilterResultsInContext(ctx, + requester, + status, + filterCtx, + ) + if err != nil { + l.Errorf("error filtering: %v", err) + continue + } + + if hide { + continue + } + // Convert to API status, taking mute / filter into account. apiStatus, err := p.converter.StatusToAPIStatus(ctx, status, requester, - filterCtx, ) - if err != nil && !errors.Is(err, typeutils.ErrHideStatus) { + if err != nil { l.Errorf("error converting: %v", err) continue } - if apiStatus == nil { - // Status was - // filtered out. - continue - } + // Set filter results on status. + apiStatus.Filtered = filtered // Append converted status to return slice. apiStatuses = append(apiStatuses, *apiStatus) diff --git a/internal/processing/conversations/conversations.go b/internal/processing/conversations/conversations.go index 70fafa437..b80ba659a 100644 --- a/internal/processing/conversations/conversations.go +++ b/internal/processing/conversations/conversations.go @@ -23,6 +23,7 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/db" "code.superseriousbusiness.org/gotosocial/internal/filter/mutes" + "code.superseriousbusiness.org/gotosocial/internal/filter/status" "code.superseriousbusiness.org/gotosocial/internal/filter/visibility" "code.superseriousbusiness.org/gotosocial/internal/gtserror" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" @@ -31,10 +32,11 @@ import ( ) type Processor struct { - state *state.State - converter *typeutils.Converter - visFilter *visibility.Filter - muteFilter *mutes.Filter + state *state.State + converter *typeutils.Converter + visFilter *visibility.Filter + muteFilter *mutes.Filter + statusFilter *status.Filter } func New( @@ -42,12 +44,14 @@ func New( converter *typeutils.Converter, visFilter *visibility.Filter, muteFilter *mutes.Filter, + statusFilter *status.Filter, ) Processor { return Processor{ - state: state, - converter: converter, - visFilter: visFilter, - muteFilter: muteFilter, + state: state, + converter: converter, + visFilter: visFilter, + muteFilter: muteFilter, + statusFilter: statusFilter, } } @@ -95,21 +99,3 @@ func (p *Processor) getConversationOwnedBy( return conversation, nil } - -// getFiltersAndMutes gets the given account's filters and compiled mute list. -func (p *Processor) getFilters( - ctx context.Context, - requestingAccount *gtsmodel.Account, -) ([]*gtsmodel.Filter, gtserror.WithCode) { - filters, err := p.state.DB.GetFiltersByAccountID(ctx, requestingAccount.ID) - if err != nil { - return nil, gtserror.NewErrorInternalError( - gtserror.Newf( - "DB error getting filters for account %s: %w", - requestingAccount.ID, - err, - ), - ) - } - return filters, nil -} diff --git a/internal/processing/conversations/conversations_test.go b/internal/processing/conversations/conversations_test.go index 383938564..407623964 100644 --- a/internal/processing/conversations/conversations_test.go +++ b/internal/processing/conversations/conversations_test.go @@ -28,6 +28,7 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/email" "code.superseriousbusiness.org/gotosocial/internal/federation" "code.superseriousbusiness.org/gotosocial/internal/filter/mutes" + "code.superseriousbusiness.org/gotosocial/internal/filter/status" "code.superseriousbusiness.org/gotosocial/internal/filter/visibility" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/log" @@ -118,7 +119,7 @@ func (suite *ConversationsTestSuite) SetupTest() { suite.sentEmails = make(map[string]string) suite.emailSender = testrig.NewEmailSender("../../../web/template/", suite.sentEmails) - suite.conversationsProcessor = conversations.New(&suite.state, suite.tc, suite.visFilter, suite.muteFilter) + suite.conversationsProcessor = conversations.New(&suite.state, suite.tc, suite.visFilter, suite.muteFilter, status.NewFilter(&suite.state)) testrig.StandardDBSetup(suite.db, nil) testrig.StandardStorageSetup(suite.storage, "../../../testrig/media") diff --git a/internal/processing/conversations/get.go b/internal/processing/conversations/get.go index 5324466c9..cdc0756c3 100644 --- a/internal/processing/conversations/get.go +++ b/internal/processing/conversations/get.go @@ -64,17 +64,26 @@ func (p *Processor) GetAll( items := make([]interface{}, 0, count) - filters, errWithCode := p.getFilters(ctx, requestingAccount) - if errWithCode != nil { - return nil, errWithCode - } - for _, conversation := range conversations { + // Check whether status if filtered by local participant in context. + filtered, hide, err := p.statusFilter.StatusFilterResultsInContext(ctx, + requestingAccount, + conversation.LastStatus, + gtsmodel.FilterContextNotifications, + ) + if err != nil { + log.Errorf(ctx, "error filtering status: %v", err) + continue + } + + if hide { + continue + } + // Convert conversation to frontend API model. apiConversation, err := p.converter.ConversationToAPIConversation(ctx, conversation, requestingAccount, - filters, ) if err != nil { log.Errorf(ctx, @@ -85,6 +94,9 @@ func (p *Processor) GetAll( continue } + // Set filter results on attached status model. + apiConversation.LastStatus.Filtered = filtered + // Append conversation to return items. items = append(items, apiConversation) } diff --git a/internal/processing/conversations/read.go b/internal/processing/conversations/read.go index 4d16a4eeb..c7e4f1acd 100644 --- a/internal/processing/conversations/read.go +++ b/internal/processing/conversations/read.go @@ -23,6 +23,7 @@ import ( apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model" "code.superseriousbusiness.org/gotosocial/internal/gtserror" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" + "code.superseriousbusiness.org/gotosocial/internal/log" "code.superseriousbusiness.org/gotosocial/internal/util" ) @@ -44,20 +45,27 @@ func (p *Processor) Read( return nil, gtserror.NewErrorInternalError(err) } - filters, errWithCode := p.getFilters(ctx, requestingAccount) - if errWithCode != nil { - return nil, errWithCode + // Check whether status if filtered by local participant in context. + filtered, _, err := p.statusFilter.StatusFilterResultsInContext(ctx, + requestingAccount, + conversation.LastStatus, + gtsmodel.FilterContextNotifications, + ) + if err != nil { + log.Errorf(ctx, "error filtering status: %v", err) } apiConversation, err := p.converter.ConversationToAPIConversation(ctx, conversation, requestingAccount, - filters, ) if err != nil { err = gtserror.Newf("error converting conversation %s to API representation: %w", id, err) return nil, gtserror.NewErrorInternalError(err) } + // Set filter results on attached status model. + apiConversation.LastStatus.Filtered = filtered + return apiConversation, nil } diff --git a/internal/processing/conversations/update.go b/internal/processing/conversations/update.go index cf81d6906..21f1cf915 100644 --- a/internal/processing/conversations/update.go +++ b/internal/processing/conversations/update.go @@ -27,7 +27,6 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/id" "code.superseriousbusiness.org/gotosocial/internal/log" - "code.superseriousbusiness.org/gotosocial/internal/typeutils" "code.superseriousbusiness.org/gotosocial/internal/util" ) @@ -158,26 +157,6 @@ func (p *Processor) UpdateConversationsForStatus(ctx context.Context, status *gt continue } - // Convert the conversation to API representation. - apiConversation, err := p.converter.ConversationToAPIConversation(ctx, - conversation, - localAccount, - nil, - ) - if err != nil { - // If the conversation's last status matched a hide filter, skip it. - // If there was another kind of error, log that and skip it anyway. - if !errors.Is(err, typeutils.ErrHideStatus) { - log.Errorf(ctx, - "error converting conversation %s to API representation for account %s: %v", - status.ID, - localAccount.ID, - err, - ) - } - continue - } - // If status was authored by this participant, // don't bother notifying, they already know! if status.AccountID == localAccount.ID { @@ -198,6 +177,38 @@ func (p *Processor) UpdateConversationsForStatus(ctx context.Context, status *gt continue } + // Check whether status if filtered by local participant in context. + filtered, hide, err := p.statusFilter.StatusFilterResultsInContext(ctx, + localAccount, + status, + gtsmodel.FilterContextNotifications, + ) + if err != nil { + log.Errorf(ctx, "error filtering status: %v", err) + continue + } + + if hide { + continue + } + + // Convert the conversation to API representation. + apiConversation, err := p.converter.ConversationToAPIConversation(ctx, + conversation, + localAccount, + ) + if err != nil { + log.Errorf(ctx, "error converting conversation %s to API representation for account %s: %v", + status.ID, + localAccount.ID, + err, + ) + continue + } + + // Set filter results on attached status model. + apiConversation.LastStatus.Filtered = filtered + // Generate a notification, notifications = append(notifications, ConversationNotification{ AccountID: localAccount.ID, diff --git a/internal/processing/media/media_test.go b/internal/processing/media/media_test.go index f2462a972..01506cc6f 100644 --- a/internal/processing/media/media_test.go +++ b/internal/processing/media/media_test.go @@ -21,6 +21,7 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/admin" "code.superseriousbusiness.org/gotosocial/internal/db" "code.superseriousbusiness.org/gotosocial/internal/filter/mutes" + "code.superseriousbusiness.org/gotosocial/internal/filter/status" "code.superseriousbusiness.org/gotosocial/internal/filter/visibility" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/media" @@ -85,7 +86,8 @@ func (suite *MediaStandardTestSuite) SetupTest() { federator := testrig.NewTestFederator(&suite.state, suite.transportController, suite.mediaManager) visFilter := visibility.NewFilter(&suite.state) muteFilter := mutes.NewFilter(&suite.state) - common := common.New(&suite.state, suite.mediaManager, suite.tc, federator, visFilter, muteFilter) + statusFilter := status.NewFilter(&suite.state) + common := common.New(&suite.state, suite.mediaManager, suite.tc, federator, visFilter, muteFilter, statusFilter) suite.mediaProcessor = mediaprocessing.New(&common, &suite.state, suite.tc, federator, suite.mediaManager, suite.transportController) testrig.StandardDBSetup(suite.db, nil) diff --git a/internal/processing/polls/poll_test.go b/internal/processing/polls/poll_test.go index 848c3f169..2fd46c5ee 100644 --- a/internal/processing/polls/poll_test.go +++ b/internal/processing/polls/poll_test.go @@ -25,6 +25,7 @@ import ( apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model" "code.superseriousbusiness.org/gotosocial/internal/filter/mutes" + "code.superseriousbusiness.org/gotosocial/internal/filter/status" "code.superseriousbusiness.org/gotosocial/internal/filter/visibility" "code.superseriousbusiness.org/gotosocial/internal/gtserror" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" @@ -60,7 +61,8 @@ func (suite *PollTestSuite) SetupTest() { federator := testrig.NewTestFederator(&suite.state, controller, mediaMgr) suite.visFilter = visibility.NewFilter(&suite.state) suite.muteFilter = mutes.NewFilter(&suite.state) - common := common.New(&suite.state, mediaMgr, converter, federator, suite.visFilter, suite.muteFilter) + statusFilter := status.NewFilter(&suite.state) + common := common.New(&suite.state, mediaMgr, converter, federator, suite.visFilter, suite.muteFilter, statusFilter) suite.polls = polls.New(&common, &suite.state, converter) } diff --git a/internal/processing/processor.go b/internal/processing/processor.go index c35c807e0..a5cea5da4 100644 --- a/internal/processing/processor.go +++ b/internal/processing/processor.go @@ -23,6 +23,7 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/federation" "code.superseriousbusiness.org/gotosocial/internal/filter/interaction" "code.superseriousbusiness.org/gotosocial/internal/filter/mutes" + statusfilter "code.superseriousbusiness.org/gotosocial/internal/filter/status" "code.superseriousbusiness.org/gotosocial/internal/filter/visibility" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" mm "code.superseriousbusiness.org/gotosocial/internal/media" @@ -193,7 +194,8 @@ func (p *Processor) Workers() *workers.Processor { return &p.workers } -// NewProcessor returns a new Processor. +// NewProcessor returns +// a new Processor. func NewProcessor( cleaner *cleaner.Cleaner, subscriptions *subscriptions.Subscriptions, @@ -207,6 +209,7 @@ func NewProcessor( visFilter *visibility.Filter, muteFilter *mutes.Filter, intFilter *interaction.Filter, + statusFilter *statusfilter.Filter, ) *Processor { parseMentionFunc := GetParseMentionFunc(state, federator) processor := &Processor{ @@ -221,18 +224,18 @@ func NewProcessor( // // Start with sub processors that will // be required by the workers processor. - common := common.New(state, mediaManager, converter, federator, visFilter, muteFilter) - processor.account = account.New(&common, state, converter, mediaManager, federator, visFilter, parseMentionFunc) + common := common.New(state, mediaManager, converter, federator, visFilter, muteFilter, statusFilter) + processor.account = account.New(&common, state, converter, mediaManager, federator, visFilter, statusFilter, parseMentionFunc) processor.media = media.New(&common, state, converter, federator, mediaManager, federator.TransportController()) processor.stream = stream.New(state, oauthServer) filterCommon := filterCommon.New(state, &processor.stream) // Instantiate the rest of the sub // processors + pin them to this struct. - processor.account = account.New(&common, state, converter, mediaManager, federator, visFilter, parseMentionFunc) + processor.account = account.New(&common, state, converter, mediaManager, federator, visFilter, statusFilter, parseMentionFunc) processor.admin = admin.New(&common, state, cleaner, subscriptions, federator, converter, mediaManager, federator.TransportController(), emailSender) processor.application = application.New(state, converter) - processor.conversations = conversations.New(state, converter, visFilter, muteFilter) + processor.conversations = conversations.New(state, converter, visFilter, muteFilter, statusFilter) processor.fedi = fedi.New(state, &common, converter, federator, visFilter) processor.filtersv1 = filtersv1.New(state, converter, filterCommon) processor.filtersv2 = filtersv2.New(state, converter, filterCommon) @@ -243,7 +246,7 @@ func NewProcessor( processor.push = push.New(state, converter) processor.report = report.New(state, converter) processor.tags = tags.New(state, converter) - processor.timeline = timeline.New(state, converter, visFilter, muteFilter) + processor.timeline = timeline.New(state, converter, visFilter, muteFilter, statusFilter) processor.search = search.New(state, federator, converter, visFilter) processor.status = status.New(state, &common, &processor.polls, &processor.interactionRequests, federator, converter, visFilter, intFilter, parseMentionFunc) processor.user = user.New(state, converter, oauthServer, emailSender) @@ -261,6 +264,7 @@ func NewProcessor( converter, visFilter, muteFilter, + statusFilter, emailSender, webPushSender, &processor.account, diff --git a/internal/processing/processor_test.go b/internal/processing/processor_test.go index 847de29cf..3c564b929 100644 --- a/internal/processing/processor_test.go +++ b/internal/processing/processor_test.go @@ -28,6 +28,7 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/federation" "code.superseriousbusiness.org/gotosocial/internal/filter/interaction" "code.superseriousbusiness.org/gotosocial/internal/filter/mutes" + "code.superseriousbusiness.org/gotosocial/internal/filter/status" "code.superseriousbusiness.org/gotosocial/internal/filter/visibility" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/media" @@ -133,6 +134,7 @@ func (suite *ProcessingStandardTestSuite) SetupTest() { visibility.NewFilter(&suite.state), mutes.NewFilter(&suite.state), interaction.NewFilter(&suite.state), + status.NewFilter(&suite.state), ) testrig.StartWorkers(&suite.state, suite.processor.Workers()) diff --git a/internal/processing/search/util.go b/internal/processing/search/util.go index fc105940f..441f3f946 100644 --- a/internal/processing/search/util.go +++ b/internal/processing/search/util.go @@ -113,7 +113,7 @@ func (p *Processor) packageStatuses( continue } - apiStatus, err := p.converter.StatusToAPIStatus(ctx, status, requestingAccount, gtsmodel.FilterContextNone) + apiStatus, err := p.converter.StatusToAPIStatus(ctx, status, requestingAccount) if err != nil { log.Debugf(ctx, "skipping status %s because it couldn't be converted to its api representation: %s", status.ID, err) continue diff --git a/internal/processing/status/status_test.go b/internal/processing/status/status_test.go index 091d9716b..d709d435f 100644 --- a/internal/processing/status/status_test.go +++ b/internal/processing/status/status_test.go @@ -23,6 +23,7 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/federation" "code.superseriousbusiness.org/gotosocial/internal/filter/interaction" "code.superseriousbusiness.org/gotosocial/internal/filter/mutes" + statusfilter "code.superseriousbusiness.org/gotosocial/internal/filter/status" "code.superseriousbusiness.org/gotosocial/internal/filter/visibility" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/media" @@ -95,8 +96,9 @@ func (suite *StatusStandardTestSuite) SetupTest() { visFilter := visibility.NewFilter(&suite.state) muteFilter := mutes.NewFilter(&suite.state) intFilter := interaction.NewFilter(&suite.state) + statusFilter := statusfilter.NewFilter(&suite.state) - common := common.New(&suite.state, suite.mediaManager, suite.typeConverter, suite.federator, visFilter, muteFilter) + common := common.New(&suite.state, suite.mediaManager, suite.typeConverter, suite.federator, visFilter, muteFilter, statusFilter) polls := polls.New(&common, &suite.state, suite.typeConverter) intReqs := interactionrequests.New(&common, &suite.state, suite.typeConverter) diff --git a/internal/processing/stream/statusupdate_test.go b/internal/processing/stream/statusupdate_test.go index a3ec0415e..a82348c31 100644 --- a/internal/processing/stream/statusupdate_test.go +++ b/internal/processing/stream/statusupdate_test.go @@ -22,7 +22,6 @@ import ( "encoding/json" "testing" - "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 +38,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, gtsmodel.FilterContextNotifications) + apiStatus, err := typeutils.NewConverter(&suite.state).StatusToAPIStatus(suite.T().Context(), editedStatus, account) suite.NoError(err) suite.streamProcessor.StatusUpdate(suite.T().Context(), account, apiStatus, stream.TimelineHome) diff --git a/internal/processing/timeline/faved.go b/internal/processing/timeline/faved.go index 65b23c702..9218af9c8 100644 --- a/internal/processing/timeline/faved.go +++ b/internal/processing/timeline/faved.go @@ -26,7 +26,6 @@ import ( apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util" "code.superseriousbusiness.org/gotosocial/internal/db" "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 +55,7 @@ func (p *Processor) FavedTimelineGet(ctx context.Context, authed *apiutil.Auth, continue } - apiStatus, err := p.converter.StatusToAPIStatus(ctx, s, authed.Account, gtsmodel.FilterContextNone) + apiStatus, err := p.converter.StatusToAPIStatus(ctx, s, authed.Account) if err != nil { log.Errorf(ctx, "error convering to api status: %v", err) continue diff --git a/internal/processing/timeline/notification.go b/internal/processing/timeline/notification.go index 784b2b824..b17dda8a7 100644 --- a/internal/processing/timeline/notification.go +++ b/internal/processing/timeline/notification.go @@ -31,7 +31,6 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/log" "code.superseriousbusiness.org/gotosocial/internal/paging" - "code.superseriousbusiness.org/gotosocial/internal/typeutils" "code.superseriousbusiness.org/gotosocial/internal/util" ) @@ -93,8 +92,12 @@ func (p *Processor) NotificationsGet( continue } + var filtered []apimodel.FilterResult + if n.Status != nil { - // A status is attached, check whether status muted. + var hide bool + + // Check whether notification status is muted by requester. muted, err = p.muteFilter.StatusNotificationsMuted(ctx, requester, n.Status, @@ -107,16 +110,34 @@ func (p *Processor) NotificationsGet( if muted { continue } + + // Check whether notification status is filtered by requester in notifs. + filtered, hide, err = p.statusFilter.StatusFilterResultsInContext(ctx, + requester, + n.Status, + gtsmodel.FilterContextNotifications, + ) + if err != nil { + log.Errorf(ctx, "error checking status filtering: %v", err) + continue + } + + if hide { + continue + } } - item, err := p.converter.NotificationToAPINotification(ctx, n, true) + item, err := p.converter.NotificationToAPINotification(ctx, n) if err != nil { - if !errors.Is(err, typeutils.ErrHideStatus) { - log.Debugf(ctx, "skipping notification %s because it couldn't be converted to its api representation: %s", n.ID, err) - } continue } + if item.Status != nil { + // Set filter results on status, + // in case any were set above. + item.Status.Filtered = filtered + } + items = append(items, item) } @@ -154,7 +175,7 @@ func (p *Processor) NotificationGet(ctx context.Context, account *gtsmodel.Accou // or mute checking for a notification directly // fetched by ID. only from timelines etc. - apiNotif, err := p.converter.NotificationToAPINotification(ctx, notif, false) + apiNotif, err := p.converter.NotificationToAPINotification(ctx, notif) if err != nil { err := gtserror.Newf("error converting to api model: %w", err) return nil, gtserror.WrapWithCode(http.StatusInternalServerError, err) diff --git a/internal/processing/timeline/timeline.go b/internal/processing/timeline/timeline.go index 64d33e430..06580b3c7 100644 --- a/internal/processing/timeline/timeline.go +++ b/internal/processing/timeline/timeline.go @@ -19,13 +19,13 @@ package timeline import ( "context" - "errors" "net/http" "net/url" apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model" timelinepkg "code.superseriousbusiness.org/gotosocial/internal/cache/timeline" "code.superseriousbusiness.org/gotosocial/internal/filter/mutes" + "code.superseriousbusiness.org/gotosocial/internal/filter/status" "code.superseriousbusiness.org/gotosocial/internal/filter/visibility" "code.superseriousbusiness.org/gotosocial/internal/gtserror" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" @@ -46,18 +46,26 @@ var ( ) type Processor struct { - state *state.State - converter *typeutils.Converter - visFilter *visibility.Filter - muteFilter *mutes.Filter + state *state.State + converter *typeutils.Converter + visFilter *visibility.Filter + muteFilter *mutes.Filter + statusFilter *status.Filter } -func New(state *state.State, converter *typeutils.Converter, visFilter *visibility.Filter, muteFilter *mutes.Filter) Processor { +func New( + state *state.State, + converter *typeutils.Converter, + visFilter *visibility.Filter, + muteFilter *mutes.Filter, + statusFilter *status.Filter, +) Processor { return Processor{ - state: state, - converter: converter, - visFilter: visFilter, - muteFilter: muteFilter, + state: state, + converter: converter, + visFilter: visFilter, + muteFilter: muteFilter, + statusFilter: statusFilter, } } @@ -116,15 +124,30 @@ func (p *Processor) getStatusTimeline( return nil, nil } + // Check whether this status is filtered by requester in this context. + filters, hide, err := p.statusFilter.StatusFilterResultsInContext(ctx, + requester, + status, + filterCtx, + ) + if err != nil { + return nil, err + } else if hide { + return nil, nil + } + // Finally, pass status to get converted to API model. apiStatus, err := p.converter.StatusToAPIStatus(ctx, status, requester, - filterCtx, ) - if err != nil && !errors.Is(err, typeutils.ErrHideStatus) { + if err != nil { return nil, err } + + // Set any filters on status. + apiStatus.Filtered = filters + return apiStatus, nil }, ) diff --git a/internal/processing/timeline/timeline_test.go b/internal/processing/timeline/timeline_test.go index 01197b767..ce7817df6 100644 --- a/internal/processing/timeline/timeline_test.go +++ b/internal/processing/timeline/timeline_test.go @@ -21,6 +21,7 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/admin" "code.superseriousbusiness.org/gotosocial/internal/db" "code.superseriousbusiness.org/gotosocial/internal/filter/mutes" + "code.superseriousbusiness.org/gotosocial/internal/filter/status" "code.superseriousbusiness.org/gotosocial/internal/filter/visibility" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/processing/timeline" @@ -64,6 +65,7 @@ func (suite *TimelineStandardTestSuite) SetupTest() { typeutils.NewConverter(&suite.state), visibility.NewFilter(&suite.state), mutes.NewFilter(&suite.state), + status.NewFilter(&suite.state), ) testrig.StandardDBSetup(suite.db, suite.testAccounts) diff --git a/internal/processing/workers/fromclientapi_test.go b/internal/processing/workers/fromclientapi_test.go index 4453095fd..7da34ff42 100644 --- a/internal/processing/workers/fromclientapi_test.go +++ b/internal/processing/workers/fromclientapi_test.go @@ -212,7 +212,6 @@ func (suite *FromClientAPITestSuite) statusJSON( ctx, status, requestingAccount, - gtsmodel.FilterContextNone, ) if err != nil { suite.FailNow(err.Error()) @@ -236,7 +235,6 @@ func (suite *FromClientAPITestSuite) conversationJSON( ctx, conversation, requestingAccount, - nil, ) if err != nil { suite.FailNow(err.Error()) @@ -344,7 +342,7 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithNotification() { suite.FailNow("timed out waiting for new status notification") } - apiNotif, err := testStructs.TypeConverter.NotificationToAPINotification(ctx, notif, false) + apiNotif, err := testStructs.TypeConverter.NotificationToAPINotification(ctx, notif) if err != nil { suite.FailNow(err.Error()) } @@ -2031,7 +2029,7 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithAuthorOnExclusiv suite.FailNow("timed out waiting for new status notification") } - apiNotif, err := testStructs.TypeConverter.NotificationToAPINotification(ctx, notif, false) + apiNotif, err := testStructs.TypeConverter.NotificationToAPINotification(ctx, notif) if err != nil { suite.FailNow(err.Error()) } @@ -2216,7 +2214,7 @@ func (suite *FromClientAPITestSuite) TestProcessUpdateStatusInteractedWith() { suite.FailNow("timed out waiting for edited status notification") } - apiNotif, err := testStructs.TypeConverter.NotificationToAPINotification(ctx, notif, false) + apiNotif, err := testStructs.TypeConverter.NotificationToAPINotification(ctx, notif) if err != nil { suite.FailNow(err.Error()) } diff --git a/internal/processing/workers/surface.go b/internal/processing/workers/surface.go index 69758692f..e0e441479 100644 --- a/internal/processing/workers/surface.go +++ b/internal/processing/workers/surface.go @@ -20,6 +20,7 @@ package workers import ( "code.superseriousbusiness.org/gotosocial/internal/email" "code.superseriousbusiness.org/gotosocial/internal/filter/mutes" + "code.superseriousbusiness.org/gotosocial/internal/filter/status" "code.superseriousbusiness.org/gotosocial/internal/filter/visibility" "code.superseriousbusiness.org/gotosocial/internal/processing/conversations" "code.superseriousbusiness.org/gotosocial/internal/processing/stream" @@ -40,6 +41,7 @@ type Surface struct { Stream *stream.Processor VisFilter *visibility.Filter MuteFilter *mutes.Filter + StatusFilter *status.Filter EmailSender email.Sender WebPushSender webpush.Sender Conversations *conversations.Processor diff --git a/internal/processing/workers/surfacenotify.go b/internal/processing/workers/surfacenotify.go index de7e3d95a..15ad79b26 100644 --- a/internal/processing/workers/surfacenotify.go +++ b/internal/processing/workers/surfacenotify.go @@ -22,12 +22,12 @@ import ( "errors" "strings" + 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" "code.superseriousbusiness.org/gotosocial/internal/util" "code.superseriousbusiness.org/gotosocial/internal/util/xslices" ) @@ -727,6 +727,8 @@ func (s *Surface) Notify( return nil } + var filtered []apimodel.FilterResult + if status != nil { // Check whether status is muted by the target account. muted, err := s.MuteFilter.StatusNotificationsMuted(ctx, @@ -741,17 +743,35 @@ func (s *Surface) Notify( // Don't notify. return nil } + + var hide bool + + // Check whether notification status is filtered by requester in notifs. + filtered, hide, err = s.StatusFilter.StatusFilterResultsInContext(ctx, + targetAccount, + status, + gtsmodel.FilterContextNotifications, + ) + if err != nil { + return gtserror.Newf("error checking status filtering: %w", err) + } + + if hide { + // Don't notify. + return nil + } } - // Convert the notification to frontend API model for streaming / web push. - apiNotif, err := s.Converter.NotificationToAPINotification(ctx, notif, true) - if err != nil && !errors.Is(err, typeutils.ErrHideStatus) { + // Convert notification to frontend API model for streaming / web push. + apiNotif, err := s.Converter.NotificationToAPINotification(ctx, notif) + if err != nil { return gtserror.Newf("error converting notification to api representation: %w", err) } - if apiNotif == nil { - // Filtered. - return nil + if apiNotif.Status != nil { + // Set filter results on status, + // in case any were set above. + apiNotif.Status.Filtered = filtered } // Stream notification to the user. diff --git a/internal/processing/workers/surfacetimeline.go b/internal/processing/workers/surfacetimeline.go index 5e677c626..b1177cd28 100644 --- a/internal/processing/workers/surfacetimeline.go +++ b/internal/processing/workers/surfacetimeline.go @@ -19,7 +19,6 @@ package workers import ( "context" - "errors" "code.superseriousbusiness.org/gotosocial/internal/cache/timeline" "code.superseriousbusiness.org/gotosocial/internal/gtscontext" @@ -27,7 +26,6 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/log" "code.superseriousbusiness.org/gotosocial/internal/stream" - "code.superseriousbusiness.org/gotosocial/internal/typeutils" "code.superseriousbusiness.org/gotosocial/internal/util" ) @@ -350,28 +348,40 @@ func (s *Surface) timelineStatus( streamType string, filterCtx gtsmodel.FilterContext, ) bool { + // Check whether status is filtered in this context by timeline account. + filtered, hide, err := s.StatusFilter.StatusFilterResultsInContext(ctx, + account, + status, + filterCtx, + ) + if err != nil { + log.Errorf(ctx, "error filtering status %s: %v", status.URI, err) + } + + if hide { + // Don't even show to + // timeline account. + return false + } // Attempt to convert status to frontend API representation, // this will check whether status is filtered / muted. apiModel, err := s.Converter.StatusToAPIStatus(ctx, status, account, - filterCtx, ) - if err != nil && !errors.Is(err, typeutils.ErrHideStatus) { + if err != nil { log.Error(ctx, "error converting status %s to frontend: %v", status.URI, err) + } else { + + // Attach any filter results. + apiModel.Filtered = filtered } // Insert status to timeline cache regardless of // if API model was succesfully prepared or not. repeatBoost := timeline.InsertOne(status, apiModel) - if apiModel == nil { - // Status was - // filtered. - return false - } - if !repeatBoost { // Only stream if not repeated boost of recent status. s.Stream.Update(ctx, account, apiModel, streamType) @@ -683,26 +693,34 @@ func (s *Surface) timelineStreamStatusUpdate( status *gtsmodel.Status, streamType string, ) (bool, error) { + // Check whether status is filtered in this context by timeline account. + filtered, hide, err := s.StatusFilter.StatusFilterResultsInContext(ctx, + account, + status, + gtsmodel.FilterContextHome, + ) + if err != nil { + return false, gtserror.Newf("error filtering status: %w", err) + } + + if hide { + // Don't even show to + // timeline account. + return false, nil + } // Convert updated database model to frontend model. apiStatus, err := s.Converter.StatusToAPIStatus(ctx, status, account, - gtsmodel.FilterContextHome, ) - - switch { - case err == nil: - // no issue. - - case errors.Is(err, typeutils.ErrHideStatus): - // Don't put this status in the stream. - return false, nil - - default: + if err != nil { return false, gtserror.Newf("error converting status: %w", err) } + // Attach any filter results. + apiStatus.Filtered = filtered + // The status was updated so stream it to the user. s.Stream.StatusUpdate(ctx, account, apiStatus, streamType) diff --git a/internal/processing/workers/workers.go b/internal/processing/workers/workers.go index 1f4ef465f..67e928db0 100644 --- a/internal/processing/workers/workers.go +++ b/internal/processing/workers/workers.go @@ -21,6 +21,7 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/email" "code.superseriousbusiness.org/gotosocial/internal/federation" "code.superseriousbusiness.org/gotosocial/internal/filter/mutes" + "code.superseriousbusiness.org/gotosocial/internal/filter/status" "code.superseriousbusiness.org/gotosocial/internal/filter/visibility" "code.superseriousbusiness.org/gotosocial/internal/processing/account" "code.superseriousbusiness.org/gotosocial/internal/processing/common" @@ -46,6 +47,7 @@ func New( converter *typeutils.Converter, visFilter *visibility.Filter, muteFilter *mutes.Filter, + statusFilter *status.Filter, emailSender email.Sender, webPushSender webpush.Sender, account *account.Processor, @@ -69,6 +71,7 @@ func New( Stream: stream, VisFilter: visFilter, MuteFilter: muteFilter, + StatusFilter: statusFilter, EmailSender: emailSender, WebPushSender: webPushSender, Conversations: conversations, diff --git a/internal/typeutils/converter.go b/internal/typeutils/converter.go index 4f3658b0d..789404426 100644 --- a/internal/typeutils/converter.go +++ b/internal/typeutils/converter.go @@ -27,7 +27,6 @@ import ( apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model" "code.superseriousbusiness.org/gotosocial/internal/filter/interaction" - "code.superseriousbusiness.org/gotosocial/internal/filter/status" "code.superseriousbusiness.org/gotosocial/internal/filter/visibility" "code.superseriousbusiness.org/gotosocial/internal/log" "code.superseriousbusiness.org/gotosocial/internal/state" @@ -38,7 +37,6 @@ type Converter struct { defaultAvatars []string randAvatars sync.Map visFilter *visibility.Filter - statusFilter *status.Filter intFilter *interaction.Filter randStats atomic.Pointer[apimodel.RandomStats] } @@ -48,7 +46,6 @@ func NewConverter(state *state.State) *Converter { state: state, defaultAvatars: populateDefaultAvatars(), visFilter: visibility.NewFilter(state), - statusFilter: status.NewFilter(state), intFilter: interaction.NewFilter(state), } } diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index a79387c0f..3b5af6579 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -51,10 +51,6 @@ const ( instanceMastodonVersion = "3.5.3" ) -// ErrHideStatus indicates that a status has -// been filtered and should not be returned at all. -var ErrHideStatus = errors.New("hide status") - var instanceStatusesSupportedMimeTypes = []string{ string(apimodel.StatusContentTypePlain), string(apimodel.StatusContentTypeMarkdown), @@ -850,13 +846,11 @@ func (c *Converter) StatusToAPIStatus( ctx context.Context, status *gtsmodel.Status, requestingAccount *gtsmodel.Account, - filterCtx gtsmodel.FilterContext, ) (*apimodel.Status, error) { return c.statusToAPIStatus( ctx, status, requestingAccount, - filterCtx, true, true, ) @@ -870,7 +864,6 @@ func (c *Converter) statusToAPIStatus( ctx context.Context, status *gtsmodel.Status, requestingAccount *gtsmodel.Account, - filterCtx gtsmodel.FilterContext, placeholdAttachments bool, addPendingNote bool, ) (*apimodel.Status, error) { @@ -878,7 +871,6 @@ func (c *Converter) statusToAPIStatus( ctx, status, requestingAccount, // Can be nil. - filterCtx, // Can be empty. ) if err != nil { return nil, err @@ -945,8 +937,7 @@ func (c *Converter) StatusToWebStatus( s *gtsmodel.Status, ) (*apimodel.WebStatus, error) { apiStatus, err := c.statusToFrontend(ctx, s, - nil, // No authed requester. - gtsmodel.FilterContextNone, // No filters. + nil, // No authed requester. ) if err != nil { return nil, err @@ -1115,7 +1106,6 @@ func (c *Converter) statusToFrontend( ctx context.Context, status *gtsmodel.Status, requestingAccount *gtsmodel.Account, - filterCtx gtsmodel.FilterContext, ) ( *apimodel.Status, error, @@ -1123,7 +1113,6 @@ func (c *Converter) statusToFrontend( apiStatus, err := c.baseStatusToFrontend(ctx, status, requestingAccount, - filterCtx, ) if err != nil { return nil, err @@ -1133,12 +1122,8 @@ func (c *Converter) statusToFrontend( reblog, err := c.baseStatusToFrontend(ctx, status.BoostOf, requestingAccount, - filterCtx, ) - if errors.Is(err, ErrHideStatus) { - // If we'd hide the original status, hide the boost. - return nil, err - } else if err != nil { + if err != nil { return nil, gtserror.Newf("error converting boosted status: %w", err) } @@ -1165,7 +1150,6 @@ func (c *Converter) baseStatusToFrontend( ctx context.Context, status *gtsmodel.Status, requester *gtsmodel.Account, - filterCtx gtsmodel.FilterContext, ) ( *apimodel.Status, error, @@ -1340,20 +1324,6 @@ func (c *Converter) baseStatusToFrontend( apiStatus.URL = apiStatus.URI } - var hide bool - - // Pass the status through any stored filters of requesting account's, in context. - apiStatus.Filtered, hide, err = c.statusFilter.StatusFilterResultsInContext(ctx, - requester, - status, - filterCtx, - ) - if err != nil { - return nil, gtserror.Newf("error filtering status %s: %w", status.URI, err) - } else if hide { - return nil, ErrHideStatus - } - return apiStatus, nil } @@ -1866,7 +1836,6 @@ func (c *Converter) RelationshipToAPIRelationship(ctx context.Context, r *gtsmod func (c *Converter) NotificationToAPINotification( ctx context.Context, notif *gtsmodel.Notification, - filter bool, ) (*apimodel.Notification, error) { // Ensure notif populated. if err := c.state.DB.PopulateNotification(ctx, notif); err != nil { @@ -1882,27 +1851,14 @@ func (c *Converter) NotificationToAPINotification( // Get status that triggered this notif, if set. var apiStatus *apimodel.Status if notif.Status != nil { - var filterCtx gtsmodel.FilterContext - - if filter { - filterCtx = gtsmodel.FilterContextNotifications - } - apiStatus, err = c.StatusToAPIStatus(ctx, notif.Status, notif.TargetAccount, - filterCtx, ) - if err != nil && !errors.Is(err, ErrHideStatus) { + if err != nil { return nil, gtserror.Newf("error converting status to api: %w", err) } - if apiStatus == nil { - // Notif filtered for this - // status, nothing to do. - return nil, err - } - if apiStatus.Reblog != nil { // Use the actual reblog status // for the notifications endpoint. @@ -1926,7 +1882,6 @@ func (c *Converter) ConversationToAPIConversation( ctx context.Context, conversation *gtsmodel.Conversation, requester *gtsmodel.Account, - filters []*gtsmodel.Filter, ) (*apimodel.Conversation, error) { apiConversation := &apimodel.Conversation{ ID: conversation.ID, @@ -1941,9 +1896,8 @@ func (c *Converter) ConversationToAPIConversation( ctx, conversation.LastStatus, requester, - gtsmodel.FilterContextNotifications, ) - if err != nil && !errors.Is(err, ErrHideStatus) { + if err != nil { return nil, gtserror.Newf( "error converting status %s to API representation: %w", conversation.LastStatus.ID, @@ -2209,7 +2163,6 @@ func (c *Converter) ReportToAdminAPIReport(ctx context.Context, r *gtsmodel.Repo ctx, s, requestingAccount, - gtsmodel.FilterContextNone, true, // Placehold unknown attachments. // Don't add note about @@ -2913,7 +2866,6 @@ func (c *Converter) InteractionReqToAPIInteractionReq( ctx, req.Status, requestingAcct, - gtsmodel.FilterContextNone, ) if err != nil { err := gtserror.Newf("error converting interacted status: %w", err) @@ -2926,7 +2878,6 @@ func (c *Converter) InteractionReqToAPIInteractionReq( ctx, req.Reply, requestingAcct, - gtsmodel.FilterContextNone, true, // Placehold unknown attachments. // Don't add note about pending; diff --git a/internal/typeutils/internaltofrontend_test.go b/internal/typeutils/internaltofrontend_test.go index 1fc55acca..8b0d15f10 100644 --- a/internal/typeutils/internaltofrontend_test.go +++ b/internal/typeutils/internaltofrontend_test.go @@ -24,12 +24,9 @@ import ( "strings" "testing" - apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model" "code.superseriousbusiness.org/gotosocial/internal/config" "code.superseriousbusiness.org/gotosocial/internal/db" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" - "code.superseriousbusiness.org/gotosocial/internal/id" - "code.superseriousbusiness.org/gotosocial/internal/typeutils" "code.superseriousbusiness.org/gotosocial/internal/util" "code.superseriousbusiness.org/gotosocial/testrig" "github.com/stretchr/testify/suite" @@ -466,7 +463,7 @@ func (suite *InternalToFrontendTestSuite) TestLocalInstanceAccountToFrontendBloc func (suite *InternalToFrontendTestSuite) TestStatusToFrontend() { testStatus := suite.testStatuses["admin_account_status_1"] requestingAccount := suite.testAccounts["local_account_1"] - apiStatus, err := suite.typeconverter.StatusToAPIStatus(suite.T().Context(), testStatus, requestingAccount, gtsmodel.FilterContextNone) + apiStatus, err := suite.typeconverter.StatusToAPIStatus(suite.T().Context(), testStatus, requestingAccount) suite.NoError(err) b, err := json.MarshalIndent(apiStatus, "", " ") @@ -629,7 +626,7 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontendHTMLContentWarning testStatus.ContentWarning = `

First paragraph of content warning

Here's the title!

Big boobs
Tee hee!

Some more text
And a bunch more

Hasta la victoria siempre!

` requestingAccount := suite.testAccounts["local_account_1"] - apiStatus, err := suite.typeconverter.StatusToAPIStatus(suite.T().Context(), testStatus, requestingAccount, gtsmodel.FilterContextNone) + apiStatus, err := suite.typeconverter.StatusToAPIStatus(suite.T().Context(), testStatus, requestingAccount) suite.NoError(err) b, err := json.MarshalIndent(apiStatus, "", " ") @@ -795,7 +792,7 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontendApplicationDeleted } requestingAccount := suite.testAccounts["local_account_1"] - apiStatus, err := suite.typeconverter.StatusToAPIStatus(ctx, testStatus, requestingAccount, gtsmodel.FilterContextNone) + apiStatus, err := suite.typeconverter.StatusToAPIStatus(ctx, testStatus, requestingAccount) suite.NoError(err) b, err := json.MarshalIndent(apiStatus, "", " ") @@ -950,625 +947,11 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontendApplicationDeleted }`, string(b)) } -// Modify a fixture status into a status that should be filtered, -// and then filter it, returning the API status or any error from converting it. -func (suite *InternalToFrontendTestSuite) filteredStatusToFrontend(action gtsmodel.FilterAction, boost bool) (*apimodel.Status, error) { - ctx := suite.T().Context() - - testStatus := suite.testStatuses["admin_account_status_1"] - testStatus.Content += " fnord" - testStatus.Text += " fnord" - - if boost { - // Modify a fixture boost into a boost of the above status. - boostStatus := suite.testStatuses["admin_account_status_4"] - boostStatus.BoostOf = testStatus - boostStatus.BoostOfID = testStatus.ID - testStatus = boostStatus - } - - requestingAccount := suite.testAccounts["local_account_1"] - - expectedMatchingFilter := suite.testFilters["local_account_1_filter_1"] - expectedMatchingFilter.Action = action - - err := suite.state.DB.UpdateFilter(ctx, expectedMatchingFilter, "action") - suite.NoError(err) - - return suite.typeconverter.StatusToAPIStatus( - suite.T().Context(), - testStatus, - requestingAccount, - gtsmodel.FilterContextHome, - ) -} - -// Test that a status which is filtered with a warn filter by the requesting user has `filtered` set correctly. -func (suite *InternalToFrontendTestSuite) TestWarnFilteredStatusToFrontend() { - apiStatus, err := suite.filteredStatusToFrontend(gtsmodel.FilterActionWarn, false) - suite.NoError(err) - - b, err := json.MarshalIndent(apiStatus, "", " ") - suite.NoError(err) - - suite.Equal(`{ - "id": "01F8MH75CBF9JFX4ZAD54N0W0R", - "created_at": "2021-10-20T11:36:45.000Z", - "edited_at": null, - "in_reply_to_id": null, - "in_reply_to_account_id": null, - "sensitive": false, - "spoiler_text": "", - "visibility": "public", - "language": "en", - "uri": "http://localhost:8080/users/admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R", - "url": "http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R", - "replies_count": 1, - "reblogs_count": 0, - "favourites_count": 1, - "favourited": true, - "reblogged": false, - "muted": false, - "bookmarked": true, - "pinned": false, - "content": "\u003cp\u003ehello world! \u003ca href=\"http://localhost:8080/tags/welcome\" class=\"mention hashtag\" rel=\"tag nofollow noreferrer noopener\" target=\"_blank\"\u003e#\u003cspan\u003ewelcome\u003c/span\u003e\u003c/a\u003e ! first post on the instance :rainbow: !\u003c/p\u003e fnord", - "reblog": null, - "application": { - "name": "superseriousbusiness", - "website": "https://superserious.business" - }, - "account": { - "id": "01F8MH17FWEB39HZJ76B6VXSKF", - "username": "admin", - "acct": "admin", - "display_name": "", - "locked": false, - "discoverable": true, - "bot": false, - "created_at": "2022-05-17T13:10:59.000Z", - "note": "", - "url": "http://localhost:8080/@admin", - "avatar": "", - "avatar_static": "", - "header": "http://localhost:8080/assets/default_header.webp", - "header_static": "http://localhost:8080/assets/default_header.webp", - "header_description": "Flat gray background (default header).", - "followers_count": 1, - "following_count": 1, - "statuses_count": 4, - "last_status_at": "2021-10-20", - "emojis": [], - "fields": [], - "enable_rss": true, - "roles": [ - { - "id": "admin", - "name": "admin", - "color": "" - } - ], - "group": false - }, - "media_attachments": [ - { - "id": "01F8MH6NEM8D7527KZAECTCR76", - "type": "image", - "url": "http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpg", - "text_url": "http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpg", - "preview_url": "http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/small/01F8MH6NEM8D7527KZAECTCR76.webp", - "remote_url": null, - "preview_remote_url": null, - "meta": { - "original": { - "width": 1200, - "height": 630, - "size": "1200x630", - "aspect": 1.9047619 - }, - "small": { - "width": 512, - "height": 268, - "size": "512x268", - "aspect": 1.9104477 - }, - "focus": { - "x": -0.5, - "y": 0.5 - } - }, - "description": "Black and white image of some 50's style text saying: Welcome On Board", - "blurhash": "LIIE|gRj00WB-;j[t7j[4nWBj[Rj" - } - ], - "mentions": [], - "tags": [ - { - "name": "welcome", - "url": "http://localhost:8080/tags/welcome" - } - ], - "emojis": [ - { - "shortcode": "rainbow", - "url": "http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png", - "static_url": "http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/emoji/static/01F8MH9H8E4VG3KDYJR9EGPXCQ.png", - "visible_in_picker": true, - "category": "reactions" - } - ], - "card": null, - "poll": null, - "text": "hello world! #welcome ! first post on the instance :rainbow: ! fnord", - "content_type": "text/plain", - "filtered": [ - { - "filter": { - "id": "01HN26VM6KZTW1ANNRVSBMA461", - "title": "fnord", - "context": [ - "home", - "public" - ], - "expires_at": null, - "filter_action": "warn", - "keywords": [ - { - "id": "01HN272TAVWAXX72ZX4M8JZ0PS", - "keyword": "fnord", - "whole_word": true - } - ], - "statuses": [] - }, - "keyword_matches": [ - "fnord" - ], - "status_matches": [] - } - ], - "interaction_policy": { - "can_favourite": { - "automatic_approval": [ - "public", - "me" - ], - "manual_approval": [], - "always": [ - "public", - "me" - ], - "with_approval": [] - }, - "can_reply": { - "automatic_approval": [ - "public", - "me" - ], - "manual_approval": [], - "always": [ - "public", - "me" - ], - "with_approval": [] - }, - "can_reblog": { - "automatic_approval": [ - "public", - "me" - ], - "manual_approval": [], - "always": [ - "public", - "me" - ], - "with_approval": [] - } - } -}`, string(b)) -} - -// Test that a status which is filtered with a warn filter by the requesting user has `filtered` set correctly when boosted. -func (suite *InternalToFrontendTestSuite) TestWarnFilteredBoostToFrontend() { - apiStatus, err := suite.filteredStatusToFrontend(gtsmodel.FilterActionWarn, true) - suite.NoError(err) - - b, err := json.MarshalIndent(apiStatus, "", " ") - suite.NoError(err) - - suite.Equal(`{ - "id": "01G36SF3V6Y6V5BF9P4R7PQG7G", - "created_at": "2021-10-20T10:41:37.000Z", - "edited_at": null, - "in_reply_to_id": null, - "in_reply_to_account_id": null, - "sensitive": false, - "spoiler_text": "", - "visibility": "public", - "language": null, - "uri": "http://localhost:8080/users/admin/statuses/01G36SF3V6Y6V5BF9P4R7PQG7G", - "url": "http://localhost:8080/@admin/statuses/01G36SF3V6Y6V5BF9P4R7PQG7G", - "replies_count": 0, - "reblogs_count": 0, - "favourites_count": 0, - "favourited": true, - "reblogged": false, - "muted": false, - "bookmarked": true, - "pinned": false, - "content": "", - "reblog": { - "id": "01F8MH75CBF9JFX4ZAD54N0W0R", - "created_at": "2021-10-20T11:36:45.000Z", - "edited_at": null, - "in_reply_to_id": null, - "in_reply_to_account_id": null, - "sensitive": false, - "spoiler_text": "", - "visibility": "public", - "language": "en", - "uri": "http://localhost:8080/users/admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R", - "url": "http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R", - "replies_count": 1, - "reblogs_count": 0, - "favourites_count": 1, - "favourited": true, - "reblogged": false, - "muted": false, - "bookmarked": true, - "pinned": false, - "content": "\u003cp\u003ehello world! \u003ca href=\"http://localhost:8080/tags/welcome\" class=\"mention hashtag\" rel=\"tag nofollow noreferrer noopener\" target=\"_blank\"\u003e#\u003cspan\u003ewelcome\u003c/span\u003e\u003c/a\u003e ! first post on the instance :rainbow: !\u003c/p\u003e fnord", - "reblog": null, - "application": { - "name": "superseriousbusiness", - "website": "https://superserious.business" - }, - "account": { - "id": "01F8MH1H7YV1Z7D2C8K2730QBF", - "username": "the_mighty_zork", - "acct": "the_mighty_zork", - "display_name": "original zork (he/they)", - "locked": false, - "discoverable": true, - "bot": false, - "created_at": "2022-05-20T11:09:18.000Z", - "note": "\u003cp\u003ehey yo this is my profile!\u003c/p\u003e", - "url": "http://localhost:8080/@the_mighty_zork", - "avatar": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg", - "avatar_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.webp", - "avatar_description": "a green goblin looking nasty", - "avatar_media_id": "01F8MH58A357CV5K7R7TJMSH6S", - "header": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg", - "header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.webp", - "header_description": "A very old-school screenshot of the original team fortress mod for quake", - "header_media_id": "01PFPMWK2FF0D9WMHEJHR07C3Q", - "followers_count": 2, - "following_count": 2, - "statuses_count": 9, - "last_status_at": "2024-11-01", - "emojis": [], - "fields": [], - "enable_rss": true, - "group": false - }, - "media_attachments": [ - { - "id": "01F8MH6NEM8D7527KZAECTCR76", - "type": "image", - "url": "http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpg", - "text_url": "http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpg", - "preview_url": "http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/small/01F8MH6NEM8D7527KZAECTCR76.webp", - "remote_url": null, - "preview_remote_url": null, - "meta": { - "original": { - "width": 1200, - "height": 630, - "size": "1200x630", - "aspect": 1.9047619 - }, - "small": { - "width": 512, - "height": 268, - "size": "512x268", - "aspect": 1.9104477 - }, - "focus": { - "x": -0.5, - "y": 0.5 - } - }, - "description": "Black and white image of some 50's style text saying: Welcome On Board", - "blurhash": "LIIE|gRj00WB-;j[t7j[4nWBj[Rj" - } - ], - "mentions": [], - "tags": [ - { - "name": "welcome", - "url": "http://localhost:8080/tags/welcome" - } - ], - "emojis": [ - { - "shortcode": "rainbow", - "url": "http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png", - "static_url": "http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/emoji/static/01F8MH9H8E4VG3KDYJR9EGPXCQ.png", - "visible_in_picker": true, - "category": "reactions" - } - ], - "card": null, - "poll": null, - "text": "hello world! #welcome ! first post on the instance :rainbow: ! fnord", - "content_type": "text/plain", - "filtered": [ - { - "filter": { - "id": "01HN26VM6KZTW1ANNRVSBMA461", - "title": "fnord", - "context": [ - "home", - "public" - ], - "expires_at": null, - "filter_action": "warn", - "keywords": [ - { - "id": "01HN272TAVWAXX72ZX4M8JZ0PS", - "keyword": "fnord", - "whole_word": true - } - ], - "statuses": [] - }, - "keyword_matches": [ - "fnord" - ], - "status_matches": [] - } - ], - "interaction_policy": { - "can_favourite": { - "automatic_approval": [ - "public", - "me" - ], - "manual_approval": [], - "always": [ - "public", - "me" - ], - "with_approval": [] - }, - "can_reply": { - "automatic_approval": [ - "public", - "me" - ], - "manual_approval": [], - "always": [ - "public", - "me" - ], - "with_approval": [] - }, - "can_reblog": { - "automatic_approval": [ - "public", - "me" - ], - "manual_approval": [], - "always": [ - "public", - "me" - ], - "with_approval": [] - } - } - }, - "application": { - "name": "superseriousbusiness", - "website": "https://superserious.business" - }, - "account": { - "id": "01F8MH17FWEB39HZJ76B6VXSKF", - "username": "admin", - "acct": "admin", - "display_name": "", - "locked": false, - "discoverable": true, - "bot": false, - "created_at": "2022-05-17T13:10:59.000Z", - "note": "", - "url": "http://localhost:8080/@admin", - "avatar": "", - "avatar_static": "", - "header": "http://localhost:8080/assets/default_header.webp", - "header_static": "http://localhost:8080/assets/default_header.webp", - "header_description": "Flat gray background (default header).", - "followers_count": 1, - "following_count": 1, - "statuses_count": 4, - "last_status_at": "2021-10-20", - "emojis": [], - "fields": [], - "enable_rss": true, - "roles": [ - { - "id": "admin", - "name": "admin", - "color": "" - } - ], - "group": false - }, - "media_attachments": [], - "mentions": [], - "tags": [], - "emojis": [], - "card": null, - "poll": null, - "filtered": [ - { - "filter": { - "id": "01HN26VM6KZTW1ANNRVSBMA461", - "title": "fnord", - "context": [ - "home", - "public" - ], - "expires_at": null, - "filter_action": "warn", - "keywords": [ - { - "id": "01HN272TAVWAXX72ZX4M8JZ0PS", - "keyword": "fnord", - "whole_word": true - } - ], - "statuses": [] - }, - "keyword_matches": [ - "fnord" - ], - "status_matches": [] - } - ], - "interaction_policy": { - "can_favourite": { - "automatic_approval": [ - "public", - "me" - ], - "manual_approval": [], - "always": [ - "public", - "me" - ], - "with_approval": [] - }, - "can_reply": { - "automatic_approval": [ - "public", - "me" - ], - "manual_approval": [], - "always": [ - "public", - "me" - ], - "with_approval": [] - }, - "can_reblog": { - "automatic_approval": [ - "public", - "me" - ], - "manual_approval": [], - "always": [ - "public", - "me" - ], - "with_approval": [] - } - } -}`, string(b)) -} - -// Test that a status which is filtered with a hide filter by the requesting user results in the ErrHideStatus error. -func (suite *InternalToFrontendTestSuite) TestHideFilteredStatusToFrontend() { - _, err := suite.filteredStatusToFrontend(gtsmodel.FilterActionHide, false) - suite.ErrorIs(err, typeutils.ErrHideStatus) -} - -// Test that a status which is filtered with a hide filter by the requesting user results in the ErrHideStatus error for a boost of that status. -func (suite *InternalToFrontendTestSuite) TestHideFilteredBoostToFrontend() { - _, err := suite.filteredStatusToFrontend(gtsmodel.FilterActionHide, true) - suite.ErrorIs(err, typeutils.ErrHideStatus) -} - -// Test that a hashtag filter for a hashtag in Mastodon HTML content works the way most users would expect. -func (suite *InternalToFrontendTestSuite) testHashtagFilteredStatusToFrontend(wholeWord bool, boost bool) { - ctx := suite.T().Context() - - testStatus := new(gtsmodel.Status) - *testStatus = *suite.testStatuses["admin_account_status_1"] - testStatus.Content = `

doggo doggin' it

#dogsofmastodon

` - - if boost { - boost, err := suite.typeconverter.StatusToBoost( - suite.T().Context(), - testStatus, - suite.testAccounts["admin_account"], - "", - ) - if err != nil { - suite.FailNow(err.Error()) - } - testStatus = boost - } - - var err error - - requestingAccount := suite.testAccounts["local_account_1"] - - filter := >smodel.Filter{ - ID: id.NewULID(), - Title: id.NewULID(), - AccountID: requestingAccount.ID, - Action: gtsmodel.FilterActionWarn, - Contexts: gtsmodel.FilterContexts(gtsmodel.FilterContextHome), - } - - filterKeyword := >smodel.FilterKeyword{ - ID: id.NewULID(), - FilterID: filter.ID, - Keyword: "#dogsofmastodon", - WholeWord: &wholeWord, - } - - filter.KeywordIDs = []string{filterKeyword.ID} - - err = suite.state.DB.PutFilterKeyword(ctx, filterKeyword) - suite.NoError(err) - - err = suite.state.DB.PutFilter(ctx, filter) - suite.NoError(err) - - apiStatus, err := suite.typeconverter.StatusToAPIStatus( - suite.T().Context(), - testStatus, - requestingAccount, - gtsmodel.FilterContextHome, - ) - if err != nil { - suite.FailNow(err.Error()) - } - - suite.NotEmpty(apiStatus.Filtered) -} - -func (suite *InternalToFrontendTestSuite) TestHashtagWholeWordFilteredStatusToFrontend() { - suite.testHashtagFilteredStatusToFrontend(true, false) -} - -func (suite *InternalToFrontendTestSuite) TestHashtagWholeWordFilteredBoostToFrontend() { - suite.testHashtagFilteredStatusToFrontend(true, true) -} - -func (suite *InternalToFrontendTestSuite) TestHashtagAnywhereFilteredStatusToFrontend() { - suite.testHashtagFilteredStatusToFrontend(false, false) -} - -func (suite *InternalToFrontendTestSuite) TestHashtagAnywhereFilteredBoostToFrontend() { - suite.testHashtagFilteredStatusToFrontend(false, true) -} - func (suite *InternalToFrontendTestSuite) TestStatusToFrontendUnknownAttachments() { testStatus := suite.testStatuses["remote_account_2_status_1"] requestingAccount := suite.testAccounts["admin_account"] - apiStatus, err := suite.typeconverter.StatusToAPIStatus(suite.T().Context(), testStatus, requestingAccount, gtsmodel.FilterContextNone) + apiStatus, err := suite.typeconverter.StatusToAPIStatus(suite.T().Context(), testStatus, requestingAccount) suite.NoError(err) b, err := json.MarshalIndent(apiStatus, "", " ") @@ -1895,7 +1278,7 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontendUnknownLanguage() *testStatus = *suite.testStatuses["admin_account_status_1"] testStatus.Language = "" requestingAccount := suite.testAccounts["local_account_1"] - apiStatus, err := suite.typeconverter.StatusToAPIStatus(suite.T().Context(), testStatus, requestingAccount, gtsmodel.FilterContextNone) + apiStatus, err := suite.typeconverter.StatusToAPIStatus(suite.T().Context(), testStatus, requestingAccount) suite.NoError(err) b, err := json.MarshalIndent(apiStatus, "", " ") @@ -2056,7 +1439,7 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontendPartialInteraction *testStatus = *suite.testStatuses["local_account_1_status_3"] testStatus.Language = "" requestingAccount := suite.testAccounts["admin_account"] - apiStatus, err := suite.typeconverter.StatusToAPIStatus(suite.T().Context(), testStatus, requestingAccount, gtsmodel.FilterContextNone) + apiStatus, err := suite.typeconverter.StatusToAPIStatus(suite.T().Context(), testStatus, requestingAccount) suite.NoError(err) b, err := json.MarshalIndent(apiStatus, "", " ") @@ -2169,7 +1552,6 @@ func (suite *InternalToFrontendTestSuite) TestStatusToAPIStatusPendingApproval() suite.T().Context(), testStatus, requestingAccount, - gtsmodel.FilterContextNone, ) if err != nil { suite.FailNow(err.Error()) @@ -3933,10 +3315,9 @@ func (suite *InternalToFrontendTestSuite) TestIntReqToAPI() { func (suite *InternalToFrontendTestSuite) TestConversationToAPISelfConvo() { var ( - ctx = suite.T().Context() - requester = suite.testAccounts["local_account_1"] - lastStatus = suite.testStatuses["local_account_1_status_1"] - filters []*gtsmodel.Filter = nil + ctx = suite.T().Context() + requester = suite.testAccounts["local_account_1"] + lastStatus = suite.testStatuses["local_account_1_status_1"] ) convo := >smodel.Conversation{ @@ -3954,7 +3335,6 @@ func (suite *InternalToFrontendTestSuite) TestConversationToAPISelfConvo() { ctx, convo, requester, - filters, ) if err != nil { suite.FailNow(err.Error()) @@ -4106,10 +3486,9 @@ func (suite *InternalToFrontendTestSuite) TestConversationToAPISelfConvo() { func (suite *InternalToFrontendTestSuite) TestConversationToAPI() { var ( - ctx = suite.T().Context() - requester = suite.testAccounts["local_account_1"] - lastStatus = suite.testStatuses["local_account_1_status_1"] - filters []*gtsmodel.Filter = nil + ctx = suite.T().Context() + requester = suite.testAccounts["local_account_1"] + lastStatus = suite.testStatuses["local_account_1_status_1"] ) convo := >smodel.Conversation{ @@ -4129,7 +3508,6 @@ func (suite *InternalToFrontendTestSuite) TestConversationToAPI() { ctx, convo, requester, - filters, ) if err != nil { suite.FailNow(err.Error()) diff --git a/internal/webpush/realsender_test.go b/internal/webpush/realsender_test.go index e11067e1d..52f95ce49 100644 --- a/internal/webpush/realsender_test.go +++ b/internal/webpush/realsender_test.go @@ -33,6 +33,7 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/federation" "code.superseriousbusiness.org/gotosocial/internal/filter/interaction" "code.superseriousbusiness.org/gotosocial/internal/filter/mutes" + "code.superseriousbusiness.org/gotosocial/internal/filter/status" "code.superseriousbusiness.org/gotosocial/internal/filter/visibility" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/media" @@ -126,6 +127,7 @@ func (suite *RealSenderStandardTestSuite) SetupTest() { visibility.NewFilter(&suite.state), mutes.NewFilter(&suite.state), interaction.NewFilter(&suite.state), + status.NewFilter(&suite.state), ) testrig.StartWorkers(&suite.state, suite.processor.Workers()) @@ -190,7 +192,7 @@ func (suite *RealSenderStandardTestSuite) simulatePushNotification( }, nil } - apiNotif, err := suite.typeconverter.NotificationToAPINotification(ctx, notification, false) + apiNotif, err := suite.typeconverter.NotificationToAPINotification(ctx, notification) suite.NoError(err) // Send the push notification. diff --git a/testrig/processor.go b/testrig/processor.go index 4acb7c648..7d18c1c23 100644 --- a/testrig/processor.go +++ b/testrig/processor.go @@ -23,6 +23,7 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/federation" "code.superseriousbusiness.org/gotosocial/internal/filter/interaction" "code.superseriousbusiness.org/gotosocial/internal/filter/mutes" + "code.superseriousbusiness.org/gotosocial/internal/filter/status" "code.superseriousbusiness.org/gotosocial/internal/filter/visibility" "code.superseriousbusiness.org/gotosocial/internal/media" "code.superseriousbusiness.org/gotosocial/internal/processing" @@ -59,5 +60,6 @@ func NewTestProcessor( visibility.NewFilter(state), mutes.NewFilter(state), interaction.NewFilter(state), + status.NewFilter(state), ) } diff --git a/testrig/teststructs.go b/testrig/teststructs.go index a1e241f4e..f002dd079 100644 --- a/testrig/teststructs.go +++ b/testrig/teststructs.go @@ -23,6 +23,7 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/email" "code.superseriousbusiness.org/gotosocial/internal/filter/interaction" "code.superseriousbusiness.org/gotosocial/internal/filter/mutes" + "code.superseriousbusiness.org/gotosocial/internal/filter/status" "code.superseriousbusiness.org/gotosocial/internal/filter/visibility" "code.superseriousbusiness.org/gotosocial/internal/processing" "code.superseriousbusiness.org/gotosocial/internal/processing/common" @@ -51,6 +52,7 @@ type TestStructs struct { WebPushSender *WebPushMockSender TransportController transport.Controller InteractionFilter *interaction.Filter + StatusFilter *status.Filter } func SetupTestStructs( @@ -71,6 +73,7 @@ func SetupTestStructs( visFilter := visibility.NewFilter(&state) muteFilter := mutes.NewFilter(&state) intFilter := interaction.NewFilter(&state) + statusFilter := status.NewFilter(&state) httpClient := NewMockHTTPClient(nil, rMediaPath) httpClient.TestRemotePeople = NewTestFediPeople() @@ -90,6 +93,7 @@ func SetupTestStructs( federator, visFilter, muteFilter, + statusFilter, ) processor := processing.NewProcessor( @@ -105,6 +109,7 @@ func SetupTestStructs( visFilter, muteFilter, intFilter, + statusFilter, ) StartWorkers(&state, processor.Workers()) @@ -122,6 +127,7 @@ func SetupTestStructs( WebPushSender: webPushSender, TransportController: transportController, InteractionFilter: intFilter, + StatusFilter: statusFilter, } }