diff options
Diffstat (limited to 'internal/processing')
35 files changed, 417 insertions, 344 deletions
diff --git a/internal/processing/account/account_test.go b/internal/processing/account/account_test.go index 4af63d1b3..d4fb6ddfb 100644 --- a/internal/processing/account/account_test.go +++ b/internal/processing/account/account_test.go @@ -25,6 +25,7 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/db" "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/visibility" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/media" @@ -104,9 +105,10 @@ func (suite *AccountStandardTestSuite) SetupTest() { suite.sentEmails = make(map[string]string) suite.emailSender = testrig.NewEmailSender("../../../web/template/", suite.sentEmails) - filter := visibility.NewFilter(&suite.state) - common := common.New(&suite.state, suite.mediaManager, suite.tc, suite.federator, filter) - suite.accountProcessor = account.New(&common, &suite.state, suite.tc, suite.mediaManager, suite.federator, filter, processing.GetParseMentionFunc(&suite.state, suite.federator)) + 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)) 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 7c7f7cb07..329bcf30c 100644 --- a/internal/processing/account/bookmarks.go +++ b/internal/processing/account/bookmarks.go @@ -75,7 +75,7 @@ func (p *Processor) BookmarksGet(ctx context.Context, requestingAccount *gtsmode } // Convert the status. - item, err := p.converter.StatusToAPIStatus(ctx, status, requestingAccount, statusfilter.FilterContextNone, nil, nil) + item, err := p.converter.StatusToAPIStatus(ctx, status, requestingAccount, statusfilter.FilterContextNone, nil) if err != nil { log.Errorf(ctx, "error converting bookmarked status to api: %s", err) continue diff --git a/internal/processing/account/statuses.go b/internal/processing/account/statuses.go index 191fdcb5f..0ff9ef7e1 100644 --- a/internal/processing/account/statuses.go +++ b/internal/processing/account/statuses.go @@ -105,7 +105,7 @@ func (p *Processor) StatusesGet( for _, s := range filtered { // Convert filtered statuses to API statuses. - item, err := p.converter.StatusToAPIStatus(ctx, s, requestingAccount, statusfilter.FilterContextAccount, filters, nil) + item, err := p.converter.StatusToAPIStatus(ctx, s, requestingAccount, statusfilter.FilterContextAccount, filters) if err != nil { log.Errorf(ctx, "error convering to api status: %v", err) continue diff --git a/internal/processing/admin/admin_test.go b/internal/processing/admin/admin_test.go index db46fb17f..8f2eb23f2 100644 --- a/internal/processing/admin/admin_test.go +++ b/internal/processing/admin/admin_test.go @@ -24,6 +24,7 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/email" "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/visibility" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/media" @@ -113,6 +114,7 @@ func (suite *AdminStandardTestSuite) SetupTest() { suite.emailSender, testrig.NewNoopWebPushSender(), visibility.NewFilter(&suite.state), + mutes.NewFilter(&suite.state), interaction.NewFilter(&suite.state), ) diff --git a/internal/processing/common/common.go b/internal/processing/common/common.go index 9f4753147..bebbdffea 100644 --- a/internal/processing/common/common.go +++ b/internal/processing/common/common.go @@ -19,6 +19,7 @@ package common import ( "code.superseriousbusiness.org/gotosocial/internal/federation" + "code.superseriousbusiness.org/gotosocial/internal/filter/mutes" "code.superseriousbusiness.org/gotosocial/internal/filter/visibility" "code.superseriousbusiness.org/gotosocial/internal/media" "code.superseriousbusiness.org/gotosocial/internal/state" @@ -29,11 +30,12 @@ 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 + state *state.State + media *media.Manager + converter *typeutils.Converter + federator *federation.Federator + visFilter *visibility.Filter + muteFilter *mutes.Filter } // New returns a new Processor instance. @@ -43,12 +45,14 @@ func New( converter *typeutils.Converter, federator *federation.Federator, visFilter *visibility.Filter, + muteFilter *mutes.Filter, ) Processor { return Processor{ - state: state, - media: media, - converter: converter, - federator: federator, - visFilter: visFilter, + state: state, + media: media, + converter: converter, + federator: federator, + visFilter: visFilter, + muteFilter: muteFilter, } } diff --git a/internal/processing/common/status.go b/internal/processing/common/status.go index 14245f88a..441a58384 100644 --- a/internal/processing/common/status.go +++ b/internal/processing/common/status.go @@ -25,7 +25,6 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/db" "code.superseriousbusiness.org/gotosocial/internal/federation/dereferencing" statusfilter "code.superseriousbusiness.org/gotosocial/internal/filter/status" - "code.superseriousbusiness.org/gotosocial/internal/filter/usermute" "code.superseriousbusiness.org/gotosocial/internal/gtserror" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/log" @@ -216,7 +215,6 @@ func (p *Processor) GetAPIStatus( requester, statusfilter.FilterContextNone, nil, - nil, ) if err != nil { err := gtserror.Newf("error converting: %w", err) @@ -238,7 +236,6 @@ func (p *Processor) GetVisibleAPIStatuses( statuses []*gtsmodel.Status, filterContext statusfilter.FilterContext, filters []*gtsmodel.Filter, - userMutes []*gtsmodel.UserMute, ) []apimodel.Status { // Start new log entry with @@ -247,9 +244,6 @@ func (p *Processor) GetVisibleAPIStatuses( l := log.WithContext(ctx). WithField("caller", log.Caller(3)) - // Compile mutes to useable user mutes for type converter. - compUserMutes := usermute.NewCompiledUserMuteList(userMutes) - // Iterate filtered statuses for conversion to API model. apiStatuses := make([]apimodel.Status, 0, len(statuses)) for _, status := range statuses { @@ -268,13 +262,23 @@ func (p *Processor) GetVisibleAPIStatuses( continue } + // Check whether this status is muted by requesting account. + muted, err := p.muteFilter.StatusMuted(ctx, requester, status) + if err != nil { + log.Errorf(ctx, "error checking mute: %v", err) + continue + } + + if muted { + continue + } + // Convert to API status, taking mute / filter into account. apiStatus, err := p.converter.StatusToAPIStatus(ctx, status, requester, filterContext, filters, - compUserMutes, ) if err != nil && !errors.Is(err, statusfilter.ErrHideStatus) { l.Errorf("error converting: %v", err) diff --git a/internal/processing/conversations/conversations.go b/internal/processing/conversations/conversations.go index a4b8b7234..e31f60500 100644 --- a/internal/processing/conversations/conversations.go +++ b/internal/processing/conversations/conversations.go @@ -22,9 +22,8 @@ import ( "errors" "code.superseriousbusiness.org/gotosocial/internal/db" - "code.superseriousbusiness.org/gotosocial/internal/filter/usermute" + "code.superseriousbusiness.org/gotosocial/internal/filter/mutes" "code.superseriousbusiness.org/gotosocial/internal/filter/visibility" - "code.superseriousbusiness.org/gotosocial/internal/gtscontext" "code.superseriousbusiness.org/gotosocial/internal/gtserror" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/state" @@ -32,20 +31,23 @@ import ( ) type Processor struct { - state *state.State - converter *typeutils.Converter - filter *visibility.Filter + state *state.State + converter *typeutils.Converter + visFilter *visibility.Filter + muteFilter *mutes.Filter } func New( state *state.State, converter *typeutils.Converter, - filter *visibility.Filter, + visFilter *visibility.Filter, + muteFilter *mutes.Filter, ) Processor { return Processor{ - state: state, - converter: converter, - filter: filter, + state: state, + converter: converter, + visFilter: visFilter, + muteFilter: muteFilter, } } @@ -95,13 +97,13 @@ func (p *Processor) getConversationOwnedBy( } // getFiltersAndMutes gets the given account's filters and compiled mute list. -func (p *Processor) getFiltersAndMutes( +func (p *Processor) getFilters( ctx context.Context, requestingAccount *gtsmodel.Account, -) ([]*gtsmodel.Filter, *usermute.CompiledUserMuteList, gtserror.WithCode) { +) ([]*gtsmodel.Filter, gtserror.WithCode) { filters, err := p.state.DB.GetFiltersForAccountID(ctx, requestingAccount.ID) if err != nil { - return nil, nil, gtserror.NewErrorInternalError( + return nil, gtserror.NewErrorInternalError( gtserror.Newf( "DB error getting filters for account %s: %w", requestingAccount.ID, @@ -109,18 +111,5 @@ func (p *Processor) getFiltersAndMutes( ), ) } - - mutes, err := p.state.DB.GetAccountMutes(gtscontext.SetBarebones(ctx), requestingAccount.ID, nil) - if err != nil { - return nil, nil, gtserror.NewErrorInternalError( - gtserror.Newf( - "DB error getting mutes for account %s: %w", - requestingAccount.ID, - err, - ), - ) - } - compiledMutes := usermute.NewCompiledUserMuteList(mutes) - - return filters, compiledMutes, nil + return filters, nil } diff --git a/internal/processing/conversations/conversations_test.go b/internal/processing/conversations/conversations_test.go index 40145c2fb..383938564 100644 --- a/internal/processing/conversations/conversations_test.go +++ b/internal/processing/conversations/conversations_test.go @@ -27,6 +27,7 @@ import ( dbtest "code.superseriousbusiness.org/gotosocial/internal/db/test" "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/visibility" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/log" @@ -53,7 +54,8 @@ type ConversationsTestSuite struct { federator *federation.Federator emailSender email.Sender sentEmails map[string]string - filter *visibility.Filter + visFilter *visibility.Filter + muteFilter *mutes.Filter // standard suite models testTokens map[string]*gtsmodel.Token @@ -104,7 +106,8 @@ func (suite *ConversationsTestSuite) SetupTest() { suite.state.DB = suite.db suite.state.AdminActions = admin.New(suite.state.DB, &suite.state.Workers) suite.tc = typeutils.NewConverter(&suite.state) - suite.filter = visibility.NewFilter(&suite.state) + suite.visFilter = visibility.NewFilter(&suite.state) + suite.muteFilter = mutes.NewFilter(&suite.state) suite.storage = testrig.NewInMemoryStorage() suite.state.Storage = suite.storage @@ -115,7 +118,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.filter) + suite.conversationsProcessor = conversations.New(&suite.state, suite.tc, suite.visFilter, suite.muteFilter) 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 9218942bf..5324466c9 100644 --- a/internal/processing/conversations/get.go +++ b/internal/processing/conversations/get.go @@ -64,23 +64,20 @@ func (p *Processor) GetAll( items := make([]interface{}, 0, count) - filters, mutes, errWithCode := p.getFiltersAndMutes(ctx, requestingAccount) + filters, errWithCode := p.getFilters(ctx, requestingAccount) if errWithCode != nil { return nil, errWithCode } for _, conversation := range conversations { // Convert conversation to frontend API model. - apiConversation, err := p.converter.ConversationToAPIConversation( - ctx, + apiConversation, err := p.converter.ConversationToAPIConversation(ctx, conversation, requestingAccount, filters, - mutes, ) if err != nil { - log.Errorf( - ctx, + log.Errorf(ctx, "error converting conversation %s to API representation: %v", conversation.ID, err, diff --git a/internal/processing/conversations/read.go b/internal/processing/conversations/read.go index 42f369582..4d16a4eeb 100644 --- a/internal/processing/conversations/read.go +++ b/internal/processing/conversations/read.go @@ -44,17 +44,15 @@ func (p *Processor) Read( return nil, gtserror.NewErrorInternalError(err) } - filters, mutes, errWithCode := p.getFiltersAndMutes(ctx, requestingAccount) + filters, errWithCode := p.getFilters(ctx, requestingAccount) if errWithCode != nil { return nil, errWithCode } - apiConversation, err := p.converter.ConversationToAPIConversation( - ctx, + apiConversation, err := p.converter.ConversationToAPIConversation(ctx, conversation, requestingAccount, filters, - mutes, ) if err != nil { err = gtserror.Newf("error converting conversation %s to API representation: %w", id, err) diff --git a/internal/processing/conversations/update.go b/internal/processing/conversations/update.go index 1651d5367..e4024a24a 100644 --- a/internal/processing/conversations/update.go +++ b/internal/processing/conversations/update.go @@ -31,10 +31,14 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/util" ) -// ConversationNotification carries the arguments to processing/stream.Processor.Conversation. +// ConversationNotification carries the arguments +// to processing/stream.Processor.Conversation. type ConversationNotification struct { - // AccountID of a local account to deliver the notification to. + + // AccountID of a local account to + // deliver the notification to. AccountID string + // Conversation as the notification payload. Conversation *apimodel.Conversation } @@ -46,11 +50,13 @@ func (p *Processor) UpdateConversationsForStatus(ctx context.Context, status *gt // Only DMs are considered part of conversations. return nil, nil } + if status.BoostOfID != "" { // Boosts can't be part of conversations. // FUTURE: This may change if we ever implement quote posts. return nil, nil } + if status.ThreadID == "" { // If the status doesn't have a thread ID, it didn't mention a local account, // and thus can't be part of a conversation. @@ -77,51 +83,15 @@ func (p *Processor) UpdateConversationsForStatus(ctx context.Context, status *gt } localAccount := participant - // If the status is not visible to this account, skip processing it for this account. - visible, err := p.filter.StatusVisible(ctx, localAccount, status) + // If status not visible to this account, skip further processing. + visible, err := p.visFilter.StatusVisible(ctx, localAccount, status) if err != nil { - log.Errorf( - ctx, - "error checking status %s visibility for account %s: %v", - status.ID, - localAccount.ID, - err, - ) + log.Errorf(ctx, "error checking status %s visibility for account %s: %v", status.URI, localAccount.URI, err) continue } else if !visible { continue } - // Is the status filtered or muted for this user? - // Converting the status to an API status runs the filter/mute checks. - filters, mutes, errWithCode := p.getFiltersAndMutes(ctx, localAccount) - if errWithCode != nil { - log.Error(ctx, errWithCode) - continue - } - _, err = p.converter.StatusToAPIStatus( - ctx, - status, - localAccount, - statusfilter.FilterContextNotifications, - filters, - mutes, - ) - if err != nil { - // If the status matched a hide filter, skip processing it for this account. - // If there was another kind of error, log that and skip it anyway. - if !errors.Is(err, statusfilter.ErrHideStatus) { - log.Errorf( - ctx, - "error checking status %s filtering/muting for account %s: %v", - status.ID, - localAccount.ID, - err, - ) - } - continue - } - // Collect other accounts participating in the conversation. otherAccounts := make([]*gtsmodel.Account, 0, len(allParticipantsSet)-1) otherAccountIDs := make([]string, 0, len(allParticipantsSet)-1) @@ -133,20 +103,14 @@ func (p *Processor) UpdateConversationsForStatus(ctx context.Context, status *gt } // Check for a previously existing conversation, if there is one. - conversation, err := p.state.DB.GetConversationByThreadAndAccountIDs( - ctx, + conversation, err := p.state.DB.GetConversationByThreadAndAccountIDs(ctx, status.ThreadID, localAccount.ID, otherAccountIDs, ) if err != nil && !errors.Is(err, db.ErrNoEntries) { - log.Errorf( - ctx, - "error trying to find a previous conversation for status %s and account %s: %v", - status.ID, - localAccount.ID, - err, - ) + log.Errorf(ctx, "error finding previous conversation for status %s and account %s: %v", + status.URI, localAccount.URI, err) continue } @@ -172,6 +136,7 @@ func (p *Processor) UpdateConversationsForStatus(ctx context.Context, status *gt conversation.LastStatusID = status.ID conversation.LastStatus = status } + // If the conversation is unread, leave it marked as unread. // If the conversation is read but this status might not have been, mark the conversation as unread. if !statusAuthoredByConversationOwner { @@ -181,43 +146,29 @@ func (p *Processor) UpdateConversationsForStatus(ctx context.Context, status *gt // Create or update the conversation. err = p.state.DB.UpsertConversation(ctx, conversation) if err != nil { - log.Errorf( - ctx, - "error creating or updating conversation %s for status %s and account %s: %v", - conversation.ID, - status.ID, - localAccount.ID, - err, - ) + log.Errorf(ctx, "error creating or updating conversation %s for status %s and account %s: %v", + conversation.ID, status.URI, localAccount.URI, err) continue } // Link the conversation to the status. if err := p.state.DB.LinkConversationToStatus(ctx, conversation.ID, status.ID); err != nil { - log.Errorf( - ctx, - "error linking conversation %s to status %s: %v", - conversation.ID, - status.ID, - err, - ) + log.Errorf(ctx, "error linking conversation %s to status %s: %v", + conversation.ID, status.URI, err) continue } // Convert the conversation to API representation. - apiConversation, err := p.converter.ConversationToAPIConversation( - ctx, + apiConversation, err := p.converter.ConversationToAPIConversation(ctx, conversation, localAccount, - filters, - mutes, + 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, statusfilter.ErrHideStatus) { - log.Errorf( - ctx, + log.Errorf(ctx, "error converting conversation %s to API representation for account %s: %v", status.ID, localAccount.ID, @@ -227,15 +178,31 @@ func (p *Processor) UpdateConversationsForStatus(ctx context.Context, status *gt continue } - // Generate a notification, - // unless the status was authored by the user who would be notified, - // in which case they already know. - if status.AccountID != localAccount.ID { - notifications = append(notifications, ConversationNotification{ - AccountID: localAccount.ID, - Conversation: apiConversation, - }) + // If status was authored by this participant, + // don't bother notifying, they already know! + if status.AccountID == localAccount.ID { + continue } + + // Check whether status is muted to local participant. + muted, err := p.muteFilter.StatusNotificationsMuted(ctx, + localAccount, + status, + ) + if err != nil { + log.Errorf(ctx, "error checking status mute: %v", err) + continue + } + + if muted { + continue + } + + // Generate a notification, + notifications = append(notifications, ConversationNotification{ + AccountID: localAccount.ID, + Conversation: apiConversation, + }) } return notifications, nil diff --git a/internal/processing/media/media_test.go b/internal/processing/media/media_test.go index b074ae768..f2462a972 100644 --- a/internal/processing/media/media_test.go +++ b/internal/processing/media/media_test.go @@ -20,6 +20,7 @@ package media_test 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/visibility" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/media" @@ -82,8 +83,9 @@ func (suite *MediaStandardTestSuite) SetupTest() { suite.transportController = testrig.NewTestTransportController(&suite.state, testrig.NewMockHTTPClient(nil, "../../../testrig/media")) federator := testrig.NewTestFederator(&suite.state, suite.transportController, suite.mediaManager) - filter := visibility.NewFilter(&suite.state) - common := common.New(&suite.state, suite.mediaManager, suite.tc, federator, filter) + visFilter := visibility.NewFilter(&suite.state) + muteFilter := mutes.NewFilter(&suite.state) + common := common.New(&suite.state, suite.mediaManager, suite.tc, federator, visFilter, muteFilter) 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 f7c0f9f02..848c3f169 100644 --- a/internal/processing/polls/poll_test.go +++ b/internal/processing/polls/poll_test.go @@ -24,6 +24,7 @@ import ( "testing" apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model" + "code.superseriousbusiness.org/gotosocial/internal/filter/mutes" "code.superseriousbusiness.org/gotosocial/internal/filter/visibility" "code.superseriousbusiness.org/gotosocial/internal/gtserror" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" @@ -38,9 +39,10 @@ import ( type PollTestSuite struct { suite.Suite - state state.State - filter *visibility.Filter - polls polls.Processor + state state.State + visFilter *visibility.Filter + muteFilter *mutes.Filter + polls polls.Processor testAccounts map[string]*gtsmodel.Account testPolls map[string]*gtsmodel.Poll @@ -56,8 +58,9 @@ func (suite *PollTestSuite) SetupTest() { controller := testrig.NewTestTransportController(&suite.state, nil) mediaMgr := media.NewManager(&suite.state) federator := testrig.NewTestFederator(&suite.state, controller, mediaMgr) - suite.filter = visibility.NewFilter(&suite.state) - common := common.New(&suite.state, mediaMgr, converter, federator, suite.filter) + suite.visFilter = visibility.NewFilter(&suite.state) + suite.muteFilter = mutes.NewFilter(&suite.state) + common := common.New(&suite.state, mediaMgr, converter, federator, suite.visFilter, suite.muteFilter) suite.polls = polls.New(&common, &suite.state, converter) } @@ -88,7 +91,7 @@ func (suite *PollTestSuite) testPollGet(ctx context.Context, requester *gtsmodel var check func(*apimodel.Poll, gtserror.WithCode) bool switch { - case !pollIsVisible(suite.filter, ctx, requester, poll): + case !pollIsVisible(suite.visFilter, ctx, requester, poll): // Poll should not be visible to requester, this should // return an error code 404 (to prevent info leak). check = func(poll *apimodel.Poll, err gtserror.WithCode) bool { @@ -188,7 +191,7 @@ func (suite *PollTestSuite) testPollVote(ctx context.Context, requester *gtsmode return poll == nil && err.Code() == http.StatusUnprocessableEntity } - case !pollIsVisible(suite.filter, ctx, requester, poll): + case !pollIsVisible(suite.visFilter, ctx, requester, poll): // Poll should not be visible to requester, this should // return an error code 404 (to prevent info leak). check = func(poll *apimodel.Poll, err gtserror.WithCode) bool { diff --git a/internal/processing/processor.go b/internal/processing/processor.go index 63e996b38..b8adb9bb8 100644 --- a/internal/processing/processor.go +++ b/internal/processing/processor.go @@ -22,6 +22,7 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/email" "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/visibility" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" mm "code.superseriousbusiness.org/gotosocial/internal/media" @@ -203,6 +204,7 @@ func NewProcessor( emailSender email.Sender, webPushSender webpush.Sender, visFilter *visibility.Filter, + muteFilter *mutes.Filter, intFilter *interaction.Filter, ) *Processor { parseMentionFunc := GetParseMentionFunc(state, federator) @@ -218,7 +220,7 @@ func NewProcessor( // // Start with sub processors that will // be required by the workers processor. - common := common.New(state, mediaManager, converter, federator, visFilter) + common := common.New(state, mediaManager, converter, federator, visFilter, muteFilter) processor.account = account.New(&common, state, converter, mediaManager, federator, visFilter, parseMentionFunc) processor.media = media.New(&common, state, converter, federator, mediaManager, federator.TransportController()) processor.stream = stream.New(state, oauthServer) @@ -228,7 +230,7 @@ func NewProcessor( processor.account = account.New(&common, state, converter, mediaManager, federator, visFilter, 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) + processor.conversations = conversations.New(state, converter, visFilter, muteFilter) processor.fedi = fedi.New(state, &common, converter, federator, visFilter) processor.filtersv1 = filtersv1.New(state, converter, &processor.stream) processor.filtersv2 = filtersv2.New(state, converter, &processor.stream) @@ -239,7 +241,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) + processor.timeline = timeline.New(state, converter, visFilter, muteFilter) 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) @@ -256,6 +258,7 @@ func NewProcessor( federator, converter, visFilter, + muteFilter, emailSender, webPushSender, &processor.account, diff --git a/internal/processing/processor_test.go b/internal/processing/processor_test.go index 6f5e79afd..847de29cf 100644 --- a/internal/processing/processor_test.go +++ b/internal/processing/processor_test.go @@ -27,6 +27,7 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/email" "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/visibility" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/media" @@ -130,6 +131,7 @@ func (suite *ProcessingStandardTestSuite) SetupTest() { suite.emailSender, testrig.NewNoopWebPushSender(), visibility.NewFilter(&suite.state), + mutes.NewFilter(&suite.state), interaction.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 5d8e34960..97eb813db 100644 --- a/internal/processing/search/util.go +++ b/internal/processing/search/util.go @@ -114,7 +114,7 @@ func (p *Processor) packageStatuses( continue } - apiStatus, err := p.converter.StatusToAPIStatus(ctx, status, requestingAccount, statusfilter.FilterContextNone, nil, nil) + apiStatus, err := p.converter.StatusToAPIStatus(ctx, status, requestingAccount, statusfilter.FilterContextNone, nil) if err != nil { log.Debugf(ctx, "skipping status %s because it couldn't be converted to its api representation: %s", status.ID, err) continue diff --git a/internal/processing/status/context.go b/internal/processing/status/context.go index c18a4f7bd..6f3e7a4fd 100644 --- a/internal/processing/status/context.go +++ b/internal/processing/status/context.go @@ -25,7 +25,6 @@ import ( apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model" statusfilter "code.superseriousbusiness.org/gotosocial/internal/filter/status" - "code.superseriousbusiness.org/gotosocial/internal/gtscontext" "code.superseriousbusiness.org/gotosocial/internal/gtserror" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" ) @@ -291,26 +290,8 @@ func (p *Processor) ContextGet( return nil, gtserror.NewErrorInternalError(err) } - // Retrieve mutes as they affect - // what should be shown to requester. - mutes, err := p.state.DB.GetAccountMutes( - // No need to populate mutes, - // IDs are enough here. - gtscontext.SetBarebones(ctx), - requester.ID, - nil, // No paging - get all. - ) - if err != nil { - err = gtserror.Newf( - "couldn't retrieve mutes for account %s: %w", - requester.ID, err, - ) - return nil, gtserror.NewErrorInternalError(err) - } - // Retrieve the full thread context. - threadContext, errWithCode := p.contextGet( - ctx, + threadContext, errWithCode := p.contextGet(ctx, requester, targetStatusID, ) @@ -326,7 +307,6 @@ func (p *Processor) ContextGet( threadContext.ancestors, statusfilter.FilterContextThread, filters, - mutes, ) // Convert and filter the thread context descendants @@ -335,7 +315,6 @@ func (p *Processor) ContextGet( threadContext.descendants, statusfilter.FilterContextThread, filters, - mutes, ) return &apiContext, nil @@ -352,8 +331,8 @@ func (p *Processor) WebContextGet( targetStatusID string, ) (*apimodel.WebThreadContext, gtserror.WithCode) { // Retrieve the internal thread context. - iCtx, errWithCode := p.contextGet( - ctx, + iCtx, errWithCode := p.contextGet(ctx, + nil, // No authed requester. targetStatusID, ) diff --git a/internal/processing/status/status_test.go b/internal/processing/status/status_test.go index 75775cf8a..091d9716b 100644 --- a/internal/processing/status/status_test.go +++ b/internal/processing/status/status_test.go @@ -22,6 +22,7 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/db" "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/visibility" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/media" @@ -92,9 +93,10 @@ func (suite *StatusStandardTestSuite) SetupTest() { suite.federator = testrig.NewTestFederator(&suite.state, suite.tc, suite.mediaManager) visFilter := visibility.NewFilter(&suite.state) + muteFilter := mutes.NewFilter(&suite.state) intFilter := interaction.NewFilter(&suite.state) - common := common.New(&suite.state, suite.mediaManager, suite.typeConverter, suite.federator, visFilter) + common := common.New(&suite.state, suite.mediaManager, suite.typeConverter, suite.federator, visFilter, muteFilter) 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 483388823..8fc4bcfe8 100644 --- a/internal/processing/stream/statusupdate_test.go +++ b/internal/processing/stream/statusupdate_test.go @@ -39,7 +39,7 @@ func (suite *StatusUpdateTestSuite) TestStreamNotification() { suite.NoError(errWithCode) editedStatus := suite.testStatuses["remote_account_1_status_1"] - apiStatus, err := typeutils.NewConverter(&suite.state).StatusToAPIStatus(suite.T().Context(), editedStatus, account, statusfilter.FilterContextNotifications, nil, nil) + apiStatus, err := typeutils.NewConverter(&suite.state).StatusToAPIStatus(suite.T().Context(), editedStatus, account, statusfilter.FilterContextNotifications, nil) suite.NoError(err) suite.streamProcessor.StatusUpdate(suite.T().Context(), account, apiStatus, stream.TimelineHome) diff --git a/internal/processing/timeline/faved.go b/internal/processing/timeline/faved.go index 4cb3c30a5..84788a8fa 100644 --- a/internal/processing/timeline/faved.go +++ b/internal/processing/timeline/faved.go @@ -56,7 +56,7 @@ func (p *Processor) FavedTimelineGet(ctx context.Context, authed *apiutil.Auth, continue } - apiStatus, err := p.converter.StatusToAPIStatus(ctx, s, authed.Account, statusfilter.FilterContextNone, nil, nil) + apiStatus, err := p.converter.StatusToAPIStatus(ctx, s, authed.Account, statusfilter.FilterContextNone, nil) if err != nil { log.Errorf(ctx, "error convering to api status: %v", err) continue diff --git a/internal/processing/timeline/home.go b/internal/processing/timeline/home.go index bcb63fcff..ba74b770c 100644 --- a/internal/processing/timeline/home.go +++ b/internal/processing/timeline/home.go @@ -91,9 +91,22 @@ func (p *Processor) HomeTimelineGet( // Check the visibility of passed status to requesting user. ok, err := p.visFilter.StatusHomeTimelineable(ctx, requester, s) if err != nil { - log.Errorf(ctx, "error filtering status %s: %v", s.URI, err) + log.Errorf(ctx, "error checking status %s visibility: %v", s.URI, err) + return true // default assume not visible + } else if !ok { + return true } - return !ok + + // Check if status been muted by requester from timelines. + muted, err := p.muteFilter.StatusMuted(ctx, requester, s) + if err != nil { + log.Errorf(ctx, "error checking status %s mutes: %v", s.URI, err) + return true // default assume muted + } else if muted { + return true + } + + return false }, // Post filtering funtion, diff --git a/internal/processing/timeline/list.go b/internal/processing/timeline/list.go index dbf07cdd4..c8e6bc5f1 100644 --- a/internal/processing/timeline/list.go +++ b/internal/processing/timeline/list.go @@ -102,9 +102,22 @@ func (p *Processor) ListTimelineGet( // Check the visibility of passed status to requesting user. ok, err := p.visFilter.StatusHomeTimelineable(ctx, requester, s) if err != nil { - log.Errorf(ctx, "error filtering status %s: %v", s.URI, err) + log.Errorf(ctx, "error checking status %s visibility: %v", s.URI, err) + return true // default assume not visible + } else if !ok { + return true } - return !ok + + // Check if status been muted by requester from timelines. + muted, err := p.muteFilter.StatusMuted(ctx, requester, s) + if err != nil { + log.Errorf(ctx, "error checking status %s mutes: %v", s.URI, err) + return true // default assume muted + } else if muted { + return true + } + + return false }, // Post filtering funtion, diff --git a/internal/processing/timeline/notification.go b/internal/processing/timeline/notification.go index 0da9fb55e..ad60fd90c 100644 --- a/internal/processing/timeline/notification.go +++ b/internal/processing/timeline/notification.go @@ -21,14 +21,13 @@ import ( "context" "errors" "fmt" + "net/http" "net/url" apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model" apiutil "code.superseriousbusiness.org/gotosocial/internal/api/util" "code.superseriousbusiness.org/gotosocial/internal/db" "code.superseriousbusiness.org/gotosocial/internal/filter/status" - "code.superseriousbusiness.org/gotosocial/internal/filter/usermute" - "code.superseriousbusiness.org/gotosocial/internal/gtscontext" "code.superseriousbusiness.org/gotosocial/internal/gtserror" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/log" @@ -39,14 +38,13 @@ import ( // NotificationsGet ... func (p *Processor) NotificationsGet( ctx context.Context, - authed *apiutil.Auth, + requester *gtsmodel.Account, page *paging.Page, types []gtsmodel.NotificationType, excludeTypes []gtsmodel.NotificationType, ) (*apimodel.PageableResponse, gtserror.WithCode) { - notifs, err := p.state.DB.GetAccountNotifications( - ctx, - authed.Account.ID, + notifs, err := p.state.DB.GetAccountNotifications(ctx, + requester.ID, page, types, excludeTypes, @@ -61,19 +59,12 @@ func (p *Processor) NotificationsGet( return util.EmptyPageableResponse(), nil } - filters, err := p.state.DB.GetFiltersForAccountID(ctx, authed.Account.ID) + filters, err := p.state.DB.GetFiltersForAccountID(ctx, requester.ID) if err != nil { - err = gtserror.Newf("couldn't retrieve filters for account %s: %w", authed.Account.ID, err) + err = gtserror.Newf("error getting account %s filters: %w", requester.ID, err) return nil, gtserror.NewErrorInternalError(err) } - mutes, err := p.state.DB.GetAccountMutes(gtscontext.SetBarebones(ctx), authed.Account.ID, nil) - if err != nil { - err = gtserror.Newf("couldn't retrieve mutes for account %s: %w", authed.Account.ID, err) - return nil, gtserror.NewErrorInternalError(err) - } - compiledMutes := usermute.NewCompiledUserMuteList(mutes) - var ( items = make([]interface{}, 0, count) @@ -84,7 +75,7 @@ func (p *Processor) NotificationsGet( ) for _, n := range notifs { - visible, err := p.notifVisible(ctx, n, authed.Account) + visible, err := p.notifVisible(ctx, n, requester) if err != nil { log.Debugf(ctx, "skipping notification %s because of an error checking notification visibility: %v", n.ID, err) continue @@ -94,7 +85,37 @@ func (p *Processor) NotificationsGet( continue } - item, err := p.converter.NotificationToAPINotification(ctx, n, filters, compiledMutes) + // Check whether notification origin account is muted. + muted, err := p.muteFilter.AccountNotificationsMuted(ctx, + requester, + n.OriginAccount, + ) + if err != nil { + log.Errorf(ctx, "error checking account mute: %v", err) + continue + } + + if muted { + continue + } + + if n.Status != nil { + // A status is attached, check whether status muted. + muted, err = p.muteFilter.StatusNotificationsMuted(ctx, + requester, + n.Status, + ) + if err != nil { + log.Errorf(ctx, "error checking status mute: %v", err) + continue + } + + if muted { + continue + } + } + + item, err := p.converter.NotificationToAPINotification(ctx, n, filters) if err != nil { if !errors.Is(err, status.ErrHideStatus) { log.Debugf(ctx, "skipping notification %s because it couldn't be converted to its api representation: %s", n.ID, err) @@ -125,41 +146,24 @@ func (p *Processor) NotificationsGet( func (p *Processor) NotificationGet(ctx context.Context, account *gtsmodel.Account, targetNotifID string) (*apimodel.Notification, gtserror.WithCode) { notif, err := p.state.DB.GetNotificationByID(ctx, targetNotifID) - if err != nil { - if errors.Is(err, db.ErrNoEntries) { - return nil, gtserror.NewErrorNotFound(err) - } - - // Real error. + if err != nil && !errors.Is(err, db.ErrNoEntries) { + err := gtserror.Newf("error getting from db: %w", err) return nil, gtserror.NewErrorInternalError(err) } - if notifTargetAccountID := notif.TargetAccountID; notifTargetAccountID != account.ID { - err = fmt.Errorf("account %s does not have permission to view notification belong to account %s", account.ID, notifTargetAccountID) + if notif.TargetAccountID != account.ID { + err := gtserror.New("requester does not match notification target") return nil, gtserror.NewErrorNotFound(err) } - filters, err := p.state.DB.GetFiltersForAccountID(ctx, account.ID) - if err != nil { - err = gtserror.Newf("couldn't retrieve filters for account %s: %w", account.ID, err) - return nil, gtserror.NewErrorInternalError(err) - } + // NOTE: we specifically don't do any filtering + // or mute checking for a notification directly + // fetched by ID. only from timelines etc. - mutes, err := p.state.DB.GetAccountMutes(gtscontext.SetBarebones(ctx), account.ID, nil) + apiNotif, err := p.converter.NotificationToAPINotification(ctx, notif, nil) if err != nil { - err = gtserror.Newf("couldn't retrieve mutes for account %s: %w", account.ID, err) - return nil, gtserror.NewErrorInternalError(err) - } - compiledMutes := usermute.NewCompiledUserMuteList(mutes) - - apiNotif, err := p.converter.NotificationToAPINotification(ctx, notif, filters, compiledMutes) - if err != nil { - if errors.Is(err, db.ErrNoEntries) { - return nil, gtserror.NewErrorNotFound(err) - } - - // Real error. - return nil, gtserror.NewErrorInternalError(err) + err := gtserror.Newf("error converting to api model: %w", err) + return nil, gtserror.WrapWithCode(http.StatusInternalServerError, err) } return apiNotif, nil diff --git a/internal/processing/timeline/public.go b/internal/processing/timeline/public.go index 527000166..cfb58201d 100644 --- a/internal/processing/timeline/public.go +++ b/internal/processing/timeline/public.go @@ -93,9 +93,22 @@ func (p *Processor) publicTimelineGet( // Check the visibility of passed status to requesting user. ok, err := p.visFilter.StatusPublicTimelineable(ctx, requester, s) if err != nil { - log.Errorf(ctx, "error filtering status %s: %v", s.URI, err) + log.Errorf(ctx, "error checking status %s visibility: %v", s.URI, err) + return true // default assume not visible + } else if !ok { + return true } - return !ok + + // Check if status been muted by requester from timelines. + muted, err := p.muteFilter.StatusMuted(ctx, requester, s) + if err != nil { + log.Errorf(ctx, "error checking status %s mutes: %v", s.URI, err) + return true // default assume muted + } else if muted { + return true + } + + return false }, // Post filtering funtion, @@ -149,9 +162,20 @@ func (p *Processor) localTimelineGet( // Check the visibility of passed status to requesting user. ok, err := p.visFilter.StatusPublicTimelineable(ctx, requester, s) if err != nil { - log.Errorf(ctx, "error filtering status %s: %v", s.URI, err) + log.Errorf(ctx, "error checking status %s visibility: %v", s.URI, err) + } else if !ok { + return true } - return !ok + + // Check if status been muted by requester from timelines. + muted, err := p.muteFilter.StatusMuted(ctx, requester, s) + if err != nil { + log.Errorf(ctx, "error checking status %s mutes: %v", s.URI, err) + } else if muted { + return true + } + + return false }, // Post filtering funtion, diff --git a/internal/processing/timeline/tag.go b/internal/processing/timeline/tag.go index f48f89049..88333d343 100644 --- a/internal/processing/timeline/tag.go +++ b/internal/processing/timeline/tag.go @@ -101,9 +101,22 @@ func (p *Processor) TagTimelineGet( // Check the visibility of passed status to requesting user. ok, err := p.visFilter.StatusPublicTimelineable(ctx, requester, s) if err != nil { - log.Errorf(ctx, "error filtering status %s: %v", s.URI, err) + log.Errorf(ctx, "error checking status %s visibility: %v", s.URI, err) + return true // default assume not visible + } else if !ok { + return true } - return !ok + + // Check if status been muted by requester from timelines. + muted, err := p.muteFilter.StatusMuted(ctx, requester, s) + if err != nil { + log.Errorf(ctx, "error checking status %s mutes: %v", s.URI, err) + return true // default assume muted + } else if muted { + return true + } + + return false }, // Post filtering funtion, diff --git a/internal/processing/timeline/timeline.go b/internal/processing/timeline/timeline.go index 2eab57195..a86702d42 100644 --- a/internal/processing/timeline/timeline.go +++ b/internal/processing/timeline/timeline.go @@ -26,8 +26,8 @@ import ( apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model" timelinepkg "code.superseriousbusiness.org/gotosocial/internal/cache/timeline" "code.superseriousbusiness.org/gotosocial/internal/db" + "code.superseriousbusiness.org/gotosocial/internal/filter/mutes" statusfilter "code.superseriousbusiness.org/gotosocial/internal/filter/status" - "code.superseriousbusiness.org/gotosocial/internal/filter/usermute" "code.superseriousbusiness.org/gotosocial/internal/filter/visibility" "code.superseriousbusiness.org/gotosocial/internal/gtserror" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" @@ -48,16 +48,18 @@ var ( ) type Processor struct { - state *state.State - converter *typeutils.Converter - visFilter *visibility.Filter + state *state.State + converter *typeutils.Converter + visFilter *visibility.Filter + muteFilter *mutes.Filter } -func New(state *state.State, converter *typeutils.Converter, visFilter *visibility.Filter) Processor { +func New(state *state.State, converter *typeutils.Converter, visFilter *visibility.Filter, muteFilter *mutes.Filter) Processor { return Processor{ - state: state, - converter: converter, - visFilter: visFilter, + state: state, + converter: converter, + visFilter: visFilter, + muteFilter: muteFilter, } } @@ -78,7 +80,6 @@ func (p *Processor) getStatusTimeline( ) { var err error var filters []*gtsmodel.Filter - var mutes *usermute.CompiledUserMuteList if requester != nil { // Fetch all filters relevant for requesting account. @@ -89,19 +90,6 @@ func (p *Processor) getStatusTimeline( err := gtserror.Newf("error getting account filters: %w", err) return nil, gtserror.NewErrorInternalError(err) } - - // Get a list of all account mutes for requester. - allMutes, err := p.state.DB.GetAccountMutes(ctx, - requester.ID, - nil, // i.e. all - ) - if err != nil && !errors.Is(err, db.ErrNoEntries) { - err := gtserror.Newf("error getting account mutes: %w", err) - return nil, gtserror.NewErrorInternalError(err) - } - - // Compile all account mutes to useable form. - mutes = usermute.NewCompiledUserMuteList(allMutes) } // Ensure we have valid @@ -148,7 +136,6 @@ func (p *Processor) getStatusTimeline( requester, filterCtx, filters, - mutes, ) if err != nil && !errors.Is(err, statusfilter.ErrHideStatus) { return nil, err diff --git a/internal/processing/timeline/timeline_test.go b/internal/processing/timeline/timeline_test.go index 5afbb2353..01197b767 100644 --- a/internal/processing/timeline/timeline_test.go +++ b/internal/processing/timeline/timeline_test.go @@ -20,6 +20,7 @@ package timeline_test 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/visibility" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/processing/timeline" @@ -62,6 +63,7 @@ func (suite *TimelineStandardTestSuite) SetupTest() { &suite.state, typeutils.NewConverter(&suite.state), visibility.NewFilter(&suite.state), + mutes.NewFilter(&suite.state), ) testrig.StandardDBSetup(suite.db, suite.testAccounts) diff --git a/internal/processing/workers/fromclientapi.go b/internal/processing/workers/fromclientapi.go index 5d9ebf41a..04ad4152c 100644 --- a/internal/processing/workers/fromclientapi.go +++ b/internal/processing/workers/fromclientapi.go @@ -748,10 +748,17 @@ func (p *clientAPI) UpdateStatus(ctx context.Context, cMsg *messages.FromClientA } } - // Notify of the latest edit. - if editsLen := len(status.EditIDs); editsLen != 0 { - editID := status.EditIDs[editsLen-1] - if err := p.surface.notifyStatusEdit(ctx, status, editID); err != nil { + if len(status.EditIDs) > 0 { + // Ensure edits are fully populated for this status before anything. + if err := p.surface.State.DB.PopulateStatusEdits(ctx, status); err != nil { + log.Error(ctx, "error populating updated status edits: %v") + + // Then send notifications of a status edit + // to any local interactors of the status. + } else if err := p.surface.notifyStatusEdit(ctx, + status, + status.Edits[len(status.Edits)-1], // latest + ); err != nil { log.Errorf(ctx, "error notifying status edit: %v", err) } } diff --git a/internal/processing/workers/fromclientapi_test.go b/internal/processing/workers/fromclientapi_test.go index 3abd05295..3f6964259 100644 --- a/internal/processing/workers/fromclientapi_test.go +++ b/internal/processing/workers/fromclientapi_test.go @@ -215,7 +215,6 @@ func (suite *FromClientAPITestSuite) statusJSON( requestingAccount, statusfilter.FilterContextNone, nil, - nil, ) if err != nil { suite.FailNow(err.Error()) @@ -240,7 +239,6 @@ func (suite *FromClientAPITestSuite) conversationJSON( conversation, requestingAccount, nil, - nil, ) if err != nil { suite.FailNow(err.Error()) @@ -348,7 +346,7 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithNotification() { suite.FailNow("timed out waiting for new status notification") } - apiNotif, err := testStructs.TypeConverter.NotificationToAPINotification(ctx, notif, nil, nil) + apiNotif, err := testStructs.TypeConverter.NotificationToAPINotification(ctx, notif, nil) if err != nil { suite.FailNow(err.Error()) } @@ -2035,7 +2033,7 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithAuthorOnExclusiv suite.FailNow("timed out waiting for new status notification") } - apiNotif, err := testStructs.TypeConverter.NotificationToAPINotification(ctx, notif, nil, nil) + apiNotif, err := testStructs.TypeConverter.NotificationToAPINotification(ctx, notif, nil) if err != nil { suite.FailNow(err.Error()) } @@ -2220,7 +2218,7 @@ func (suite *FromClientAPITestSuite) TestProcessUpdateStatusInteractedWith() { suite.FailNow("timed out waiting for edited status notification") } - apiNotif, err := testStructs.TypeConverter.NotificationToAPINotification(ctx, notif, nil, nil) + apiNotif, err := testStructs.TypeConverter.NotificationToAPINotification(ctx, notif, nil) if err != nil { suite.FailNow(err.Error()) } diff --git a/internal/processing/workers/fromfediapi.go b/internal/processing/workers/fromfediapi.go index d1e5bb2f7..5dbb8ba2e 100644 --- a/internal/processing/workers/fromfediapi.go +++ b/internal/processing/workers/fromfediapi.go @@ -1010,10 +1010,17 @@ func (p *fediAPI) UpdateStatus(ctx context.Context, fMsg *messages.FromFediAPI) } } - // Notify of the latest edit. - if editsLen := len(status.EditIDs); editsLen != 0 { - editID := status.EditIDs[editsLen-1] - if err := p.surface.notifyStatusEdit(ctx, status, editID); err != nil { + if len(status.EditIDs) > 0 { + // Ensure edits are fully populated for this status before anything. + if err := p.surface.State.DB.PopulateStatusEdits(ctx, status); err != nil { + log.Error(ctx, "error populating updated status edits: %v") + + // Then send notifications of a status edit + // to any local interactors of the status. + } else if err := p.surface.notifyStatusEdit(ctx, + status, + status.Edits[len(status.Edits)-1], // latest + ); err != nil { log.Errorf(ctx, "error notifying status edit: %v", err) } } diff --git a/internal/processing/workers/surface.go b/internal/processing/workers/surface.go index 5604ad71d..69758692f 100644 --- a/internal/processing/workers/surface.go +++ b/internal/processing/workers/surface.go @@ -19,6 +19,7 @@ package workers import ( "code.superseriousbusiness.org/gotosocial/internal/email" + "code.superseriousbusiness.org/gotosocial/internal/filter/mutes" "code.superseriousbusiness.org/gotosocial/internal/filter/visibility" "code.superseriousbusiness.org/gotosocial/internal/processing/conversations" "code.superseriousbusiness.org/gotosocial/internal/processing/stream" @@ -38,6 +39,7 @@ type Surface struct { Converter *typeutils.Converter Stream *stream.Processor VisFilter *visibility.Filter + MuteFilter *mutes.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 11c3fd059..044315349 100644 --- a/internal/processing/workers/surfacenotify.go +++ b/internal/processing/workers/surfacenotify.go @@ -23,8 +23,7 @@ import ( "strings" "code.superseriousbusiness.org/gotosocial/internal/db" - "code.superseriousbusiness.org/gotosocial/internal/filter/status" - "code.superseriousbusiness.org/gotosocial/internal/filter/usermute" + statusfilter "code.superseriousbusiness.org/gotosocial/internal/filter/status" "code.superseriousbusiness.org/gotosocial/internal/gtscontext" "code.superseriousbusiness.org/gotosocial/internal/gtserror" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" @@ -59,8 +58,7 @@ func (s *Surface) notifyPendingReply( // Ensure thread not muted // by replied-to account. - muted, err := s.State.DB.IsThreadMutedByAccount( - ctx, + muted, err := s.State.DB.IsThreadMutedByAccount(ctx, status.ThreadID, status.InReplyToAccountID, ) @@ -81,7 +79,8 @@ func (s *Surface) notifyPendingReply( gtsmodel.NotificationPendingReply, status.InReplyToAccount, status.Account, - status.ID, + status, + nil, ); err != nil { return gtserror.Newf("error notifying replied-to account %s: %w", status.InReplyToAccountID, err) } @@ -135,8 +134,7 @@ func (s *Surface) notifyMention( // Ensure thread not muted // by mentioned account. - muted, err := s.State.DB.IsThreadMutedByAccount( - ctx, + muted, err := s.State.DB.IsThreadMutedByAccount(ctx, mention.Status.ThreadID, mention.TargetAccountID, ) @@ -160,7 +158,8 @@ func (s *Surface) notifyMention( gtsmodel.NotificationMention, mention.TargetAccount, mention.OriginAccount, - mention.StatusID, + mention.Status, + nil, ); err != nil { return gtserror.Newf( "error notifying mention target %s: %w", @@ -193,7 +192,8 @@ func (s *Surface) notifyFollowRequest( gtsmodel.NotificationFollowRequest, followReq.TargetAccount, followReq.Account, - "", + nil, + nil, ); err != nil { return gtserror.Newf("error notifying follow target %s: %w", followReq.TargetAccountID, err) } @@ -245,7 +245,8 @@ func (s *Surface) notifyFollow( gtsmodel.NotificationFollow, follow.TargetAccount, follow.Account, - "", + nil, + nil, ); err != nil { return gtserror.Newf("error notifying follow target %s: %w", follow.TargetAccountID, err) } @@ -275,7 +276,8 @@ func (s *Surface) notifyFave( gtsmodel.NotificationFavourite, fave.TargetAccount, fave.Account, - fave.StatusID, + fave.Status, + nil, ); err != nil { return gtserror.Newf("error notifying status author %s: %w", fave.TargetAccountID, err) } @@ -306,7 +308,8 @@ func (s *Surface) notifyPendingFave( gtsmodel.NotificationPendingFave, fave.TargetAccount, fave.Account, - fave.StatusID, + fave.Status, + nil, ); err != nil { return gtserror.Newf("error notifying status author %s: %w", fave.TargetAccountID, err) } @@ -339,8 +342,7 @@ func (s *Surface) notifyableFave( // Ensure favee hasn't // muted the thread. - muted, err := s.State.DB.IsThreadMutedByAccount( - ctx, + muted, err := s.State.DB.IsThreadMutedByAccount(ctx, fave.Status.ThreadID, fave.TargetAccountID, ) @@ -379,7 +381,8 @@ func (s *Surface) notifyAnnounce( gtsmodel.NotificationReblog, boost.BoostOfAccount, boost.Account, - boost.ID, + boost, + nil, ); err != nil { return gtserror.Newf("error notifying boost target %s: %w", boost.BoostOfAccountID, err) } @@ -410,7 +413,8 @@ func (s *Surface) notifyPendingAnnounce( gtsmodel.NotificationPendingReblog, boost.BoostOfAccount, boost.Account, - boost.ID, + boost, + nil, ); err != nil { return gtserror.Newf("error notifying boost target %s: %w", boost.BoostOfAccountID, err) } @@ -448,8 +452,7 @@ func (s *Surface) notifyableAnnounce( // Ensure boostee hasn't // muted the thread. - muted, err := s.State.DB.IsThreadMutedByAccount( - ctx, + muted, err := s.State.DB.IsThreadMutedByAccount(ctx, status.BoostOf.ThreadID, status.BoostOfAccountID, ) @@ -488,7 +491,8 @@ func (s *Surface) notifyPollClose(ctx context.Context, status *gtsmodel.Status) gtsmodel.NotificationPoll, status.Account, status.Account, - status.ID, + status, + nil, ); err != nil { errs.Appendf("error notifying poll author: %w", err) } @@ -507,7 +511,8 @@ func (s *Surface) notifyPollClose(ctx context.Context, status *gtsmodel.Status) gtsmodel.NotificationPoll, vote.Account, status.Account, - status.ID, + status, + nil, ); err != nil { errs.Appendf("error notifying poll voter %s: %w", vote.AccountID, err) continue @@ -546,7 +551,8 @@ func (s *Surface) notifySignup(ctx context.Context, newUser *gtsmodel.User) erro gtsmodel.NotificationAdminSignup, mod, newUser.Account, - "", + nil, + nil, ); err != nil { errs.Appendf("error notifying moderator %s: %w", mod.ID, err) continue @@ -559,7 +565,7 @@ func (s *Surface) notifySignup(ctx context.Context, newUser *gtsmodel.User) erro func (s *Surface) notifyStatusEdit( ctx context.Context, status *gtsmodel.Status, - editID string, + edit *gtsmodel.StatusEdit, ) error { // Get local-only interactions (we can't/don't notify remotes). interactions, err := s.State.DB.GetStatusInteractions(ctx, status.ID, true) @@ -594,7 +600,8 @@ func (s *Surface) notifyStatusEdit( gtsmodel.NotificationUpdate, targetAcct, status.Account, - editID, + status, + edit, ); err != nil { errs.Appendf("error notifying status edit: %w", err) continue @@ -637,22 +644,32 @@ func (s *Surface) Notify( notificationType gtsmodel.NotificationType, targetAccount *gtsmodel.Account, originAccount *gtsmodel.Account, - statusOrEditID string, + status *gtsmodel.Status, + edit *gtsmodel.StatusEdit, ) error { if targetAccount.IsRemote() { // nothing to do. return nil } + // Get status / edit ID + // if either was provided. + // (prefer edit though!) + var statusOrEditID string + if edit != nil { + statusOrEditID = edit.ID + } else if status != nil { + statusOrEditID = status.ID + } + // We're doing state-y stuff so get a // lock on this combo of notif params. - lockURI := getNotifyLockURI( + unlock := s.State.ProcessingLocks.Lock(getNotifyLockURI( notificationType, targetAccount, originAccount, statusOrEditID, - ) - unlock := s.State.ProcessingLocks.Lock(lockURI) + )) // Wrap the unlock so we // can do granular unlocking. @@ -696,29 +713,57 @@ func (s *Surface) Notify( // with the state-y stuff. unlock() - // Stream notification to the user. - filters, err := s.State.DB.GetFiltersForAccountID(ctx, targetAccount.ID) + // Check whether origin account is muted by target account. + muted, err := s.MuteFilter.AccountNotificationsMuted(ctx, + targetAccount, + originAccount, + ) if err != nil { - return gtserror.Newf("couldn't retrieve filters for account %s: %w", targetAccount.ID, err) + return gtserror.Newf("error checking account mute: %w", err) } - mutes, err := s.State.DB.GetAccountMutes(gtscontext.SetBarebones(ctx), targetAccount.ID, nil) - if err != nil { - return gtserror.Newf("couldn't retrieve mutes for account %s: %w", targetAccount.ID, err) + if muted { + // Don't notify. + return nil } - compiledMutes := usermute.NewCompiledUserMuteList(mutes) - apiNotif, err := s.Converter.NotificationToAPINotification(ctx, notif, filters, compiledMutes) - if err != nil { - if errors.Is(err, status.ErrHideStatus) { + if status != nil { + // Check whether status is muted by the target account. + muted, err := s.MuteFilter.StatusNotificationsMuted(ctx, + targetAccount, + status, + ) + if err != nil { + return gtserror.Newf("error checking status mute: %w", err) + } + + if muted { + // Don't notify. return nil } + } + + filters, err := s.State.DB.GetFiltersForAccountID(ctx, targetAccount.ID) + if err != nil { + return gtserror.Newf("couldn't retrieve filters for account %s: %w", targetAccount.ID, err) + } + + // Convert the notification to frontend API model for streaming / push. + apiNotif, err := s.Converter.NotificationToAPINotification(ctx, notif, filters) + if err != nil && !errors.Is(err, statusfilter.ErrHideStatus) { return gtserror.Newf("error converting notification to api representation: %w", err) } + + if apiNotif == nil { + // Filtered. + return nil + } + + // Stream notification to the user. s.Stream.Notify(ctx, targetAccount, apiNotif) // Send Web Push notification to the user. - if err = s.WebPushSender.Send(ctx, notif, filters, compiledMutes); err != nil { + if err = s.WebPushSender.Send(ctx, notif, apiNotif); err != nil { return gtserror.Newf("error sending Web Push notifications: %w", err) } diff --git a/internal/processing/workers/surfacenotify_test.go b/internal/processing/workers/surfacenotify_test.go index 459b3e125..a5124a3af 100644 --- a/internal/processing/workers/surfacenotify_test.go +++ b/internal/processing/workers/surfacenotify_test.go @@ -22,6 +22,7 @@ import ( "testing" "time" + "code.superseriousbusiness.org/gotosocial/internal/filter/mutes" "code.superseriousbusiness.org/gotosocial/internal/filter/visibility" "code.superseriousbusiness.org/gotosocial/internal/gtscontext" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" @@ -43,6 +44,7 @@ func (suite *SurfaceNotifyTestSuite) TestSpamNotifs() { Converter: testStructs.TypeConverter, Stream: testStructs.Processor.Stream(), VisFilter: visibility.NewFilter(testStructs.State), + MuteFilter: mutes.NewFilter(testStructs.State), EmailSender: testStructs.EmailSender, WebPushSender: testStructs.WebPushSender, Conversations: testStructs.Processor.Conversations(), @@ -74,7 +76,8 @@ func (suite *SurfaceNotifyTestSuite) TestSpamNotifs() { notificationType, targetAccount, originAccount, - "", + nil, + nil, ); err != nil { suite.FailNow(err.Error()) } diff --git a/internal/processing/workers/surfacetimeline.go b/internal/processing/workers/surfacetimeline.go index 018ef976e..7ef5fee87 100644 --- a/internal/processing/workers/surfacetimeline.go +++ b/internal/processing/workers/surfacetimeline.go @@ -23,7 +23,6 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/cache/timeline" statusfilter "code.superseriousbusiness.org/gotosocial/internal/filter/status" - "code.superseriousbusiness.org/gotosocial/internal/filter/usermute" "code.superseriousbusiness.org/gotosocial/internal/gtscontext" "code.superseriousbusiness.org/gotosocial/internal/gtserror" "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" @@ -119,11 +118,12 @@ func (s *Surface) timelineAndNotifyStatusForFollowers( // if something is hometimelineable according to this filter, // it's also eligible to appear in exclusive lists, // even if it ultimately doesn't appear on the home timeline. - timelineable, err := s.VisFilter.StatusHomeTimelineable( - ctx, follow.Account, status, + timelineable, err := s.VisFilter.StatusHomeTimelineable(ctx, + follow.Account, + status, ) if err != nil { - log.Errorf(ctx, "error checking status home visibility for follow: %v", err) + log.Errorf(ctx, "error checking status home visibility: %v", err) continue } @@ -132,9 +132,24 @@ func (s *Surface) timelineAndNotifyStatusForFollowers( continue } - // Get relevant filters and mutes for this follow's account. + // Check if the status is muted by this follower. + muted, err := s.MuteFilter.StatusMuted(ctx, + follow.Account, + status, + ) + if err != nil { + log.Errorf(ctx, "error checking status mute: %v", err) + continue + } + + if muted { + // Nothing to do. + continue + } + + // Get relevant filters for this follow's account. // (note the origin account of the follow is receiver of status). - filters, mutes, err := s.getFiltersAndMutes(ctx, follow.AccountID) + filters, err := s.getFilters(ctx, follow.AccountID) if err != nil { log.Error(ctx, err) continue @@ -145,7 +160,6 @@ func (s *Surface) timelineAndNotifyStatusForFollowers( status, follow, filters, - mutes, ) if err != nil { log.Errorf(ctx, "error list timelining status: %v", err) @@ -168,7 +182,6 @@ func (s *Surface) timelineAndNotifyStatusForFollowers( stream.TimelineHome, statusfilter.FilterContextHome, filters, - mutes, ); homeTimelined { // If hometimelined, add to list of returned account IDs. @@ -205,7 +218,8 @@ func (s *Surface) timelineAndNotifyStatusForFollowers( gtsmodel.NotificationStatus, follow.Account, status.Account, - status.ID, + status, + nil, ); err != nil { log.Errorf(ctx, "error notifying status for account: %v", err) continue @@ -226,7 +240,6 @@ func (s *Surface) listTimelineStatusForFollow( status *gtsmodel.Status, follow *gtsmodel.Follow, filters []*gtsmodel.Filter, - mutes *usermute.CompiledUserMuteList, ) (timelined bool, exclusive bool, err error) { // Get all lists that contain this given follow. @@ -264,7 +277,6 @@ func (s *Surface) listTimelineStatusForFollow( stream.TimelineList+":"+list.ID, // key streamType to this specific list statusfilter.FilterContextHome, filters, - mutes, ) // Update flag based on if timelined. @@ -275,19 +287,12 @@ func (s *Surface) listTimelineStatusForFollow( } // getFiltersAndMutes returns an account's filters and mutes. -func (s *Surface) getFiltersAndMutes(ctx context.Context, accountID string) ([]*gtsmodel.Filter, *usermute.CompiledUserMuteList, error) { +func (s *Surface) getFilters(ctx context.Context, accountID string) ([]*gtsmodel.Filter, error) { filters, err := s.State.DB.GetFiltersForAccountID(ctx, accountID) if err != nil { - return nil, nil, gtserror.Newf("couldn't retrieve filters for account %s: %w", accountID, err) + return nil, gtserror.Newf("couldn't retrieve filters for account %s: %w", accountID, err) } - - mutes, err := s.State.DB.GetAccountMutes(gtscontext.SetBarebones(ctx), accountID, nil) - if err != nil { - return nil, nil, gtserror.Newf("couldn't retrieve mutes for account %s: %w", accountID, err) - } - - compiledMutes := usermute.NewCompiledUserMuteList(mutes) - return filters, compiledMutes, err + return filters, err } // listEligible checks if the given status is eligible @@ -366,7 +371,6 @@ func (s *Surface) timelineStatus( streamType string, filterCtx statusfilter.FilterContext, filters []*gtsmodel.Filter, - mutes *usermute.CompiledUserMuteList, ) bool { // Attempt to convert status to frontend API representation, @@ -376,7 +380,6 @@ func (s *Surface) timelineStatus( account, filterCtx, filters, - mutes, ) if err != nil && !errors.Is(err, statusfilter.ErrHideStatus) { log.Error(ctx, "error converting status %s to frontend: %v", status.URI, err) @@ -388,7 +391,7 @@ func (s *Surface) timelineStatus( if apiModel == nil { // Status was - // filtered / muted. + // filtered. return false } @@ -422,7 +425,7 @@ func (s *Surface) timelineAndNotifyStatusForTagFollowers( // Insert the status into the home timeline of each tag follower. errs := gtserror.MultiError{} for _, tagFollowerAccount := range tagFollowerAccounts { - filters, mutes, err := s.getFiltersAndMutes(ctx, tagFollowerAccount.ID) + filters, err := s.getFilters(ctx, tagFollowerAccount.ID) if err != nil { errs.Append(err) continue @@ -435,7 +438,6 @@ func (s *Surface) timelineAndNotifyStatusForTagFollowers( stream.TimelineHome, statusfilter.FilterContextHome, filters, - mutes, ) } @@ -605,7 +607,7 @@ func (s *Surface) timelineStatusUpdateForFollowers( // Get relevant filters and mutes for this follow's account. // (note the origin account of the follow is receiver of status). - filters, mutes, err := s.getFiltersAndMutes(ctx, follow.AccountID) + filters, err := s.getFilters(ctx, follow.AccountID) if err != nil { log.Error(ctx, err) continue @@ -616,7 +618,6 @@ func (s *Surface) timelineStatusUpdateForFollowers( status, follow, filters, - mutes, ) if err != nil { log.Errorf(ctx, "error list timelining status: %v", err) @@ -637,7 +638,6 @@ func (s *Surface) timelineStatusUpdateForFollowers( status, stream.TimelineHome, filters, - mutes, ) if err != nil { log.Errorf(ctx, "error home timelining status: %v", err) @@ -663,7 +663,6 @@ func (s *Surface) listTimelineStatusUpdateForFollow( status *gtsmodel.Status, follow *gtsmodel.Follow, filters []*gtsmodel.Filter, - mutes *usermute.CompiledUserMuteList, ) (bool, bool, error) { // Get all lists that contain this given follow. @@ -703,7 +702,6 @@ func (s *Surface) listTimelineStatusUpdateForFollow( status, stream.TimelineList+":"+list.ID, // key streamType to this specific list filters, - mutes, ) if err != nil { log.Errorf(ctx, "error adding status to list timeline: %v", err) @@ -727,7 +725,6 @@ func (s *Surface) timelineStreamStatusUpdate( status *gtsmodel.Status, streamType string, filters []*gtsmodel.Filter, - mutes *usermute.CompiledUserMuteList, ) (bool, error) { // Convert updated database model to frontend model. @@ -736,7 +733,6 @@ func (s *Surface) timelineStreamStatusUpdate( account, statusfilter.FilterContextHome, filters, - mutes, ) switch { @@ -778,7 +774,7 @@ func (s *Surface) timelineStatusUpdateForTagFollowers( // Stream the update to the home timeline of each tag follower. errs := gtserror.MultiError{} for _, tagFollowerAccount := range tagFollowerAccounts { - filters, mutes, err := s.getFiltersAndMutes(ctx, tagFollowerAccount.ID) + filters, err := s.getFilters(ctx, tagFollowerAccount.ID) if err != nil { errs.Append(err) continue @@ -790,7 +786,6 @@ func (s *Surface) timelineStatusUpdateForTagFollowers( status, stream.TimelineHome, filters, - mutes, ); err != nil { errs.Appendf( "error updating status %s on home timeline for account %s: %w", diff --git a/internal/processing/workers/workers.go b/internal/processing/workers/workers.go index c5b5f6ce2..1f4ef465f 100644 --- a/internal/processing/workers/workers.go +++ b/internal/processing/workers/workers.go @@ -20,6 +20,7 @@ package workers 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/visibility" "code.superseriousbusiness.org/gotosocial/internal/processing/account" "code.superseriousbusiness.org/gotosocial/internal/processing/common" @@ -44,6 +45,7 @@ func New( federator *federation.Federator, converter *typeutils.Converter, visFilter *visibility.Filter, + muteFilter *mutes.Filter, emailSender email.Sender, webPushSender webpush.Sender, account *account.Processor, @@ -66,6 +68,7 @@ func New( Converter: converter, Stream: stream, VisFilter: visFilter, + MuteFilter: muteFilter, EmailSender: emailSender, WebPushSender: webPushSender, Conversations: conversations, |
