diff --git a/cmd/gotosocial/action/server/server.go b/cmd/gotosocial/action/server/server.go
index 703df5e4b..3ec12fb83 100644
--- a/cmd/gotosocial/action/server/server.go
+++ b/cmd/gotosocial/action/server/server.go
@@ -41,6 +41,7 @@ import (
"code.superseriousbusiness.org/gotosocial/internal/federation"
"code.superseriousbusiness.org/gotosocial/internal/federation/federatingdb"
"code.superseriousbusiness.org/gotosocial/internal/filter/interaction"
+ "code.superseriousbusiness.org/gotosocial/internal/filter/mutes"
"code.superseriousbusiness.org/gotosocial/internal/filter/spam"
"code.superseriousbusiness.org/gotosocial/internal/filter/visibility"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
@@ -268,6 +269,7 @@ var Start action.GTSAction = func(ctx context.Context) error {
)
typeConverter := typeutils.NewConverter(state)
visFilter := visibility.NewFilter(state)
+ muteFilter := mutes.NewFilter(state)
intFilter := interaction.NewFilter(state)
spamFilter := spam.NewFilter(state)
federatingDB := federatingdb.New(state, typeConverter, visFilter, intFilter, spamFilter)
@@ -348,6 +350,7 @@ var Start action.GTSAction = func(ctx context.Context) error {
emailSender,
webPushSender,
visFilter,
+ muteFilter,
intFilter,
)
diff --git a/internal/api/client/notifications/notificationsget.go b/internal/api/client/notifications/notificationsget.go
index e6f0d342b..5550ed082 100644
--- a/internal/api/client/notifications/notificationsget.go
+++ b/internal/api/client/notifications/notificationsget.go
@@ -169,7 +169,7 @@ func (m *Module) NotificationsGETHandler(c *gin.Context) {
ctx := c.Request.Context()
resp, errWithCode := m.processor.Timeline().NotificationsGet(
ctx,
- authed,
+ authed.Account,
page,
parseNotificationTypes(ctx, c.QueryArray(TypesKey)), // Include types.
parseNotificationTypes(ctx, c.QueryArray(ExcludeTypesKey)), // Exclude types.
diff --git a/internal/api/wellknown/webfinger/webfingerget_test.go b/internal/api/wellknown/webfinger/webfingerget_test.go
index 0ad35d4d9..2bdc0f461 100644
--- a/internal/api/wellknown/webfinger/webfingerget_test.go
+++ b/internal/api/wellknown/webfinger/webfingerget_test.go
@@ -33,6 +33,7 @@ import (
"code.superseriousbusiness.org/gotosocial/internal/cleaner"
"code.superseriousbusiness.org/gotosocial/internal/config"
"code.superseriousbusiness.org/gotosocial/internal/filter/interaction"
+ "code.superseriousbusiness.org/gotosocial/internal/filter/mutes"
"code.superseriousbusiness.org/gotosocial/internal/filter/visibility"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"code.superseriousbusiness.org/gotosocial/internal/processing"
@@ -98,6 +99,7 @@ func (suite *WebfingerGetTestSuite) funkifyAccountDomain(host string, accountDom
suite.emailSender,
testrig.NewNoopWebPushSender(),
visibility.NewFilter(&suite.state),
+ mutes.NewFilter(&suite.state),
interaction.NewFilter(&suite.state),
)
diff --git a/internal/cache/cache.go b/internal/cache/cache.go
index d05b85a15..54777441f 100644
--- a/internal/cache/cache.go
+++ b/internal/cache/cache.go
@@ -49,6 +49,10 @@ type Caches struct {
// Timelines ...
Timelines TimelineCaches
+ // Mutes provides access to the item mutes
+ // cache. (used by the item mutes filter).
+ Mutes MutesCache
+
// Visibility provides access to the item visibility
// cache. (used by the visibility filter).
Visibility VisibilityCache
@@ -125,6 +129,7 @@ func (c *Caches) Init() {
c.initWebfinger()
c.initWebPushSubscription()
c.initWebPushSubscriptionIDs()
+ c.initMutes()
c.initVisibility()
}
diff --git a/internal/cache/db.go b/internal/cache/db.go
index 78cc01e06..5592ca493 100644
--- a/internal/cache/db.go
+++ b/internal/cache/db.go
@@ -1531,9 +1531,10 @@ func (c *Caches) initThreadMute() {
{Fields: "AccountID", Multiple: true},
{Fields: "ThreadID,AccountID"},
},
- MaxSize: cap,
- IgnoreErr: ignoreErrors,
- Copy: copyF,
+ MaxSize: cap,
+ IgnoreErr: ignoreErrors,
+ Copy: copyF,
+ Invalidate: c.OnInvalidateThreadMute,
})
}
diff --git a/internal/cache/invalidate.go b/internal/cache/invalidate.go
index 3512bb51e..88b7415ae 100644
--- a/internal/cache/invalidate.go
+++ b/internal/cache/invalidate.go
@@ -28,12 +28,18 @@ import (
// HOOKS TO BE CALLED ON DELETE YOU MUST FIRST POPULATE IT IN THE CACHE.
func (c *Caches) OnInvalidateAccount(account *gtsmodel.Account) {
- // Invalidate stats for this account.
+ // Invalidate cached stats objects for this account.
c.DB.AccountStats.Invalidate("AccountID", account.ID)
- // Invalidate account ID cached visibility.
+ // Invalidate as possible visibility target result.
c.Visibility.Invalidate("ItemID", account.ID)
- c.Visibility.Invalidate("RequesterID", account.ID)
+
+ // If account is local, invalidate as
+ // possible mute / visibility result requester.
+ if account.IsLocal() {
+ c.Visibility.Invalidate("RequesterID", account.ID)
+ c.Mutes.Invalidate("RequesterID", account.ID)
+ }
// Invalidate this account's
// following / follower lists.
@@ -66,13 +72,31 @@ func (c *Caches) OnInvalidateApplication(app *gtsmodel.Application) {
}
func (c *Caches) OnInvalidateBlock(block *gtsmodel.Block) {
- // Invalidate block origin account ID cached visibility.
- c.Visibility.Invalidate("ItemID", block.AccountID)
- c.Visibility.Invalidate("RequesterID", block.AccountID)
+ // Invalidate both block origin and target as
+ // possible lookup targets for visibility results.
+ c.Visibility.InvalidateIDs("ItemID", []string{
+ block.TargetAccountID,
+ block.AccountID,
+ })
- // Invalidate block target account ID cached visibility.
- c.Visibility.Invalidate("ItemID", block.TargetAccountID)
- c.Visibility.Invalidate("RequesterID", block.TargetAccountID)
+ // Track which of block / target are local.
+ localAccountIDs := make([]string, 0, 2)
+
+ // If origin is local (or uncertain), also invalidate
+ // results for them as mute / visibility result requester.
+ if block.Account == nil || block.Account.IsLocal() {
+ localAccountIDs = append(localAccountIDs, block.AccountID)
+ }
+
+ // If target is local (or uncertain), also invalidate
+ // results for them as mute / visibility result requester.
+ if block.TargetAccount == nil || block.TargetAccount.IsLocal() {
+ localAccountIDs = append(localAccountIDs, block.TargetAccountID)
+ }
+
+ // Now perform local mute / visibility result invalidations.
+ c.Visibility.InvalidateIDs("RequesterID", localAccountIDs)
+ c.Mutes.InvalidateIDs("RequesterID", localAccountIDs)
// Invalidate source account's block lists.
c.DB.BlockIDs.Invalidate(block.AccountID)
@@ -92,13 +116,31 @@ func (c *Caches) OnInvalidateFollow(follow *gtsmodel.Follow) {
// Invalidate follow request with this same ID.
c.DB.FollowRequest.Invalidate("ID", follow.ID)
- // Invalidate follow origin account ID cached visibility.
- c.Visibility.Invalidate("ItemID", follow.AccountID)
- c.Visibility.Invalidate("RequesterID", follow.AccountID)
+ // Invalidate both follow origin and target as
+ // possible lookup targets for visibility results.
+ c.Visibility.InvalidateIDs("ItemID", []string{
+ follow.TargetAccountID,
+ follow.AccountID,
+ })
- // Invalidate follow target account ID cached visibility.
- c.Visibility.Invalidate("ItemID", follow.TargetAccountID)
- c.Visibility.Invalidate("RequesterID", follow.TargetAccountID)
+ // Track which of follow / target are local.
+ localAccountIDs := make([]string, 0, 2)
+
+ // If origin is local (or uncertain), also invalidate
+ // results for them as mute / visibility result requester.
+ if follow.Account == nil || follow.Account.IsLocal() {
+ localAccountIDs = append(localAccountIDs, follow.AccountID)
+ }
+
+ // If target is local (or uncertain), also invalidate
+ // results for them as mute / visibility result requester.
+ if follow.TargetAccount == nil || follow.TargetAccount.IsLocal() {
+ localAccountIDs = append(localAccountIDs, follow.TargetAccountID)
+ }
+
+ // Now perform local mute / visibility result invalidations.
+ c.Visibility.InvalidateIDs("RequesterID", localAccountIDs)
+ c.Mutes.InvalidateIDs("RequesterID", localAccountIDs)
// Invalidate ID slice cache.
c.DB.FollowIDs.Invalidate(
@@ -227,12 +269,16 @@ func (c *Caches) OnInvalidatePollVote(vote *gtsmodel.PollVote) {
}
func (c *Caches) OnInvalidateStatus(status *gtsmodel.Status) {
- // Invalidate stats for this account.
+ // Invalidate cached stats objects for this account.
c.DB.AccountStats.Invalidate("AccountID", status.AccountID)
// Invalidate status ID cached visibility.
c.Visibility.Invalidate("ItemID", status.ID)
+ // Invalidate mute results involving status.
+ c.Mutes.Invalidate("StatusID", status.ID)
+ c.Mutes.Invalidate("ThreadID", status.ThreadID)
+
// Invalidate each media by the IDs we're aware of.
// This must be done as the status table is aware of
// the media IDs in use before the media table is
@@ -277,6 +323,11 @@ func (c *Caches) OnInvalidateStatusFave(fave *gtsmodel.StatusFave) {
c.DB.StatusFaveIDs.Invalidate(fave.StatusID)
}
+func (c *Caches) OnInvalidateThreadMute(mute *gtsmodel.ThreadMute) {
+ // Invalidate cached mute ressults encapsulating this thread and account.
+ c.Mutes.Invalidate("RequesterID,ThreadID", mute.AccountID, mute.ThreadID)
+}
+
func (c *Caches) OnInvalidateToken(token *gtsmodel.Token) {
// Invalidate token's push subscription.
c.DB.WebPushSubscription.Invalidate("ID", token.ID)
@@ -294,6 +345,9 @@ func (c *Caches) OnInvalidateUser(user *gtsmodel.User) {
func (c *Caches) OnInvalidateUserMute(mute *gtsmodel.UserMute) {
// Invalidate source account's user mute lists.
c.DB.UserMuteIDs.Invalidate(mute.AccountID)
+
+ // Invalidate source account's cached mute results.
+ c.Mutes.Invalidate("RequesterID", mute.AccountID)
}
func (c *Caches) OnInvalidateWebPushSubscription(subscription *gtsmodel.WebPushSubscription) {
diff --git a/internal/cache/mutes.go b/internal/cache/mutes.go
new file mode 100644
index 000000000..9ad7736a0
--- /dev/null
+++ b/internal/cache/mutes.go
@@ -0,0 +1,109 @@
+// 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 .
+
+package cache
+
+import (
+ "time"
+
+ "code.superseriousbusiness.org/gotosocial/internal/config"
+ "code.superseriousbusiness.org/gotosocial/internal/log"
+ "codeberg.org/gruf/go-structr"
+)
+
+type MutesCache struct {
+ StructCache[*CachedMute]
+}
+
+func (c *Caches) initMutes() {
+ // Calculate maximum cache size.
+ cap := calculateResultCacheMax(
+ sizeofMute(), // model in-mem size.
+ config.GetCacheMutesMemRatio(),
+ )
+
+ log.Infof(nil, "cache size = %d", cap)
+
+ copyF := func(m1 *CachedMute) *CachedMute {
+ m2 := new(CachedMute)
+ *m2 = *m1
+ return m2
+ }
+
+ c.Mutes.Init(structr.CacheConfig[*CachedMute]{
+ Indices: []structr.IndexConfig{
+ {Fields: "RequesterID,StatusID"},
+ {Fields: "RequesterID,ThreadID", Multiple: true},
+ {Fields: "StatusID", Multiple: true},
+ {Fields: "ThreadID", Multiple: true},
+ {Fields: "RequesterID", Multiple: true},
+ },
+ MaxSize: cap,
+ IgnoreErr: func(err error) bool {
+ // don't cache any errors,
+ // it gets a little too tricky
+ // otherwise with ensuring
+ // errors are cleared out
+ return true
+ },
+ Copy: copyF,
+ })
+}
+
+// CachedMute contains the details
+// of a cached mute lookup.
+type CachedMute struct {
+
+ // StatusID is the ID of the
+ // status this is a result for.
+ StatusID string
+
+ // ThreadID is the ID of the
+ // thread status is a part of.
+ ThreadID string
+
+ // RequesterID is the ID of the requesting
+ // account for this user mute lookup.
+ RequesterID string
+
+ // Mute indicates whether ItemID
+ // is muted by RequesterID.
+ Mute bool
+
+ // MuteExpiry stores the time at which
+ // (if any) the stored mute value expires.
+ MuteExpiry time.Time
+
+ // Notifications indicates whether
+ // this mute should prevent notifications
+ // being shown for ItemID to RequesterID.
+ Notifications bool
+
+ // NotificationExpiry stores the time at which
+ // (if any) the stored notification value expires.
+ NotificationExpiry time.Time
+}
+
+// MuteExpired returns whether the mute value has expired.
+func (m *CachedMute) MuteExpired(now time.Time) bool {
+ return !m.MuteExpiry.IsZero() && !m.MuteExpiry.After(now)
+}
+
+// NotificationExpired returns whether the notification mute value has expired.
+func (m *CachedMute) NotificationExpired(now time.Time) bool {
+ return !m.NotificationExpiry.IsZero() && !m.NotificationExpiry.After(now)
+}
diff --git a/internal/cache/size.go b/internal/cache/size.go
index 7898f9dfd..ef9259f88 100644
--- a/internal/cache/size.go
+++ b/internal/cache/size.go
@@ -150,7 +150,7 @@ func calculateCacheMax(keySz, valSz uintptr, ratio float64) int {
// The inputted memory ratio does not take into account the
// total of all ratios, so divide it here to get perc. ratio.
- totalRatio := ratio / totalOfRatios()
+ totalRatio := ratio / config.GetTotalOfMemRatios()
// TODO: we should also further weight this ratio depending
// on the combined keySz + valSz as a ratio of all available
@@ -172,65 +172,6 @@ func calculateCacheMax(keySz, valSz uintptr, ratio float64) int {
return int(fMaxMem / (fKeySz + fValSz + emptyBucketOverhead + float64(cacheElemOverhead)))
}
-// totalOfRatios returns the total of all cache ratios added together.
-func totalOfRatios() float64 {
-
- // NOTE: this is not performant calculating
- // this every damn time (mainly the mutex unlocks
- // required to access each config var). fortunately
- // we only do this on init so fuck it :D
- return 0 +
- config.GetCacheAccountMemRatio() +
- config.GetCacheAccountNoteMemRatio() +
- config.GetCacheAccountSettingsMemRatio() +
- config.GetCacheAccountStatsMemRatio() +
- config.GetCacheApplicationMemRatio() +
- config.GetCacheBlockMemRatio() +
- config.GetCacheBlockIDsMemRatio() +
- config.GetCacheBoostOfIDsMemRatio() +
- config.GetCacheClientMemRatio() +
- config.GetCacheEmojiMemRatio() +
- config.GetCacheEmojiCategoryMemRatio() +
- config.GetCacheFilterMemRatio() +
- config.GetCacheFilterKeywordMemRatio() +
- config.GetCacheFilterStatusMemRatio() +
- config.GetCacheFollowMemRatio() +
- config.GetCacheFollowIDsMemRatio() +
- config.GetCacheFollowRequestMemRatio() +
- config.GetCacheFollowRequestIDsMemRatio() +
- config.GetCacheFollowingTagIDsMemRatio() +
- config.GetCacheInReplyToIDsMemRatio() +
- config.GetCacheInstanceMemRatio() +
- config.GetCacheInteractionRequestMemRatio() +
- config.GetCacheListMemRatio() +
- config.GetCacheListIDsMemRatio() +
- config.GetCacheListedIDsMemRatio() +
- config.GetCacheMarkerMemRatio() +
- config.GetCacheMediaMemRatio() +
- config.GetCacheMentionMemRatio() +
- config.GetCacheMoveMemRatio() +
- config.GetCacheNotificationMemRatio() +
- config.GetCachePollMemRatio() +
- config.GetCachePollVoteMemRatio() +
- config.GetCachePollVoteIDsMemRatio() +
- config.GetCacheReportMemRatio() +
- config.GetCacheSinBinStatusMemRatio() +
- config.GetCacheStatusMemRatio() +
- config.GetCacheStatusBookmarkMemRatio() +
- config.GetCacheStatusBookmarkIDsMemRatio() +
- config.GetCacheStatusFaveMemRatio() +
- config.GetCacheStatusFaveIDsMemRatio() +
- config.GetCacheTagMemRatio() +
- config.GetCacheThreadMuteMemRatio() +
- config.GetCacheTokenMemRatio() +
- config.GetCacheTombstoneMemRatio() +
- config.GetCacheUserMemRatio() +
- config.GetCacheUserMuteMemRatio() +
- config.GetCacheUserMuteIDsMemRatio() +
- config.GetCacheWebfingerMemRatio() +
- config.GetCacheVisibilityMemRatio()
-}
-
func sizeofAccount() uintptr {
return uintptr(size.Of(>smodel.Account{
ID: exampleID,
@@ -769,6 +710,18 @@ func sizeofTombstone() uintptr {
}))
}
+func sizeofMute() uintptr {
+ return uintptr(size.Of(&CachedMute{
+ StatusID: exampleID,
+ ThreadID: exampleID,
+ RequesterID: exampleID,
+ Mute: true,
+ MuteExpiry: exampleTime,
+ Notifications: true,
+ NotificationExpiry: exampleTime,
+ }))
+}
+
func sizeofVisibility() uintptr {
return uintptr(size.Of(&CachedVisibility{
ItemID: exampleID,
diff --git a/internal/cache/visibility.go b/internal/cache/visibility.go
index 63927cf08..3797ab701 100644
--- a/internal/cache/visibility.go
+++ b/internal/cache/visibility.go
@@ -34,7 +34,7 @@ func (c *Caches) initVisibility() {
config.GetCacheVisibilityMemRatio(),
)
- log.Infof(nil, "Visibility cache size = %d", cap)
+ log.Infof(nil, "cache size = %d", cap)
copyF := func(v1 *CachedVisibility) *CachedVisibility {
v2 := new(CachedVisibility)
@@ -73,12 +73,16 @@ const (
VisibilityTypePublic = VisibilityType('p')
)
-// CachedVisibility represents a cached visibility lookup value.
+// CachedVisibility represents a
+// cached visibility lookup value.
type CachedVisibility struct {
- // ItemID is the ID of the item in question (status / account).
+
+ // ItemID is the ID of the item
+ // in question (status / account).
ItemID string
- // RequesterID is the ID of the requesting account for this visibility lookup.
+ // RequesterID is the ID of the requesting
+ // account for this visibility lookup.
RequesterID string
// Type is the visibility lookup type.
diff --git a/internal/config/config.go b/internal/config/config.go
index d9293e062..303bf8266 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -257,6 +257,7 @@ type CacheConfiguration struct {
WebfingerMemRatio float64 `name:"webfinger-mem-ratio"`
WebPushSubscriptionMemRatio float64 `name:"web-push-subscription-mem-ratio"`
WebPushSubscriptionIDsMemRatio float64 `name:"web-push-subscription-ids-mem-ratio"`
+ MutesMemRatio float64 `name:"mutes-mem-ratio"`
VisibilityMemRatio float64 `name:"visibility-mem-ratio"`
}
diff --git a/internal/config/defaults.go b/internal/config/defaults.go
index ad124e90f..43168a471 100644
--- a/internal/config/defaults.go
+++ b/internal/config/defaults.go
@@ -229,6 +229,7 @@ var Defaults = Configuration{
WebPushSubscriptionMemRatio: 1,
WebPushSubscriptionIDsMemRatio: 1,
VisibilityMemRatio: 2,
+ MutesMemRatio: 2,
},
HTTPClient: HTTPClientConfiguration{
diff --git a/internal/config/gen/gen.go b/internal/config/gen/gen.go
index b3532caf8..faede7987 100644
--- a/internal/config/gen/gen.go
+++ b/internal/config/gen/gen.go
@@ -24,6 +24,7 @@ import (
"os"
"os/exec"
"reflect"
+ "slices"
"strings"
"time"
@@ -485,6 +486,24 @@ func generateGetSetters(out io.Writer, fields []ConfigField) {
fprintf(out, "// Set%s safely sets the value for global configuration '%s' field\n", name, field.Path)
fprintf(out, "func Set%[1]s(v %[2]s) { global.Set%[1]s(v) }\n\n", name, fieldType)
}
+
+ // Separate out the config fields (from a clone!!!) to get only the 'mem-ratio' members.
+ memFields := slices.DeleteFunc(slices.Clone(fields), func(field ConfigField) bool {
+ return !strings.Contains(field.Path, "MemRatio")
+ })
+
+ fprintf(out, "// GetTotalOfMemRatios safely fetches the combined value for all the state's mem ratio fields\n")
+ fprintf(out, "func (st *ConfigState) GetTotalOfMemRatios() (total float64) {\n")
+ fprintf(out, "\tst.mutex.RLock()\n")
+ for _, field := range memFields {
+ fprintf(out, "\ttotal += st.config.%s\n", field.Path)
+ }
+ fprintf(out, "\tst.mutex.RUnlock()\n")
+ fprintf(out, "\treturn\n")
+ fprintf(out, "}\n\n")
+
+ fprintf(out, "// GetTotalOfMemRatios safely fetches the combined value for all the global state's mem ratio fields\n")
+ fprintf(out, "func GetTotalOfMemRatios() (total float64) { return global.GetTotalOfMemRatios() }\n\n")
}
func generateMapFlattener(out io.Writer, fields []ConfigField) {
diff --git a/internal/config/helpers.gen.go b/internal/config/helpers.gen.go
index 0803bab08..e710a9dc2 100644
--- a/internal/config/helpers.gen.go
+++ b/internal/config/helpers.gen.go
@@ -145,7 +145,7 @@ func (cfg *Configuration) RegisterFlags(flags *pflag.FlagSet) {
flags.Int("advanced-throttling-multiplier", cfg.Advanced.Throttling.Multiplier, "Multiplier to use per cpu for http request throttling. 0 or less turns throttling off.")
flags.Duration("advanced-throttling-retry-after", cfg.Advanced.Throttling.RetryAfter, "Retry-After duration response to send for throttled requests.")
flags.Bool("advanced-scraper-deterrence-enabled", cfg.Advanced.ScraperDeterrence.Enabled, "Enable proof-of-work based scraper deterrence on profile / status pages")
- flags.Uint32("advanced-scraper-deterrence-difficulty", cfg.Advanced.ScraperDeterrence.Difficulty, "The proof-of-work difficulty, which determines how many leading zeros to try solve in hash solutions.")
+ flags.Uint32("advanced-scraper-deterrence-difficulty", cfg.Advanced.ScraperDeterrence.Difficulty, "The proof-of-work difficulty, which determines roughly how many hash-encode rounds required of each client.")
flags.StringSlice("http-client-allow-ips", cfg.HTTPClient.AllowIPs, "")
flags.StringSlice("http-client-block-ips", cfg.HTTPClient.BlockIPs, "")
flags.Duration("http-client-timeout", cfg.HTTPClient.Timeout, "")
@@ -206,11 +206,12 @@ func (cfg *Configuration) RegisterFlags(flags *pflag.FlagSet) {
flags.Float64("cache-webfinger-mem-ratio", cfg.Cache.WebfingerMemRatio, "")
flags.Float64("cache-web-push-subscription-mem-ratio", cfg.Cache.WebPushSubscriptionMemRatio, "")
flags.Float64("cache-web-push-subscription-ids-mem-ratio", cfg.Cache.WebPushSubscriptionIDsMemRatio, "")
+ flags.Float64("cache-mutes-mem-ratio", cfg.Cache.MutesMemRatio, "")
flags.Float64("cache-visibility-mem-ratio", cfg.Cache.VisibilityMemRatio, "")
}
func (cfg *Configuration) MarshalMap() map[string]any {
- cfgmap := make(map[string]any, 184)
+ cfgmap := make(map[string]any, 186)
cfgmap["log-level"] = cfg.LogLevel
cfgmap["log-timestamp-format"] = cfg.LogTimestampFormat
cfgmap["log-db-queries"] = cfg.LogDbQueries
@@ -388,6 +389,7 @@ func (cfg *Configuration) MarshalMap() map[string]any {
cfgmap["cache-webfinger-mem-ratio"] = cfg.Cache.WebfingerMemRatio
cfgmap["cache-web-push-subscription-mem-ratio"] = cfg.Cache.WebPushSubscriptionMemRatio
cfgmap["cache-web-push-subscription-ids-mem-ratio"] = cfg.Cache.WebPushSubscriptionIDsMemRatio
+ cfgmap["cache-mutes-mem-ratio"] = cfg.Cache.MutesMemRatio
cfgmap["cache-visibility-mem-ratio"] = cfg.Cache.VisibilityMemRatio
cfgmap["username"] = cfg.AdminAccountUsername
cfgmap["email"] = cfg.AdminAccountEmail
@@ -1855,6 +1857,14 @@ func (cfg *Configuration) UnmarshalMap(cfgmap map[string]any) error {
}
}
+ if ival, ok := cfgmap["cache-mutes-mem-ratio"]; ok {
+ var err error
+ cfg.Cache.MutesMemRatio, err = cast.ToFloat64E(ival)
+ if err != nil {
+ return fmt.Errorf("error casting %#v -> float64 for 'cache-mutes-mem-ratio': %w", ival, err)
+ }
+ }
+
if ival, ok := cfgmap["cache-visibility-mem-ratio"]; ok {
var err error
cfg.Cache.VisibilityMemRatio, err = cast.ToFloat64E(ival)
@@ -6385,6 +6395,31 @@ func SetCacheWebPushSubscriptionIDsMemRatio(v float64) {
global.SetCacheWebPushSubscriptionIDsMemRatio(v)
}
+// CacheMutesMemRatioFlag returns the flag name for the 'Cache.MutesMemRatio' field
+func CacheMutesMemRatioFlag() string { return "cache-mutes-mem-ratio" }
+
+// GetCacheMutesMemRatio safely fetches the Configuration value for state's 'Cache.MutesMemRatio' field
+func (st *ConfigState) GetCacheMutesMemRatio() (v float64) {
+ st.mutex.RLock()
+ v = st.config.Cache.MutesMemRatio
+ st.mutex.RUnlock()
+ return
+}
+
+// SetCacheMutesMemRatio safely sets the Configuration value for state's 'Cache.MutesMemRatio' field
+func (st *ConfigState) SetCacheMutesMemRatio(v float64) {
+ st.mutex.Lock()
+ defer st.mutex.Unlock()
+ st.config.Cache.MutesMemRatio = v
+ st.reloadToViper()
+}
+
+// GetCacheMutesMemRatio safely fetches the value for global configuration 'Cache.MutesMemRatio' field
+func GetCacheMutesMemRatio() float64 { return global.GetCacheMutesMemRatio() }
+
+// SetCacheMutesMemRatio safely sets the value for global configuration 'Cache.MutesMemRatio' field
+func SetCacheMutesMemRatio(v float64) { global.SetCacheMutesMemRatio(v) }
+
// CacheVisibilityMemRatioFlag returns the flag name for the 'Cache.VisibilityMemRatio' field
func CacheVisibilityMemRatioFlag() string { return "cache-visibility-mem-ratio" }
@@ -6585,6 +6620,73 @@ func GetAdminMediaListRemoteOnly() bool { return global.GetAdminMediaListRemoteO
// SetAdminMediaListRemoteOnly safely sets the value for global configuration 'AdminMediaListRemoteOnly' field
func SetAdminMediaListRemoteOnly(v bool) { global.SetAdminMediaListRemoteOnly(v) }
+// GetTotalOfMemRatios safely fetches the combined value for all the state's mem ratio fields
+func (st *ConfigState) GetTotalOfMemRatios() (total float64) {
+ st.mutex.RLock()
+ total += st.config.Cache.AccountMemRatio
+ total += st.config.Cache.AccountNoteMemRatio
+ total += st.config.Cache.AccountSettingsMemRatio
+ total += st.config.Cache.AccountStatsMemRatio
+ total += st.config.Cache.ApplicationMemRatio
+ total += st.config.Cache.BlockMemRatio
+ total += st.config.Cache.BlockIDsMemRatio
+ total += st.config.Cache.BoostOfIDsMemRatio
+ total += st.config.Cache.ClientMemRatio
+ total += st.config.Cache.ConversationMemRatio
+ total += st.config.Cache.ConversationLastStatusIDsMemRatio
+ total += st.config.Cache.DomainPermissionDraftMemRation
+ total += st.config.Cache.DomainPermissionSubscriptionMemRation
+ total += st.config.Cache.EmojiMemRatio
+ total += st.config.Cache.EmojiCategoryMemRatio
+ total += st.config.Cache.FilterMemRatio
+ total += st.config.Cache.FilterKeywordMemRatio
+ total += st.config.Cache.FilterStatusMemRatio
+ total += st.config.Cache.FollowMemRatio
+ total += st.config.Cache.FollowIDsMemRatio
+ total += st.config.Cache.FollowRequestMemRatio
+ total += st.config.Cache.FollowRequestIDsMemRatio
+ total += st.config.Cache.FollowingTagIDsMemRatio
+ total += st.config.Cache.InReplyToIDsMemRatio
+ total += st.config.Cache.InstanceMemRatio
+ total += st.config.Cache.InteractionRequestMemRatio
+ total += st.config.Cache.ListMemRatio
+ total += st.config.Cache.ListIDsMemRatio
+ total += st.config.Cache.ListedIDsMemRatio
+ total += st.config.Cache.MarkerMemRatio
+ total += st.config.Cache.MediaMemRatio
+ total += st.config.Cache.MentionMemRatio
+ total += st.config.Cache.MoveMemRatio
+ total += st.config.Cache.NotificationMemRatio
+ total += st.config.Cache.PollMemRatio
+ total += st.config.Cache.PollVoteMemRatio
+ total += st.config.Cache.PollVoteIDsMemRatio
+ total += st.config.Cache.ReportMemRatio
+ total += st.config.Cache.SinBinStatusMemRatio
+ total += st.config.Cache.StatusMemRatio
+ total += st.config.Cache.StatusBookmarkMemRatio
+ total += st.config.Cache.StatusBookmarkIDsMemRatio
+ total += st.config.Cache.StatusEditMemRatio
+ total += st.config.Cache.StatusFaveMemRatio
+ total += st.config.Cache.StatusFaveIDsMemRatio
+ total += st.config.Cache.TagMemRatio
+ total += st.config.Cache.ThreadMuteMemRatio
+ total += st.config.Cache.TokenMemRatio
+ total += st.config.Cache.TombstoneMemRatio
+ total += st.config.Cache.UserMemRatio
+ total += st.config.Cache.UserMuteMemRatio
+ total += st.config.Cache.UserMuteIDsMemRatio
+ total += st.config.Cache.WebfingerMemRatio
+ total += st.config.Cache.WebPushSubscriptionMemRatio
+ total += st.config.Cache.WebPushSubscriptionIDsMemRatio
+ total += st.config.Cache.MutesMemRatio
+ total += st.config.Cache.VisibilityMemRatio
+ st.mutex.RUnlock()
+ return
+}
+
+// GetTotalOfMemRatios safely fetches the combined value for all the global state's mem ratio fields
+func GetTotalOfMemRatios() (total float64) { return global.GetTotalOfMemRatios() }
+
func flattenConfigMap(cfgmap map[string]any) {
nestedKeys := make(map[string]struct{})
for _, key := range [][]string{
@@ -7363,6 +7465,17 @@ func flattenConfigMap(cfgmap map[string]any) {
}
}
+ for _, key := range [][]string{
+ {"cache", "mutes-mem-ratio"},
+ } {
+ ival, ok := mapGet(cfgmap, key...)
+ if ok {
+ cfgmap["cache-mutes-mem-ratio"] = ival
+ nestedKeys[key[0]] = struct{}{}
+ break
+ }
+ }
+
for _, key := range [][]string{
{"cache", "visibility-mem-ratio"},
} {
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 .
+
+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 .
+
+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 .
+
+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 .
+
+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 .
+
+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 .
-
-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
}
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.
+ // Check whether origin account is muted by target account.
+ muted, err := s.MuteFilter.AccountNotificationsMuted(ctx,
+ targetAccount,
+ originAccount,
+ )
+ if err != nil {
+ return gtserror.Newf("error checking account mute: %w", err)
+ }
+
+ if muted {
+ // Don't notify.
+ return nil
+ }
+
+ 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)
}
- 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)
- }
- compiledMutes := usermute.NewCompiledUserMuteList(mutes)
-
- apiNotif, err := s.Converter.NotificationToAPINotification(ctx, notif, filters, compiledMutes)
- if err != nil {
- if errors.Is(err, status.ErrHideStatus) {
- return nil
- }
+ // 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,
diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go
index bef44e32f..ecf817c66 100644
--- a/internal/typeutils/internaltofrontend.go
+++ b/internal/typeutils/internaltofrontend.go
@@ -32,7 +32,6 @@ import (
"code.superseriousbusiness.org/gotosocial/internal/config"
"code.superseriousbusiness.org/gotosocial/internal/db"
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/id"
@@ -851,7 +850,6 @@ func (c *Converter) StatusToAPIStatus(
requestingAccount *gtsmodel.Account,
filterContext statusfilter.FilterContext,
filters []*gtsmodel.Filter,
- mutes *usermute.CompiledUserMuteList,
) (*apimodel.Status, error) {
return c.statusToAPIStatus(
ctx,
@@ -859,7 +857,6 @@ func (c *Converter) StatusToAPIStatus(
requestingAccount,
filterContext,
filters,
- mutes,
true,
true,
)
@@ -875,7 +872,6 @@ func (c *Converter) statusToAPIStatus(
requestingAccount *gtsmodel.Account,
filterContext statusfilter.FilterContext,
filters []*gtsmodel.Filter,
- mutes *usermute.CompiledUserMuteList,
placeholdAttachments bool,
addPendingNote bool,
) (*apimodel.Status, error) {
@@ -885,7 +881,6 @@ func (c *Converter) statusToAPIStatus(
requestingAccount, // Can be nil.
filterContext, // Can be empty.
filters,
- mutes,
)
if err != nil {
return nil, err
@@ -952,77 +947,18 @@ func (c *Converter) statusToAPIFilterResults(
requestingAccount *gtsmodel.Account,
filterContext statusfilter.FilterContext,
filters []*gtsmodel.Filter,
- mutes *usermute.CompiledUserMuteList,
) ([]apimodel.FilterResult, error) {
// 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 {
+ if filterContext == "" || (len(filters) == 0) || s.AccountID == requestingAccount.ID {
return nil, nil
}
- // Both mutes and filters can expire.
+ // Both mutes and
+ // filters can expire.
now := time.Now()
- // If requesting account mutes the author (taking boosts into account), hide the status.
- if (s.BoostOfAccountID != "" && mutes.Matches(s.BoostOfAccountID, filterContext, now)) ||
- 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.visFilter.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.
- if len(filters) == 0 {
- // If it can't be filtered because there are no filters, we're done.
- return nil, nil
- }
-
// Key this status based on ID + last updated time,
// to ensure we always filter on latest version.
statusKey := s.ID + strconv.FormatInt(s.UpdatedAt().Unix(), 10)
@@ -1130,7 +1066,6 @@ func (c *Converter) StatusToWebStatus(
nil, // No authed requester.
statusfilter.FilterContextNone, // No filters.
nil, // No filters.
- nil, // No mutes.
)
if err != nil {
return nil, err
@@ -1301,7 +1236,6 @@ func (c *Converter) statusToFrontend(
requestingAccount *gtsmodel.Account,
filterContext statusfilter.FilterContext,
filters []*gtsmodel.Filter,
- mutes *usermute.CompiledUserMuteList,
) (
*apimodel.Status,
error,
@@ -1311,7 +1245,6 @@ func (c *Converter) statusToFrontend(
requestingAccount,
filterContext,
filters,
- mutes,
)
if err != nil {
return nil, err
@@ -1323,7 +1256,6 @@ func (c *Converter) statusToFrontend(
requestingAccount,
filterContext,
filters,
- mutes,
)
if errors.Is(err, statusfilter.ErrHideStatus) {
// If we'd hide the original status, hide the boost.
@@ -1357,7 +1289,6 @@ func (c *Converter) baseStatusToFrontend(
requestingAccount *gtsmodel.Account,
filterContext statusfilter.FilterContext,
filters []*gtsmodel.Filter,
- mutes *usermute.CompiledUserMuteList,
) (
*apimodel.Status,
error,
@@ -1535,7 +1466,7 @@ func (c *Converter) baseStatusToFrontend(
}
// Apply filters.
- filterResults, err := c.statusToAPIFilterResults(ctx, s, requestingAccount, filterContext, filters, mutes)
+ filterResults, err := c.statusToAPIFilterResults(ctx, s, requestingAccount, filterContext, filters)
if err != nil {
if errors.Is(err, statusfilter.ErrHideStatus) {
return nil, err
@@ -2052,7 +1983,6 @@ func (c *Converter) NotificationToAPINotification(
ctx context.Context,
n *gtsmodel.Notification,
filters []*gtsmodel.Filter,
- mutes *usermute.CompiledUserMuteList,
) (*apimodel.Notification, error) {
// Ensure notif populated.
if err := c.state.DB.PopulateNotification(ctx, n); err != nil {
@@ -2072,7 +2002,7 @@ func (c *Converter) NotificationToAPINotification(
ctx, n.Status,
n.TargetAccount,
statusfilter.FilterContextNotifications,
- filters, mutes,
+ filters,
)
if err != nil && !errors.Is(err, statusfilter.ErrHideStatus) {
return nil, gtserror.Newf("error converting status to api: %w", err)
@@ -2108,7 +2038,6 @@ func (c *Converter) ConversationToAPIConversation(
conversation *gtsmodel.Conversation,
requester *gtsmodel.Account,
filters []*gtsmodel.Filter,
- mutes *usermute.CompiledUserMuteList,
) (*apimodel.Conversation, error) {
apiConversation := &apimodel.Conversation{
ID: conversation.ID,
@@ -2125,7 +2054,6 @@ func (c *Converter) ConversationToAPIConversation(
requester,
statusfilter.FilterContextNotifications,
filters,
- mutes,
)
if err != nil && !errors.Is(err, statusfilter.ErrHideStatus) {
return nil, gtserror.Newf(
@@ -2394,7 +2322,6 @@ func (c *Converter) ReportToAdminAPIReport(ctx context.Context, r *gtsmodel.Repo
requestingAccount,
statusfilter.FilterContextNone,
nil, // No filters.
- nil, // No mutes.
true, // Placehold unknown attachments.
// Don't add note about
@@ -3057,7 +2984,6 @@ func (c *Converter) InteractionReqToAPIInteractionReq(
requestingAcct,
statusfilter.FilterContextNone,
nil, // No filters.
- nil, // No mutes.
)
if err != nil {
err := gtserror.Newf("error converting interacted status: %w", err)
@@ -3072,7 +2998,6 @@ func (c *Converter) InteractionReqToAPIInteractionReq(
requestingAcct,
statusfilter.FilterContextNone,
nil, // No filters.
- nil, // No mutes.
true, // Placehold unknown attachments.
// Don't add note about pending;
diff --git a/internal/typeutils/internaltofrontend_test.go b/internal/typeutils/internaltofrontend_test.go
index c1b29ad21..d19bd6a2f 100644
--- a/internal/typeutils/internaltofrontend_test.go
+++ b/internal/typeutils/internaltofrontend_test.go
@@ -28,7 +28,6 @@ import (
"code.superseriousbusiness.org/gotosocial/internal/config"
"code.superseriousbusiness.org/gotosocial/internal/db"
statusfilter "code.superseriousbusiness.org/gotosocial/internal/filter/status"
- "code.superseriousbusiness.org/gotosocial/internal/filter/usermute"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"code.superseriousbusiness.org/gotosocial/internal/util"
"code.superseriousbusiness.org/gotosocial/testrig"
@@ -466,7 +465,7 @@ func (suite *InternalToFrontendTestSuite) TestLocalInstanceAccountToFrontendBloc
func (suite *InternalToFrontendTestSuite) TestStatusToFrontend() {
testStatus := suite.testStatuses["admin_account_status_1"]
requestingAccount := suite.testAccounts["local_account_1"]
- apiStatus, err := suite.typeconverter.StatusToAPIStatus(suite.T().Context(), testStatus, requestingAccount, statusfilter.FilterContextNone, nil, nil)
+ apiStatus, err := suite.typeconverter.StatusToAPIStatus(suite.T().Context(), testStatus, requestingAccount, statusfilter.FilterContextNone, nil)
suite.NoError(err)
b, err := json.MarshalIndent(apiStatus, "", " ")
@@ -629,7 +628,7 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontendHTMLContentWarning
testStatus.ContentWarning = `
First paragraph of content warning
Here's the title!
Big boobs
Tee hee!
Some more text
And a bunch more
Hasta la victoria siempre!
`
requestingAccount := suite.testAccounts["local_account_1"]
- apiStatus, err := suite.typeconverter.StatusToAPIStatus(suite.T().Context(), testStatus, requestingAccount, statusfilter.FilterContextNone, nil, nil)
+ apiStatus, err := suite.typeconverter.StatusToAPIStatus(suite.T().Context(), testStatus, requestingAccount, statusfilter.FilterContextNone, nil)
suite.NoError(err)
b, err := json.MarshalIndent(apiStatus, "", " ")
@@ -795,7 +794,7 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontendApplicationDeleted
}
requestingAccount := suite.testAccounts["local_account_1"]
- apiStatus, err := suite.typeconverter.StatusToAPIStatus(ctx, testStatus, requestingAccount, statusfilter.FilterContextNone, nil, nil)
+ apiStatus, err := suite.typeconverter.StatusToAPIStatus(ctx, testStatus, requestingAccount, statusfilter.FilterContextNone, nil)
suite.NoError(err)
b, err := json.MarshalIndent(apiStatus, "", " ")
@@ -984,7 +983,6 @@ func (suite *InternalToFrontendTestSuite) filteredStatusToFrontend(action gtsmod
requestingAccount,
statusfilter.FilterContextHome,
requestingAccountFilters,
- nil,
)
}
@@ -1538,7 +1536,6 @@ func (suite *InternalToFrontendTestSuite) testHashtagFilteredStatusToFrontend(wh
requestingAccount,
statusfilter.FilterContextHome,
[]*gtsmodel.Filter{filter},
- nil,
)
if err != nil {
suite.FailNow(err.Error())
@@ -1563,102 +1560,11 @@ func (suite *InternalToFrontendTestSuite) TestHashtagAnywhereFilteredBoostToFron
suite.testHashtagFilteredStatusToFrontend(false, true)
}
-// 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(
- suite.T().Context(),
- 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(suite.T().Context(), testStatus)
- if err != nil {
- suite.FailNow(err.Error())
- }
-
- // Convert the status to API format, which should fail.
- _, err = suite.typeconverter.StatusToAPIStatus(
- suite.T().Context(),
- testStatus,
- requestingAccount,
- statusfilter.FilterContextHome,
- nil,
- mutes,
- )
- suite.ErrorIs(err, statusfilter.ErrHideStatus)
-}
-
-func (suite *InternalToFrontendTestSuite) TestMutedBoostStatusToFrontend() {
- mutedAccount := suite.testAccounts["local_account_2"]
- testStatus := suite.testStatuses["admin_account_status_1"]
- testStatus.BoostOfID = suite.testStatuses["local_account_2_status_1"].ID
- testStatus.BoostOfAccountID = 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(suite.T().Context(), testStatus)
- if err != nil {
- suite.FailNow(err.Error())
- }
-
- // Convert the status to API format, which should fail.
- _, err = suite.typeconverter.StatusToAPIStatus(
- suite.T().Context(),
- testStatus,
- requestingAccount,
- statusfilter.FilterContextHome,
- nil,
- mutes,
- )
- suite.ErrorIs(err, statusfilter.ErrHideStatus)
-}
-
func (suite *InternalToFrontendTestSuite) TestStatusToFrontendUnknownAttachments() {
testStatus := suite.testStatuses["remote_account_2_status_1"]
requestingAccount := suite.testAccounts["admin_account"]
- apiStatus, err := suite.typeconverter.StatusToAPIStatus(suite.T().Context(), testStatus, requestingAccount, statusfilter.FilterContextNone, nil, nil)
+ apiStatus, err := suite.typeconverter.StatusToAPIStatus(suite.T().Context(), testStatus, requestingAccount, statusfilter.FilterContextNone, nil)
suite.NoError(err)
b, err := json.MarshalIndent(apiStatus, "", " ")
@@ -1985,7 +1891,7 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontendUnknownLanguage()
*testStatus = *suite.testStatuses["admin_account_status_1"]
testStatus.Language = ""
requestingAccount := suite.testAccounts["local_account_1"]
- apiStatus, err := suite.typeconverter.StatusToAPIStatus(suite.T().Context(), testStatus, requestingAccount, statusfilter.FilterContextNone, nil, nil)
+ apiStatus, err := suite.typeconverter.StatusToAPIStatus(suite.T().Context(), testStatus, requestingAccount, statusfilter.FilterContextNone, nil)
suite.NoError(err)
b, err := json.MarshalIndent(apiStatus, "", " ")
@@ -2146,7 +2052,7 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontendPartialInteraction
*testStatus = *suite.testStatuses["local_account_1_status_3"]
testStatus.Language = ""
requestingAccount := suite.testAccounts["admin_account"]
- apiStatus, err := suite.typeconverter.StatusToAPIStatus(suite.T().Context(), testStatus, requestingAccount, statusfilter.FilterContextNone, nil, nil)
+ apiStatus, err := suite.typeconverter.StatusToAPIStatus(suite.T().Context(), testStatus, requestingAccount, statusfilter.FilterContextNone, nil)
suite.NoError(err)
b, err := json.MarshalIndent(apiStatus, "", " ")
@@ -2261,7 +2167,6 @@ func (suite *InternalToFrontendTestSuite) TestStatusToAPIStatusPendingApproval()
requestingAccount,
statusfilter.FilterContextNone,
nil,
- nil,
)
if err != nil {
suite.FailNow(err.Error())
@@ -4020,11 +3925,10 @@ func (suite *InternalToFrontendTestSuite) TestIntReqToAPI() {
func (suite *InternalToFrontendTestSuite) TestConversationToAPISelfConvo() {
var (
- ctx = suite.T().Context()
- requester = suite.testAccounts["local_account_1"]
- lastStatus = suite.testStatuses["local_account_1_status_1"]
- filters []*gtsmodel.Filter = nil
- mutes *usermute.CompiledUserMuteList = nil
+ ctx = suite.T().Context()
+ requester = suite.testAccounts["local_account_1"]
+ lastStatus = suite.testStatuses["local_account_1_status_1"]
+ filters []*gtsmodel.Filter = nil
)
convo := >smodel.Conversation{
@@ -4043,7 +3947,6 @@ func (suite *InternalToFrontendTestSuite) TestConversationToAPISelfConvo() {
convo,
requester,
filters,
- mutes,
)
if err != nil {
suite.FailNow(err.Error())
@@ -4195,11 +4098,10 @@ func (suite *InternalToFrontendTestSuite) TestConversationToAPISelfConvo() {
func (suite *InternalToFrontendTestSuite) TestConversationToAPI() {
var (
- ctx = suite.T().Context()
- requester = suite.testAccounts["local_account_1"]
- lastStatus = suite.testStatuses["local_account_1_status_1"]
- filters []*gtsmodel.Filter = nil
- mutes *usermute.CompiledUserMuteList = nil
+ ctx = suite.T().Context()
+ requester = suite.testAccounts["local_account_1"]
+ lastStatus = suite.testStatuses["local_account_1_status_1"]
+ filters []*gtsmodel.Filter = nil
)
convo := >smodel.Conversation{
@@ -4220,7 +4122,6 @@ func (suite *InternalToFrontendTestSuite) TestConversationToAPI() {
convo,
requester,
filters,
- mutes,
)
if err != nil {
suite.FailNow(err.Error())
diff --git a/internal/webpush/realsender.go b/internal/webpush/realsender.go
index 075927095..99472c815 100644
--- a/internal/webpush/realsender.go
+++ b/internal/webpush/realsender.go
@@ -29,7 +29,6 @@ import (
apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
"code.superseriousbusiness.org/gotosocial/internal/config"
- "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"
@@ -49,25 +48,23 @@ type realSender struct {
func (r *realSender) Send(
ctx context.Context,
- notification *gtsmodel.Notification,
- filters []*gtsmodel.Filter,
- mutes *usermute.CompiledUserMuteList,
+ notif *gtsmodel.Notification,
+ apiNotif *apimodel.Notification,
) error {
+ // Get notification target.
+ target := notif.TargetAccount
+
// Load subscriptions.
- subscriptions, err := r.state.DB.GetWebPushSubscriptionsByAccountID(ctx, notification.TargetAccountID)
+ subscriptions, err := r.state.DB.GetWebPushSubscriptionsByAccountID(ctx, target.ID)
if err != nil {
- return gtserror.Newf(
- "error getting Web Push subscriptions for account %s: %w",
- notification.TargetAccountID,
- err,
- )
+ return gtserror.Newf("error getting Web Push subscriptions for account %s: %w", target.URI, err)
}
// Subscriptions we're actually going to send to.
relevantSubscriptions := slices.DeleteFunc(
subscriptions,
func(subscription *gtsmodel.WebPushSubscription) bool {
- return r.shouldSkipSubscription(ctx, notification, subscription)
+ return r.shouldSkipSubscription(ctx, notif, subscription)
},
)
if len(relevantSubscriptions) == 0 {
@@ -80,31 +77,28 @@ func (r *realSender) Send(
return gtserror.Newf("error getting VAPID key pair: %w", err)
}
- // Get target account settings.
- targetAccountSettings, err := r.state.DB.GetAccountSettings(ctx, notification.TargetAccountID)
- if err != nil {
- return gtserror.Newf("error getting settings for account %s: %w", notification.TargetAccountID, err)
- }
+ if target.Settings == nil {
+ // Ensure the target account's settings are populated.
+ settings, err := r.state.DB.GetAccountSettings(ctx, target.ID)
+ if err != nil {
+ return gtserror.Newf("error getting settings for account %s: %w", target.URI, err)
+ }
- // Get API representations of notification and accounts involved.
- apiNotification, err := r.converter.NotificationToAPINotification(ctx, notification, filters, mutes)
- if err != nil {
- return gtserror.Newf("error converting notification %s to API representation: %w", notification.ID, err)
+ // Set target's settings.
+ target.Settings = settings
}
// Queue up a .Send() call for each relevant subscription.
for _, subscription := range relevantSubscriptions {
r.state.Workers.WebPush.Queue.Push(func(ctx context.Context) {
- if err := r.sendToSubscription(
- ctx,
+ if err := r.sendToSubscription(ctx,
vapidKeyPair,
- targetAccountSettings,
+ target.Settings,
subscription,
- notification,
- apiNotification,
+ notif,
+ apiNotif,
); err != nil {
- log.Errorf(
- ctx,
+ log.Errorf(ctx,
"error sending Web Push notification for subscription with token ID %s: %v",
subscription.TokenID,
err,
@@ -137,8 +131,7 @@ func (r *realSender) shouldSkipSubscription(
// Allow if the subscription account follows the notifying account.
isFollowing, err := r.state.DB.IsFollowing(ctx, subscription.AccountID, notification.OriginAccountID)
if err != nil {
- log.Errorf(
- ctx,
+ log.Errorf(ctx,
"error checking whether account %s follows account %s: %v",
subscription.AccountID,
notification.OriginAccountID,
@@ -152,8 +145,7 @@ func (r *realSender) shouldSkipSubscription(
// Allow if the notifying account follows the subscription account.
isFollowing, err := r.state.DB.IsFollowing(ctx, notification.OriginAccountID, subscription.AccountID)
if err != nil {
- log.Errorf(
- ctx,
+ log.Errorf(ctx,
"error checking whether account %s follows account %s: %v",
notification.OriginAccountID,
subscription.AccountID,
@@ -168,8 +160,7 @@ func (r *realSender) shouldSkipSubscription(
return true
default:
- log.Errorf(
- ctx,
+ log.Errorf(ctx,
"unknown Web Push notification policy for subscription with token ID %s: %d",
subscription.TokenID,
subscription.Policy,
diff --git a/internal/webpush/realsender_test.go b/internal/webpush/realsender_test.go
index bddcbbc28..a404c166f 100644
--- a/internal/webpush/realsender_test.go
+++ b/internal/webpush/realsender_test.go
@@ -32,6 +32,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"
@@ -123,6 +124,7 @@ func (suite *RealSenderStandardTestSuite) SetupTest() {
suite.emailSender,
suite.webPushSender,
visibility.NewFilter(&suite.state),
+ mutes.NewFilter(&suite.state),
interaction.NewFilter(&suite.state),
)
testrig.StartWorkers(&suite.state, suite.processor.Workers())
@@ -188,8 +190,14 @@ func (suite *RealSenderStandardTestSuite) simulatePushNotification(
}, nil
}
+ apiNotif, err := suite.typeconverter.NotificationToAPINotification(ctx, notification, nil)
+ suite.NoError(err)
+
// Send the push notification.
- sendError := suite.webPushSender.Send(ctx, notification, nil, nil)
+ sendError := suite.webPushSender.Send(ctx,
+ notification,
+ apiNotif,
+ )
// Wait for it to be sent or for the context to time out.
bodyClosed := false
diff --git a/internal/webpush/sender.go b/internal/webpush/sender.go
index b7bb75d41..bdc00db1b 100644
--- a/internal/webpush/sender.go
+++ b/internal/webpush/sender.go
@@ -21,7 +21,7 @@ import (
"context"
"net/http"
- "code.superseriousbusiness.org/gotosocial/internal/filter/usermute"
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"code.superseriousbusiness.org/gotosocial/internal/httpclient"
"code.superseriousbusiness.org/gotosocial/internal/state"
@@ -31,14 +31,8 @@ import (
// Sender can send Web Push notifications.
type Sender interface {
- // Send queues up a notification for delivery to
- // all of an account's Web Push subscriptions.
- Send(
- ctx context.Context,
- notification *gtsmodel.Notification,
- filters []*gtsmodel.Filter,
- mutes *usermute.CompiledUserMuteList,
- ) error
+ // Send queues up a notification for delivery to all of an account's Web Push subscriptions.
+ Send(ctx context.Context, notif *gtsmodel.Notification, apiNotif *apimodel.Notification) error
}
// NewSender creates a new sender from an HTTP client, DB, and worker pool.
diff --git a/test/envparsing.sh b/test/envparsing.sh
index 3f8a55fda..807f5831a 100755
--- a/test/envparsing.sh
+++ b/test/envparsing.sh
@@ -61,6 +61,7 @@ EXPECT=$(cat << "EOF"
"cache-memory-target": "100MiB",
"cache-mention-mem-ratio": 2,
"cache-move-mem-ratio": 0.1,
+ "cache-mutes-mem-ratio": 2,
"cache-notification-mem-ratio": 2,
"cache-poll-mem-ratio": 1,
"cache-poll-vote-ids-mem-ratio": 2,
diff --git a/testrig/processor.go b/testrig/processor.go
index d2405a6f0..4acb7c648 100644
--- a/testrig/processor.go
+++ b/testrig/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/media"
"code.superseriousbusiness.org/gotosocial/internal/processing"
@@ -41,7 +42,6 @@ func NewTestProcessor(
webPushSender webpush.Sender,
mediaManager *media.Manager,
) *processing.Processor {
-
return processing.NewProcessor(
cleaner.New(state),
subscriptions.New(
@@ -57,6 +57,7 @@ func NewTestProcessor(
emailSender,
webPushSender,
visibility.NewFilter(state),
+ mutes.NewFilter(state),
interaction.NewFilter(state),
)
}
diff --git a/testrig/teststructs.go b/testrig/teststructs.go
index 3ca45e94e..f119bd113 100644
--- a/testrig/teststructs.go
+++ b/testrig/teststructs.go
@@ -22,6 +22,7 @@ import (
"code.superseriousbusiness.org/gotosocial/internal/cleaner"
"code.superseriousbusiness.org/gotosocial/internal/email"
"code.superseriousbusiness.org/gotosocial/internal/filter/interaction"
+ "code.superseriousbusiness.org/gotosocial/internal/filter/mutes"
"code.superseriousbusiness.org/gotosocial/internal/filter/visibility"
"code.superseriousbusiness.org/gotosocial/internal/processing"
"code.superseriousbusiness.org/gotosocial/internal/processing/common"
@@ -67,6 +68,7 @@ func SetupTestStructs(
state.Storage = storage
typeconverter := typeutils.NewConverter(&state)
visFilter := visibility.NewFilter(&state)
+ muteFilter := mutes.NewFilter(&state)
intFilter := interaction.NewFilter(&state)
httpClient := NewMockHTTPClient(nil, rMediaPath)
@@ -86,6 +88,7 @@ func SetupTestStructs(
typeconverter,
federator,
visFilter,
+ muteFilter,
)
processor := processing.NewProcessor(
@@ -99,6 +102,7 @@ func SetupTestStructs(
emailSender,
webPushSender,
visFilter,
+ muteFilter,
intFilter,
)
diff --git a/testrig/webpush.go b/testrig/webpush.go
index d4752ae90..b9ca9611d 100644
--- a/testrig/webpush.go
+++ b/testrig/webpush.go
@@ -20,7 +20,7 @@ package testrig
import (
"context"
- "code.superseriousbusiness.org/gotosocial/internal/filter/usermute"
+ apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"code.superseriousbusiness.org/gotosocial/internal/webpush"
)
@@ -39,11 +39,10 @@ func NewWebPushMockSender() *WebPushMockSender {
func (m *WebPushMockSender) Send(
ctx context.Context,
- notification *gtsmodel.Notification,
- filters []*gtsmodel.Filter,
- mutes *usermute.CompiledUserMuteList,
+ notif *gtsmodel.Notification,
+ apiNotif *apimodel.Notification,
) error {
- m.Sent[notification.TargetAccountID] = append(m.Sent[notification.TargetAccountID], notification)
+ m.Sent[notif.TargetAccountID] = append(m.Sent[notif.TargetAccountID], notif)
return nil
}
@@ -57,9 +56,8 @@ func NewNoopWebPushSender() webpush.Sender {
func (n *noopWebPushSender) Send(
ctx context.Context,
- notification *gtsmodel.Notification,
- filters []*gtsmodel.Filter,
- mutes *usermute.CompiledUserMuteList,
+ notif *gtsmodel.Notification,
+ apiNotif *apimodel.Notification,
) error {
return nil
}