summaryrefslogtreecommitdiff
path: root/internal/filter
diff options
context:
space:
mode:
Diffstat (limited to 'internal/filter')
-rw-r--r--internal/filter/mutes/account.go85
-rw-r--r--internal/filter/mutes/filter.go45
-rw-r--r--internal/filter/mutes/filter_test.go75
-rw-r--r--internal/filter/mutes/status.go302
-rw-r--r--internal/filter/mutes/status_test.go283
-rw-r--r--internal/filter/usermute/usermute.go80
-rw-r--r--internal/filter/visibility/home_timeline.go23
-rw-r--r--internal/filter/visibility/status.go3
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 := &gtsmodel.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, &gtsmodel.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 := &gtsmodel.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, &gtsmodel.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 := &gtsmodel.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, &gtsmodel.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
}