diff options
Diffstat (limited to 'internal/filter')
| -rw-r--r-- | internal/filter/mutes/account.go | 85 | ||||
| -rw-r--r-- | internal/filter/mutes/filter.go | 45 | ||||
| -rw-r--r-- | internal/filter/mutes/filter_test.go | 75 | ||||
| -rw-r--r-- | internal/filter/mutes/status.go | 302 | ||||
| -rw-r--r-- | internal/filter/mutes/status_test.go | 283 | ||||
| -rw-r--r-- | internal/filter/usermute/usermute.go | 80 | ||||
| -rw-r--r-- | internal/filter/visibility/home_timeline.go | 23 | ||||
| -rw-r--r-- | internal/filter/visibility/status.go | 3 |
8 files changed, 807 insertions, 89 deletions
diff --git a/internal/filter/mutes/account.go b/internal/filter/mutes/account.go new file mode 100644 index 000000000..ecf4ffa4e --- /dev/null +++ b/internal/filter/mutes/account.go @@ -0,0 +1,85 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +package mutes + +import ( + "context" + "errors" + "time" + + "code.superseriousbusiness.org/gotosocial/internal/db" + "code.superseriousbusiness.org/gotosocial/internal/gtscontext" + "code.superseriousbusiness.org/gotosocial/internal/gtserror" + "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" +) + +// NOTE: +// we don't bother using the Mutes cache for any +// of the accounts functions below, as there's only +// a single cache load required of any UserMute. + +// AccountMuted returns whether given target account is muted by requester. +func (f *Filter) AccountMuted(ctx context.Context, requester *gtsmodel.Account, account *gtsmodel.Account) (bool, error) { + mute, expired, err := f.getUserMute(ctx, requester, account) + if err != nil { + return false, err + } else if mute == nil { + return false, nil + } + return !expired, nil +} + +// AccountNotificationsMuted returns whether notifications are muted for requester when incoming from given target account. +func (f *Filter) AccountNotificationsMuted(ctx context.Context, requester *gtsmodel.Account, account *gtsmodel.Account) (bool, error) { + mute, expired, err := f.getUserMute(ctx, requester, account) + if err != nil { + return false, err + } else if mute == nil { + return false, nil + } + return *mute.Notifications && !expired, nil +} + +func (f *Filter) getUserMute(ctx context.Context, requester *gtsmodel.Account, account *gtsmodel.Account) (*gtsmodel.UserMute, bool, error) { + if requester == nil { + // Un-authed so no account + // is possible to be muted. + return nil, false, nil + } + + // Look for mute against target. + mute, err := f.state.DB.GetMute( + gtscontext.SetBarebones(ctx), + requester.ID, + account.ID, + ) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + return nil, false, gtserror.Newf("db error getting user mute: %w", err) + } + + if mute == nil { + // No user mute exists! + return nil, false, nil + } + + // Get current time. + now := time.Now() + + // Return whether mute is expired. + return mute, mute.Expired(now), nil +} diff --git a/internal/filter/mutes/filter.go b/internal/filter/mutes/filter.go new file mode 100644 index 000000000..20adc3daf --- /dev/null +++ b/internal/filter/mutes/filter.go @@ -0,0 +1,45 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +package mutes + +import ( + "time" + + "code.superseriousbusiness.org/gotosocial/internal/state" +) + +type muteDetails struct { + // mute flags. + mute bool + notif bool + + // mute expiry times. + muteExpiry time.Time + notifExpiry time.Time +} + +// noauth is a placeholder ID used in cache lookups +// when there is no authorized account ID to use. +const noauth = "noauth" + +// Filter packages up a bunch of logic for checking whether +// given statuses or accounts are muted by a requester (user). +type Filter struct{ state *state.State } + +// NewFilter returns a new Filter interface that will use the provided database. +func NewFilter(state *state.State) *Filter { return &Filter{state: state} } diff --git a/internal/filter/mutes/filter_test.go b/internal/filter/mutes/filter_test.go new file mode 100644 index 000000000..260f6cff0 --- /dev/null +++ b/internal/filter/mutes/filter_test.go @@ -0,0 +1,75 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +package mutes_test + +import ( + "code.superseriousbusiness.org/gotosocial/internal/db" + "code.superseriousbusiness.org/gotosocial/internal/filter/mutes" + "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" + "code.superseriousbusiness.org/gotosocial/internal/state" + "code.superseriousbusiness.org/gotosocial/testrig" + "github.com/stretchr/testify/suite" +) + +type FilterStandardTestSuite struct { + // standard suite interfaces + suite.Suite + db db.DB + state state.State + + // standard suite models + testTokens map[string]*gtsmodel.Token + testApplications map[string]*gtsmodel.Application + testUsers map[string]*gtsmodel.User + testAccounts map[string]*gtsmodel.Account + testAttachments map[string]*gtsmodel.MediaAttachment + testStatuses map[string]*gtsmodel.Status + testTags map[string]*gtsmodel.Tag + testMentions map[string]*gtsmodel.Mention + testFollows map[string]*gtsmodel.Follow + + filter *mutes.Filter +} + +func (suite *FilterStandardTestSuite) SetupSuite() { + suite.testTokens = testrig.NewTestTokens() + suite.testApplications = testrig.NewTestApplications() + suite.testUsers = testrig.NewTestUsers() + suite.testAccounts = testrig.NewTestAccounts() + suite.testAttachments = testrig.NewTestAttachments() + suite.testStatuses = testrig.NewTestStatuses() + suite.testTags = testrig.NewTestTags() + suite.testMentions = testrig.NewTestMentions() + suite.testFollows = testrig.NewTestFollows() +} + +func (suite *FilterStandardTestSuite) SetupTest() { + suite.state.Caches.Init() + + testrig.InitTestConfig() + testrig.InitTestLog() + + suite.db = testrig.NewTestDB(&suite.state) + suite.filter = mutes.NewFilter(&suite.state) + + testrig.StandardDBSetup(suite.db, nil) +} + +func (suite *FilterStandardTestSuite) TearDownTest() { + testrig.StandardDBTeardown(suite.db) +} diff --git a/internal/filter/mutes/status.go b/internal/filter/mutes/status.go new file mode 100644 index 000000000..e2ef1e5a5 --- /dev/null +++ b/internal/filter/mutes/status.go @@ -0,0 +1,302 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +package mutes + +import ( + "context" + "errors" + "time" + + "code.superseriousbusiness.org/gotosocial/internal/cache" + "code.superseriousbusiness.org/gotosocial/internal/db" + "code.superseriousbusiness.org/gotosocial/internal/gtscontext" + "code.superseriousbusiness.org/gotosocial/internal/gtserror" + "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" +) + +// StatusMuted returns whether given target status is muted for requester in the context of timeline visibility. +func (f *Filter) StatusMuted(ctx context.Context, requester *gtsmodel.Account, status *gtsmodel.Status) (muted bool, err error) { + details, err := f.StatusMuteDetails(ctx, requester, status) + if err != nil { + return false, gtserror.Newf("error getting status mute details: %w", err) + } + return details.Mute && !details.MuteExpired(time.Now()), nil +} + +// StatusNotificationsMuted returns whether notifications are muted for requester when regarding given target status. +func (f *Filter) StatusNotificationsMuted(ctx context.Context, requester *gtsmodel.Account, status *gtsmodel.Status) (muted bool, err error) { + details, err := f.StatusMuteDetails(ctx, requester, status) + if err != nil { + return false, gtserror.Newf("error getting status mute details: %w", err) + } + return details.Notifications && !details.NotificationExpired(time.Now()), nil +} + +// StatusMuteDetails returns mute details about the given status for the given requesting account. +func (f *Filter) StatusMuteDetails(ctx context.Context, requester *gtsmodel.Account, status *gtsmodel.Status) (*cache.CachedMute, error) { + + // For requester ID use a + // fallback 'noauth' string + // by default for lookups. + requesterID := noauth + if requester != nil { + requesterID = requester.ID + } + + // Load mute details for this requesting account about status from cache, using load callback if needed. + details, err := f.state.Caches.Mutes.LoadOne("RequesterID,StatusID", func() (*cache.CachedMute, error) { + + // Load the mute details for given status. + details, err := f.getStatusMuteDetails(ctx, + requester, + status, + ) + if err != nil { + if err == cache.SentinelError { + // Filter-out our temporary + // race-condition error. + return &cache.CachedMute{}, nil + } + + return nil, err + } + + // Convert to cache details. + return &cache.CachedMute{ + StatusID: status.ID, + ThreadID: status.ThreadID, + RequesterID: requester.ID, + Mute: details.mute, + MuteExpiry: details.muteExpiry, + Notifications: details.notif, + NotificationExpiry: details.notifExpiry, + }, nil + }, requesterID, status.ID) + if err != nil { + return nil, err + } + + return details, err +} + +// getStatusMuteDetails loads muteDetails{} for the given +// status and the thread it is a part of, including any +// relevant muted parent status authors / mentions. +func (f *Filter) getStatusMuteDetails( + ctx context.Context, + requester *gtsmodel.Account, + status *gtsmodel.Status, +) ( + muteDetails, + error, +) { + var details muteDetails + + if requester == nil { + // Without auth, there will be no possible + // mute to exist. Always return as 'unmuted'. + return details, nil + } + + // Look for a stored mute from account against thread. + threadMute, err := f.state.DB.GetThreadMutedByAccount(ctx, + status.ThreadID, + requester.ID, + ) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + return details, gtserror.Newf("db error checking thread mute: %w", err) + } + + // Set notif mute on thread mute. + details.notif = (threadMute != nil) + + for next := status; ; { + // Load the mute details for 'next' status + // in current thread, into our details obj. + if err = f.loadOneStatusMuteDetails(ctx, + requester, + next, + &details, + ); err != nil { + return details, err + } + + if next.InReplyToURI == "" { + // Reached the top + // of the thread. + break + } + + if next.InReplyToID == "" { + // Parent is not yet dereferenced. + return details, cache.SentinelError + } + + // Check if parent is set. + inReplyTo := next.InReplyTo + if inReplyTo == nil { + + // Fetch next parent in conversation. + inReplyTo, err = f.state.DB.GetStatusByID( + gtscontext.SetBarebones(ctx), + next.InReplyToID, + ) + if err != nil { + return details, gtserror.Newf("error getting status parent %s: %w", next.InReplyToURI, err) + } + } + + // Set next status. + next = inReplyTo + } + + return details, nil +} + +// loadOneStatusMuteDetails loads the mute details for +// any relevant accounts to given status to the requesting +// account into the passed muteDetails object pointer. +func (f *Filter) loadOneStatusMuteDetails( + ctx context.Context, + requester *gtsmodel.Account, + status *gtsmodel.Status, + details *muteDetails, +) error { + // Look for mutes against related status accounts + // by requester (e.g. author, mention targets etc). + userMutes, err := f.getStatusRelatedUserMutes(ctx, + requester, + status, + ) + if err != nil { + return err + } + + for _, mute := range userMutes { + // Set as muted! + details.mute = true + + // Set notifications as + // muted if flag is set. + if *mute.Notifications { + details.notif = true + } + + // Check for expiry data given. + if !mute.ExpiresAt.IsZero() { + + // Update mute details expiry time if later. + if mute.ExpiresAt.After(details.muteExpiry) { + details.muteExpiry = mute.ExpiresAt + } + + // Update notif details expiry time if later. + if mute.ExpiresAt.After(details.notifExpiry) { + details.notifExpiry = mute.ExpiresAt + } + } + } + + return nil +} + +// getStatusRelatedUserMutes fetches user mutes for any +// of the possible related accounts regarding this status, +// i.e. the author and any account mentioned. +func (f *Filter) getStatusRelatedUserMutes( + ctx context.Context, + requester *gtsmodel.Account, + status *gtsmodel.Status, +) ( + []*gtsmodel.UserMute, + error, +) { + if status.AccountID == requester.ID { + // Status is by requester, we don't take + // into account related attached user mutes. + return nil, nil + } + + if !status.MentionsPopulated() { + var err error + + // Populate status mention objects before further mention checks. + status.Mentions, err = f.state.DB.GetMentions(ctx, status.MentionIDs) + if err != nil { + return nil, gtserror.Newf("error populating status %s mentions: %w", status.URI, err) + } + } + + // Preallocate a slice of worst possible case no. user mutes. + mutes := make([]*gtsmodel.UserMute, 0, 2+len(status.Mentions)) + + // Look for mute against author. + mute, err := f.state.DB.GetMute( + gtscontext.SetBarebones(ctx), + requester.ID, + status.AccountID, + ) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + return nil, gtserror.Newf("db error getting status author mute: %w", err) + } + + if mute != nil { + // Append author mute to total. + mutes = append(mutes, mute) + } + + if status.BoostOfAccountID != "" { + // Look for mute against boost author. + mute, err := f.state.DB.GetMute( + gtscontext.SetBarebones(ctx), + requester.ID, + status.AccountID, + ) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + return nil, gtserror.Newf("db error getting boost author mute: %w", err) + } + + if mute != nil { + // Append author mute to total. + mutes = append(mutes, mute) + } + } + + for _, mention := range status.Mentions { + // Look for mute against any target mentions. + if mention.TargetAccountID != requester.ID { + + // Look for mute against target. + mute, err := f.state.DB.GetMute( + gtscontext.SetBarebones(ctx), + requester.ID, + mention.TargetAccountID, + ) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + return nil, gtserror.Newf("db error getting mention target mute: %w", err) + } + + if mute != nil { + // Append target mute to total. + mutes = append(mutes, mute) + } + } + } + + return mutes, nil +} diff --git a/internal/filter/mutes/status_test.go b/internal/filter/mutes/status_test.go new file mode 100644 index 000000000..917900adf --- /dev/null +++ b/internal/filter/mutes/status_test.go @@ -0,0 +1,283 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +package mutes_test + +import ( + "testing" + + "code.superseriousbusiness.org/gotosocial/internal/ap" + "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" + "code.superseriousbusiness.org/gotosocial/internal/id" + "code.superseriousbusiness.org/gotosocial/internal/util" + "github.com/stretchr/testify/suite" +) + +type StatusMuteTestSuite struct { + FilterStandardTestSuite +} + +func (suite *StatusMuteTestSuite) TestMutedStatusAuthor() { + ctx := suite.T().Context() + + status := suite.testStatuses["admin_account_status_1"] + requester := suite.testAccounts["local_account_1"] + replyer := suite.testAccounts["local_account_2"] + + // Generate a new reply + // to the above status. + replyID := id.NewULID() + reply := >smodel.Status{ + ID: replyID, + URI: replyer.URI + "/statuses/" + replyID, + ThreadID: status.ThreadID, + AccountID: replyer.ID, + AccountURI: replyer.URI, + InReplyToID: status.ID, + InReplyToURI: status.URI, + InReplyToAccountID: status.AccountID, + Local: util.Ptr(false), + Federated: util.Ptr(true), + ActivityStreamsType: ap.ObjectNote, + } + + // And insert reply into the database. + err := suite.db.PutStatus(ctx, reply) + suite.NoError(err) + + // Ensure that neither status nor reply are muted to requester. + muted1, err1 := suite.filter.StatusMuted(ctx, requester, status) + muted2, err2 := suite.filter.StatusMuted(ctx, requester, reply) + suite.NoError(err1) + suite.NoError(err2) + suite.False(muted1) + suite.False(muted2) + + // Ensure notifications for neither status nor reply are muted to requester. + muted1, err = suite.filter.StatusNotificationsMuted(ctx, requester, status) + muted2, err = suite.filter.StatusNotificationsMuted(ctx, requester, reply) + suite.NoError(err1) + suite.NoError(err2) + suite.False(muted1) + suite.False(muted2) + + // Insert new user mute targetting first status author. + err = suite.state.DB.PutMute(ctx, >smodel.UserMute{ + ID: id.NewULID(), + AccountID: requester.ID, + TargetAccountID: status.AccountID, + Notifications: util.Ptr(false), + }) + suite.NoError(err) + + // Now ensure that both status and reply are muted to requester. + muted1, err1 = suite.filter.StatusMuted(ctx, requester, status) + muted2, err2 = suite.filter.StatusMuted(ctx, requester, reply) + suite.NoError(err1) + suite.NoError(err2) + suite.True(muted1) + suite.True(muted2) + + // Though neither status nor reply should have notifications muted to requester. + muted1, err = suite.filter.StatusNotificationsMuted(ctx, requester, status) + muted2, err = suite.filter.StatusNotificationsMuted(ctx, requester, reply) + suite.NoError(err1) + suite.NoError(err2) + suite.False(muted1) + suite.False(muted2) + + // Now delete account mutes to / from requesting account. + err = suite.state.DB.DeleteAccountMutes(ctx, requester.ID) + suite.NoError(err) + + // Now ensure that both status and reply are unmuted again. + muted1, err1 = suite.filter.StatusMuted(ctx, requester, status) + muted2, err2 = suite.filter.StatusMuted(ctx, requester, reply) + suite.NoError(err1) + suite.NoError(err2) + suite.False(muted1) + suite.False(muted2) +} + +func (suite *StatusMuteTestSuite) TestMutedStatusMentionee() { + ctx := suite.T().Context() + + status := suite.testStatuses["admin_account_status_5"] + requester := suite.testAccounts["local_account_1"] + mentionee := suite.testAccounts["local_account_2"] + replyer := suite.testAccounts["local_account_3"] + + // Generate a new reply + // to the above status. + replyID := id.NewULID() + reply := >smodel.Status{ + ID: replyID, + URI: replyer.URI + "/statuses/" + replyID, + ThreadID: status.ThreadID, + AccountID: replyer.ID, + AccountURI: replyer.URI, + InReplyToID: status.ID, + InReplyToURI: status.URI, + InReplyToAccountID: status.AccountID, + Local: util.Ptr(false), + Federated: util.Ptr(true), + ActivityStreamsType: ap.ObjectNote, + } + + // And insert reply into the database. + err := suite.db.PutStatus(ctx, reply) + suite.NoError(err) + + // Ensure that neither status nor reply are muted to requester. + muted1, err1 := suite.filter.StatusMuted(ctx, requester, status) + muted2, err2 := suite.filter.StatusMuted(ctx, requester, reply) + suite.NoError(err1) + suite.NoError(err2) + suite.False(muted1) + suite.False(muted2) + + // Ensure notifications for neither status nor reply are muted to requester. + muted1, err = suite.filter.StatusNotificationsMuted(ctx, requester, status) + muted2, err = suite.filter.StatusNotificationsMuted(ctx, requester, reply) + suite.NoError(err1) + suite.NoError(err2) + suite.False(muted1) + suite.False(muted2) + + // Insert user visibility mute targetting status author. + err = suite.state.DB.PutMute(ctx, >smodel.UserMute{ + ID: id.NewULID(), + AccountID: requester.ID, + TargetAccountID: mentionee.ID, + Notifications: util.Ptr(false), + }) + suite.NoError(err) + + // Now ensure that both status and reply are muted to requester. + muted1, err1 = suite.filter.StatusMuted(ctx, requester, status) + muted2, err2 = suite.filter.StatusMuted(ctx, requester, reply) + suite.NoError(err1) + suite.NoError(err2) + suite.True(muted1) + suite.True(muted2) + + // Though neither status nor reply should have notifications muted to requester. + muted1, err = suite.filter.StatusNotificationsMuted(ctx, requester, status) + muted2, err = suite.filter.StatusNotificationsMuted(ctx, requester, reply) + suite.NoError(err1) + suite.NoError(err2) + suite.False(muted1) + suite.False(muted2) + + // Now delete account mutes to / from requesting account. + err = suite.state.DB.DeleteAccountMutes(ctx, requester.ID) + suite.NoError(err) + + // Now ensure that both status and reply are unmuted again. + muted1, err1 = suite.filter.StatusMuted(ctx, requester, status) + muted2, err2 = suite.filter.StatusMuted(ctx, requester, reply) + suite.NoError(err1) + suite.NoError(err2) + suite.False(muted1) + suite.False(muted2) +} + +func (suite *StatusMuteTestSuite) TestMutedStatusThread() { + ctx := suite.T().Context() + + status := suite.testStatuses["admin_account_status_1"] + requester := suite.testAccounts["local_account_1"] + replyer := suite.testAccounts["local_account_2"] + + // Generate a new reply + // to the above status. + replyID := id.NewULID() + reply := >smodel.Status{ + ID: replyID, + URI: replyer.URI + "/statuses/" + replyID, + ThreadID: status.ThreadID, + AccountID: replyer.ID, + AccountURI: replyer.URI, + InReplyToID: status.ID, + InReplyToURI: status.URI, + InReplyToAccountID: status.AccountID, + Local: util.Ptr(false), + Federated: util.Ptr(true), + ActivityStreamsType: ap.ObjectNote, + } + + // And insert reply into the database. + err := suite.db.PutStatus(ctx, reply) + suite.NoError(err) + + // Ensure that neither status nor reply are muted to requester. + muted1, err1 := suite.filter.StatusMuted(ctx, requester, status) + muted2, err2 := suite.filter.StatusMuted(ctx, requester, reply) + suite.NoError(err1) + suite.NoError(err2) + suite.False(muted1) + suite.False(muted2) + + // Ensure notifications for neither status nor reply are muted to requester. + muted1, err = suite.filter.StatusNotificationsMuted(ctx, requester, status) + muted2, err = suite.filter.StatusNotificationsMuted(ctx, requester, reply) + suite.NoError(err1) + suite.NoError(err2) + suite.False(muted1) + suite.False(muted2) + + threadMuteID := id.NewULID() + + // Insert new notification mute targetting status thread. + err = suite.db.PutThreadMute(ctx, >smodel.ThreadMute{ + ID: threadMuteID, + AccountID: requester.ID, + ThreadID: status.ThreadID, + }) + + // Ensure status and reply are still not muted to requester. + muted1, err1 = suite.filter.StatusMuted(ctx, requester, status) + muted2, err2 = suite.filter.StatusMuted(ctx, requester, reply) + suite.NoError(err1) + suite.NoError(err2) + suite.False(muted1) + suite.False(muted2) + + // Though now ensure notifications for both ARE muted to requester. + muted1, err = suite.filter.StatusNotificationsMuted(ctx, requester, status) + muted2, err = suite.filter.StatusNotificationsMuted(ctx, requester, reply) + suite.NoError(err1) + suite.NoError(err2) + suite.True(muted1) + suite.True(muted2) + + // Now delete the mute from requester targetting thread. + err = suite.state.DB.DeleteThreadMute(ctx, threadMuteID) + suite.NoError(err) + + // Andf ensure notifications for both are unmuted to the requester again. + muted1, err = suite.filter.StatusNotificationsMuted(ctx, requester, status) + muted2, err = suite.filter.StatusNotificationsMuted(ctx, requester, reply) + suite.NoError(err1) + suite.NoError(err2) + suite.False(muted1) + suite.False(muted2) +} + +func TestStatusMuteTestSuite(t *testing.T) { + suite.Run(t, new(StatusMuteTestSuite)) +} diff --git a/internal/filter/usermute/usermute.go b/internal/filter/usermute/usermute.go deleted file mode 100644 index d8d1aae46..000000000 --- a/internal/filter/usermute/usermute.go +++ /dev/null @@ -1,80 +0,0 @@ -// GoToSocial -// Copyright (C) GoToSocial Authors admin@gotosocial.org -// SPDX-License-Identifier: AGPL-3.0-or-later -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <http://www.gnu.org/licenses/>. - -package usermute - -import ( - "time" - - statusfilter "code.superseriousbusiness.org/gotosocial/internal/filter/status" - "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" -) - -type compiledUserMuteListEntry struct { - ExpiresAt time.Time - Notifications bool -} - -func (e *compiledUserMuteListEntry) appliesInContext(filterContext statusfilter.FilterContext) bool { - switch filterContext { - case statusfilter.FilterContextHome: - return true - case statusfilter.FilterContextNotifications: - return e.Notifications - case statusfilter.FilterContextPublic: - return true - case statusfilter.FilterContextThread: - return true - case statusfilter.FilterContextAccount: - return false - } - return false -} - -func (e *compiledUserMuteListEntry) expired(now time.Time) bool { - return !e.ExpiresAt.IsZero() && !e.ExpiresAt.After(now) -} - -type CompiledUserMuteList struct { - byTargetAccountID map[string]compiledUserMuteListEntry -} - -func NewCompiledUserMuteList(mutes []*gtsmodel.UserMute) (c *CompiledUserMuteList) { - c = &CompiledUserMuteList{byTargetAccountID: make(map[string]compiledUserMuteListEntry, len(mutes))} - for _, mute := range mutes { - c.byTargetAccountID[mute.TargetAccountID] = compiledUserMuteListEntry{ - ExpiresAt: mute.ExpiresAt, - Notifications: *mute.Notifications, - } - } - return -} - -func (c *CompiledUserMuteList) Len() int { - if c == nil { - return 0 - } - return len(c.byTargetAccountID) -} - -func (c *CompiledUserMuteList) Matches(accountID string, filterContext statusfilter.FilterContext, now time.Time) bool { - if c == nil { - return false - } - e, found := c.byTargetAccountID[accountID] - return found && e.appliesInContext(filterContext) && !e.expired(now) -} diff --git a/internal/filter/visibility/home_timeline.go b/internal/filter/visibility/home_timeline.go index 03a3b62c3..fbb6ea3da 100644 --- a/internal/filter/visibility/home_timeline.go +++ b/internal/filter/visibility/home_timeline.go @@ -161,15 +161,22 @@ func (f *Filter) isStatusHomeTimelineable(ctx context.Context, owner *gtsmodel.A return false, cache.SentinelError } - // Fetch next parent in conversation. - inReplyToID := next.InReplyToID - next, err = f.state.DB.GetStatusByID( - gtscontext.SetBarebones(ctx), - inReplyToID, - ) - if err != nil { - return false, gtserror.Newf("error getting status parent %s: %w", inReplyToID, err) + // Check if parent is set. + inReplyTo := next.InReplyTo + if inReplyTo == nil { + + // Fetch next parent in conversation. + inReplyTo, err = f.state.DB.GetStatusByID( + gtscontext.SetBarebones(ctx), + next.InReplyToID, + ) + if err != nil { + return false, gtserror.Newf("error getting status parent %s: %w", next.InReplyToURI, err) + } } + + // Set next status. + next = inReplyTo } if next != status && !oneAuthor && !visible { diff --git a/internal/filter/visibility/status.go b/internal/filter/visibility/status.go index 6edb32ec0..24fa6f2e6 100644 --- a/internal/filter/visibility/status.go +++ b/internal/filter/visibility/status.go @@ -316,7 +316,8 @@ func (f *Filter) areStatusAccountsVisible(ctx context.Context, requester *gtsmod // This is a boosted status. if status.AccountID == status.BoostOfAccountID { - // Some clout-chaser boosted their own status, tch. + // Some clout-chaser boosted + // their own status, tch. return true, nil } |
