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
doggo doggin' it
` + + 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
Big boobs
Tee hee!
Some more text
And a bunch more
Hasta la victoria siempre!
doggo doggin' it
` - - 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, } }