diff options
author | 2024-06-06 09:38:02 -0700 | |
---|---|---|
committer | 2024-06-06 16:38:02 +0000 | |
commit | 5e2d4fdb19eb4fcd4c0bbfb3e2f29067a58c88c8 (patch) | |
tree | 607006af6b4bb63bb625b39f3ca0fe869eb6ba95 /internal/typeutils | |
parent | [bugfix] update media if more than just url changes (#2970) (diff) | |
download | gotosocial-5e2d4fdb19eb4fcd4c0bbfb3e2f29067a58c88c8.tar.xz |
[feature] User muting (#2960)
* User muting
* Address review feedback
* Rename uniqueness constraint on user_mutes to match convention
* Remove unused account_id from where clause
* Add UserMute to NewTestDB
* Update test/envparsing.sh with new and fixed cache stuff
* Address tobi's review comments
* Make compiledUserMuteListEntry.expired consistent with UserMute.Expired
* Make sure mute_expires_at is serialized as an explicit null for indefinite mutes
---------
Co-authored-by: tobi <tobi.smethurst@protonmail.com>
Diffstat (limited to 'internal/typeutils')
-rw-r--r-- | internal/typeutils/converter.go | 3 | ||||
-rw-r--r-- | internal/typeutils/internaltofrontend.go | 93 | ||||
-rw-r--r-- | internal/typeutils/internaltofrontend_test.go | 63 |
3 files changed, 145 insertions, 14 deletions
diff --git a/internal/typeutils/converter.go b/internal/typeutils/converter.go index 5f849c39d..dfa72fdcd 100644 --- a/internal/typeutils/converter.go +++ b/internal/typeutils/converter.go @@ -20,6 +20,7 @@ package typeutils import ( "sync" + "github.com/superseriousbusiness/gotosocial/internal/filter/visibility" "github.com/superseriousbusiness/gotosocial/internal/state" ) @@ -27,11 +28,13 @@ type Converter struct { state *state.State defaultAvatars []string randAvatars sync.Map + filter *visibility.Filter } func NewConverter(state *state.State) *Converter { return &Converter{ state: state, defaultAvatars: populateDefaultAvatars(), + filter: visibility.NewFilter(state), } } diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index e1380fc9e..787d8f099 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -31,6 +31,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status" + "github.com/superseriousbusiness/gotosocial/internal/filter/usermute" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/language" @@ -741,8 +742,9 @@ func (c *Converter) StatusToAPIStatus( requestingAccount *gtsmodel.Account, filterContext statusfilter.FilterContext, filters []*gtsmodel.Filter, + mutes *usermute.CompiledUserMuteList, ) (*apimodel.Status, error) { - apiStatus, err := c.statusToFrontend(ctx, s, requestingAccount, filterContext, filters) + apiStatus, err := c.statusToFrontend(ctx, s, requestingAccount, filterContext, filters, mutes) if err != nil { return nil, err } @@ -757,7 +759,7 @@ func (c *Converter) StatusToAPIStatus( return apiStatus, nil } -// statusToAPIFilterResults applies filters to a status and returns an API filter result object. +// statusToAPIFilterResults applies filters and mutes to a status and returns an API filter result object. // The result may be nil if no filters matched. // If the status should not be returned at all, it returns the ErrHideStatus error. func (c *Converter) statusToAPIFilterResults( @@ -766,14 +768,71 @@ func (c *Converter) statusToAPIFilterResults( requestingAccount *gtsmodel.Account, filterContext statusfilter.FilterContext, filters []*gtsmodel.Filter, + mutes *usermute.CompiledUserMuteList, ) ([]apimodel.FilterResult, error) { - if filterContext == "" || len(filters) == 0 || s.AccountID == requestingAccount.ID { + // If there are no filters or mutes, we're done. + // We never hide statuses authored by the requesting account, + // since not being able to see your own posts is confusing. + if filterContext == "" || (len(filters) == 0 && mutes.Len() == 0) || s.AccountID == requestingAccount.ID { return nil, nil } - filterResults := make([]apimodel.FilterResult, 0, len(filters)) - + // Both mutes and filters can expire. now := time.Now() + + // If the requesting account mutes the account that created this status, hide the status. + if mutes.Matches(s.AccountID, filterContext, now) { + return nil, statusfilter.ErrHideStatus + } + // If this status is part of a multi-account discussion, + // and all of the accounts replied to or mentioned are invisible to the requesting account + // (due to blocks, domain blocks, moderation, etc.), + // or are muted, hide the status. + // First, collect the accounts we have to check. + otherAccounts := make([]*gtsmodel.Account, 0, 1+len(s.Mentions)) + if s.InReplyToAccount != nil { + otherAccounts = append(otherAccounts, s.InReplyToAccount) + } + for _, mention := range s.Mentions { + otherAccounts = append(otherAccounts, mention.TargetAccount) + } + // If there are no other accounts, skip this check. + if len(otherAccounts) > 0 { + // Start by assuming that they're all invisible or muted. + allOtherAccountsInvisibleOrMuted := true + + for _, account := range otherAccounts { + // Is this account visible? + visible, err := c.filter.AccountVisible(ctx, requestingAccount, account) + if err != nil { + return nil, err + } + if !visible { + // It's invisible. Check the next account. + continue + } + + // If visible, is it muted? + if mutes.Matches(account.ID, filterContext, now) { + // It's muted. Check the next account. + continue + } + + // If we get here, the account is visible and not muted. + // We should show this status, and don't have to check any more accounts. + allOtherAccountsInvisibleOrMuted = false + break + } + + // If we didn't find any visible non-muted accounts, hide the status. + if allOtherAccountsInvisibleOrMuted { + return nil, statusfilter.ErrHideStatus + } + } + + // At this point, the status isn't muted, but might still be filtered. + // Record all matching warn filters and the reasons they matched. + filterResults := make([]apimodel.FilterResult, 0, len(filters)) for _, filter := range filters { if !filterAppliesInContext(filter, filterContext) { // Filter doesn't apply to this context. @@ -893,7 +952,7 @@ func (c *Converter) StatusToWebStatus( s *gtsmodel.Status, requestingAccount *gtsmodel.Account, ) (*apimodel.Status, error) { - webStatus, err := c.statusToFrontend(ctx, s, requestingAccount, statusfilter.FilterContextNone, nil) + webStatus, err := c.statusToFrontend(ctx, s, requestingAccount, statusfilter.FilterContextNone, nil, nil) if err != nil { return nil, err } @@ -997,6 +1056,7 @@ func (c *Converter) statusToFrontend( requestingAccount *gtsmodel.Account, filterContext statusfilter.FilterContext, filters []*gtsmodel.Filter, + mutes *usermute.CompiledUserMuteList, ) (*apimodel.Status, error) { // Try to populate status struct pointer fields. // We can continue in many cases of partial failure, @@ -1095,7 +1155,7 @@ func (c *Converter) statusToFrontend( } if s.BoostOf != nil { - reblog, err := c.StatusToAPIStatus(ctx, s.BoostOf, requestingAccount, filterContext, filters) + reblog, err := c.StatusToAPIStatus(ctx, s.BoostOf, requestingAccount, filterContext, filters, mutes) if errors.Is(err, statusfilter.ErrHideStatus) { // If we'd hide the original status, hide the boost. return nil, err @@ -1164,8 +1224,11 @@ func (c *Converter) statusToFrontend( } // Apply filters. - filterResults, err := c.statusToAPIFilterResults(ctx, s, requestingAccount, filterContext, filters) + filterResults, err := c.statusToAPIFilterResults(ctx, s, requestingAccount, filterContext, filters, mutes) if err != nil { + if errors.Is(err, statusfilter.ErrHideStatus) { + return nil, err + } return nil, fmt.Errorf("error applying filters: %w", err) } apiStatus.Filtered = filterResults @@ -1453,7 +1516,12 @@ func (c *Converter) RelationshipToAPIRelationship(ctx context.Context, r *gtsmod } // NotificationToAPINotification converts a gts notification into a api notification -func (c *Converter) NotificationToAPINotification(ctx context.Context, n *gtsmodel.Notification, filters []*gtsmodel.Filter) (*apimodel.Notification, error) { +func (c *Converter) NotificationToAPINotification( + ctx context.Context, + n *gtsmodel.Notification, + filters []*gtsmodel.Filter, + mutes *usermute.CompiledUserMuteList, +) (*apimodel.Notification, error) { if n.TargetAccount == nil { tAccount, err := c.state.DB.GetAccountByID(ctx, n.TargetAccountID) if err != nil { @@ -1494,8 +1562,11 @@ func (c *Converter) NotificationToAPINotification(ctx context.Context, n *gtsmod } var err error - apiStatus, err = c.StatusToAPIStatus(ctx, n.Status, n.TargetAccount, statusfilter.FilterContextNotifications, filters) + apiStatus, err = c.StatusToAPIStatus(ctx, n.Status, n.TargetAccount, statusfilter.FilterContextNotifications, filters, mutes) if err != nil { + if errors.Is(err, statusfilter.ErrHideStatus) { + return nil, err + } return nil, fmt.Errorf("NotificationToapi: error converting status to api: %s", err) } } @@ -1647,7 +1718,7 @@ func (c *Converter) ReportToAdminAPIReport(ctx context.Context, r *gtsmodel.Repo } } for _, s := range r.Statuses { - status, err := c.StatusToAPIStatus(ctx, s, requestingAccount, statusfilter.FilterContextNone, nil) + status, err := c.StatusToAPIStatus(ctx, s, requestingAccount, statusfilter.FilterContextNone, nil, nil) if err != nil { return nil, fmt.Errorf("ReportToAdminAPIReport: error converting status with id %s to api status: %w", s.ID, err) } diff --git a/internal/typeutils/internaltofrontend_test.go b/internal/typeutils/internaltofrontend_test.go index 946e38b30..16dc27c87 100644 --- a/internal/typeutils/internaltofrontend_test.go +++ b/internal/typeutils/internaltofrontend_test.go @@ -26,7 +26,9 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status" + "github.com/superseriousbusiness/gotosocial/internal/filter/usermute" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/util" "github.com/superseriousbusiness/gotosocial/testrig" ) @@ -428,7 +430,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(context.Background(), testStatus, requestingAccount, statusfilter.FilterContextNone, nil) + apiStatus, err := suite.typeconverter.StatusToAPIStatus(context.Background(), testStatus, requestingAccount, statusfilter.FilterContextNone, nil, nil) suite.NoError(err) b, err := json.MarshalIndent(apiStatus, "", " ") @@ -556,6 +558,7 @@ func (suite *InternalToFrontendTestSuite) TestWarnFilteredStatusToFrontend() { requestingAccount, statusfilter.FilterContextHome, requestingAccountFilters, + nil, ) suite.NoError(err) @@ -711,6 +714,60 @@ func (suite *InternalToFrontendTestSuite) TestHideFilteredStatusToFrontend() { requestingAccount, statusfilter.FilterContextHome, requestingAccountFilters, + nil, + ) + suite.ErrorIs(err, statusfilter.ErrHideStatus) +} + +// Test that a status from a user muted by the requesting user results in the ErrHideStatus error. +func (suite *InternalToFrontendTestSuite) TestMutedStatusToFrontend() { + testStatus := suite.testStatuses["admin_account_status_1"] + requestingAccount := suite.testAccounts["local_account_1"] + mutes := usermute.NewCompiledUserMuteList([]*gtsmodel.UserMute{ + { + AccountID: requestingAccount.ID, + TargetAccountID: testStatus.AccountID, + Notifications: util.Ptr(false), + }, + }) + _, err := suite.typeconverter.StatusToAPIStatus( + context.Background(), + testStatus, + requestingAccount, + statusfilter.FilterContextHome, + nil, + mutes, + ) + suite.ErrorIs(err, statusfilter.ErrHideStatus) +} + +// Test that a status replying to a user muted by the requesting user results in the ErrHideStatus error. +func (suite *InternalToFrontendTestSuite) TestMutedReplyStatusToFrontend() { + mutedAccount := suite.testAccounts["local_account_2"] + testStatus := suite.testStatuses["admin_account_status_1"] + testStatus.InReplyToID = suite.testStatuses["local_account_2_status_1"].ID + testStatus.InReplyToAccountID = mutedAccount.ID + requestingAccount := suite.testAccounts["local_account_1"] + mutes := usermute.NewCompiledUserMuteList([]*gtsmodel.UserMute{ + { + AccountID: requestingAccount.ID, + TargetAccountID: mutedAccount.ID, + Notifications: util.Ptr(false), + }, + }) + // Populate status so the converter has the account objects it needs for muting. + err := suite.db.PopulateStatus(context.Background(), testStatus) + if err != nil { + suite.FailNow(err.Error()) + } + // Convert the status to API format, which should fail. + _, err = suite.typeconverter.StatusToAPIStatus( + context.Background(), + testStatus, + requestingAccount, + statusfilter.FilterContextHome, + nil, + mutes, ) suite.ErrorIs(err, statusfilter.ErrHideStatus) } @@ -719,7 +776,7 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontendUnknownAttachments testStatus := suite.testStatuses["remote_account_2_status_1"] requestingAccount := suite.testAccounts["admin_account"] - apiStatus, err := suite.typeconverter.StatusToAPIStatus(context.Background(), testStatus, requestingAccount, statusfilter.FilterContextNone, nil) + apiStatus, err := suite.typeconverter.StatusToAPIStatus(context.Background(), testStatus, requestingAccount, statusfilter.FilterContextNone, nil, nil) suite.NoError(err) b, err := json.MarshalIndent(apiStatus, "", " ") @@ -952,7 +1009,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(context.Background(), testStatus, requestingAccount, statusfilter.FilterContextNone, nil) + apiStatus, err := suite.typeconverter.StatusToAPIStatus(context.Background(), testStatus, requestingAccount, statusfilter.FilterContextNone, nil, nil) suite.NoError(err) b, err := json.MarshalIndent(apiStatus, "", " ") |