[performance] cache mute check results (#4202)

This separates our the user mute handling from the typeconverter code, and creates a new "mutes" filter type (in a similar vein to the visibility filter) subpkg with its own result cache. This is a heavy mix of both chore given that mute calculation shouldn't have been handled in the conversion to frontend API types, and a performance bonus since we don't need to load and calculate so many things each time, just the single result each time with all necessary invalidation handled by database cache hooks.

Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4202
Co-authored-by: kim <grufwub@gmail.com>
Co-committed-by: kim <grufwub@gmail.com>
This commit is contained in:
kim
2025-05-31 17:30:57 +02:00
committed by kim
parent a82d574acc
commit faed35c938
65 changed files with 1645 additions and 766 deletions

View File

@ -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,
)

View File

@ -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.

View File

@ -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),
)

View File

@ -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()
}

View File

@ -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,
})
}

View File

@ -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) {

109
internal/cache/mutes.go vendored Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
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)
}

View File

@ -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(&gtsmodel.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,

View File

@ -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.

View File

@ -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"`
}

View File

@ -229,6 +229,7 @@ var Defaults = Configuration{
WebPushSubscriptionMemRatio: 1,
WebPushSubscriptionIDsMemRatio: 1,
VisibilityMemRatio: 2,
MutesMemRatio: 2,
},
HTTPClient: HTTPClientConfiguration{

View File

@ -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) {

View File

@ -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"},
} {

View File

@ -0,0 +1,85 @@
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package mutes
import (
"context"
"errors"
"time"
"code.superseriousbusiness.org/gotosocial/internal/db"
"code.superseriousbusiness.org/gotosocial/internal/gtscontext"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
)
// NOTE:
// we don't bother using the Mutes cache for any
// of the accounts functions below, as there's only
// a single cache load required of any UserMute.
// AccountMuted returns whether given target account is muted by requester.
func (f *Filter) AccountMuted(ctx context.Context, requester *gtsmodel.Account, account *gtsmodel.Account) (bool, error) {
mute, expired, err := f.getUserMute(ctx, requester, account)
if err != nil {
return false, err
} else if mute == nil {
return false, nil
}
return !expired, nil
}
// AccountNotificationsMuted returns whether notifications are muted for requester when incoming from given target account.
func (f *Filter) AccountNotificationsMuted(ctx context.Context, requester *gtsmodel.Account, account *gtsmodel.Account) (bool, error) {
mute, expired, err := f.getUserMute(ctx, requester, account)
if err != nil {
return false, err
} else if mute == nil {
return false, nil
}
return *mute.Notifications && !expired, nil
}
func (f *Filter) getUserMute(ctx context.Context, requester *gtsmodel.Account, account *gtsmodel.Account) (*gtsmodel.UserMute, bool, error) {
if requester == nil {
// Un-authed so no account
// is possible to be muted.
return nil, false, nil
}
// Look for mute against target.
mute, err := f.state.DB.GetMute(
gtscontext.SetBarebones(ctx),
requester.ID,
account.ID,
)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
return nil, false, gtserror.Newf("db error getting user mute: %w", err)
}
if mute == nil {
// No user mute exists!
return nil, false, nil
}
// Get current time.
now := time.Now()
// Return whether mute is expired.
return mute, mute.Expired(now), nil
}

View File

@ -0,0 +1,45 @@
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package mutes
import (
"time"
"code.superseriousbusiness.org/gotosocial/internal/state"
)
type muteDetails struct {
// mute flags.
mute bool
notif bool
// mute expiry times.
muteExpiry time.Time
notifExpiry time.Time
}
// noauth is a placeholder ID used in cache lookups
// when there is no authorized account ID to use.
const noauth = "noauth"
// Filter packages up a bunch of logic for checking whether
// given statuses or accounts are muted by a requester (user).
type Filter struct{ state *state.State }
// NewFilter returns a new Filter interface that will use the provided database.
func NewFilter(state *state.State) *Filter { return &Filter{state: state} }

View File

@ -0,0 +1,75 @@
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package mutes_test
import (
"code.superseriousbusiness.org/gotosocial/internal/db"
"code.superseriousbusiness.org/gotosocial/internal/filter/mutes"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"code.superseriousbusiness.org/gotosocial/internal/state"
"code.superseriousbusiness.org/gotosocial/testrig"
"github.com/stretchr/testify/suite"
)
type FilterStandardTestSuite struct {
// standard suite interfaces
suite.Suite
db db.DB
state state.State
// standard suite models
testTokens map[string]*gtsmodel.Token
testApplications map[string]*gtsmodel.Application
testUsers map[string]*gtsmodel.User
testAccounts map[string]*gtsmodel.Account
testAttachments map[string]*gtsmodel.MediaAttachment
testStatuses map[string]*gtsmodel.Status
testTags map[string]*gtsmodel.Tag
testMentions map[string]*gtsmodel.Mention
testFollows map[string]*gtsmodel.Follow
filter *mutes.Filter
}
func (suite *FilterStandardTestSuite) SetupSuite() {
suite.testTokens = testrig.NewTestTokens()
suite.testApplications = testrig.NewTestApplications()
suite.testUsers = testrig.NewTestUsers()
suite.testAccounts = testrig.NewTestAccounts()
suite.testAttachments = testrig.NewTestAttachments()
suite.testStatuses = testrig.NewTestStatuses()
suite.testTags = testrig.NewTestTags()
suite.testMentions = testrig.NewTestMentions()
suite.testFollows = testrig.NewTestFollows()
}
func (suite *FilterStandardTestSuite) SetupTest() {
suite.state.Caches.Init()
testrig.InitTestConfig()
testrig.InitTestLog()
suite.db = testrig.NewTestDB(&suite.state)
suite.filter = mutes.NewFilter(&suite.state)
testrig.StandardDBSetup(suite.db, nil)
}
func (suite *FilterStandardTestSuite) TearDownTest() {
testrig.StandardDBTeardown(suite.db)
}

View File

@ -0,0 +1,302 @@
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package mutes
import (
"context"
"errors"
"time"
"code.superseriousbusiness.org/gotosocial/internal/cache"
"code.superseriousbusiness.org/gotosocial/internal/db"
"code.superseriousbusiness.org/gotosocial/internal/gtscontext"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
)
// StatusMuted returns whether given target status is muted for requester in the context of timeline visibility.
func (f *Filter) StatusMuted(ctx context.Context, requester *gtsmodel.Account, status *gtsmodel.Status) (muted bool, err error) {
details, err := f.StatusMuteDetails(ctx, requester, status)
if err != nil {
return false, gtserror.Newf("error getting status mute details: %w", err)
}
return details.Mute && !details.MuteExpired(time.Now()), nil
}
// StatusNotificationsMuted returns whether notifications are muted for requester when regarding given target status.
func (f *Filter) StatusNotificationsMuted(ctx context.Context, requester *gtsmodel.Account, status *gtsmodel.Status) (muted bool, err error) {
details, err := f.StatusMuteDetails(ctx, requester, status)
if err != nil {
return false, gtserror.Newf("error getting status mute details: %w", err)
}
return details.Notifications && !details.NotificationExpired(time.Now()), nil
}
// StatusMuteDetails returns mute details about the given status for the given requesting account.
func (f *Filter) StatusMuteDetails(ctx context.Context, requester *gtsmodel.Account, status *gtsmodel.Status) (*cache.CachedMute, error) {
// For requester ID use a
// fallback 'noauth' string
// by default for lookups.
requesterID := noauth
if requester != nil {
requesterID = requester.ID
}
// Load mute details for this requesting account about status from cache, using load callback if needed.
details, err := f.state.Caches.Mutes.LoadOne("RequesterID,StatusID", func() (*cache.CachedMute, error) {
// Load the mute details for given status.
details, err := f.getStatusMuteDetails(ctx,
requester,
status,
)
if err != nil {
if err == cache.SentinelError {
// Filter-out our temporary
// race-condition error.
return &cache.CachedMute{}, nil
}
return nil, err
}
// Convert to cache details.
return &cache.CachedMute{
StatusID: status.ID,
ThreadID: status.ThreadID,
RequesterID: requester.ID,
Mute: details.mute,
MuteExpiry: details.muteExpiry,
Notifications: details.notif,
NotificationExpiry: details.notifExpiry,
}, nil
}, requesterID, status.ID)
if err != nil {
return nil, err
}
return details, err
}
// getStatusMuteDetails loads muteDetails{} for the given
// status and the thread it is a part of, including any
// relevant muted parent status authors / mentions.
func (f *Filter) getStatusMuteDetails(
ctx context.Context,
requester *gtsmodel.Account,
status *gtsmodel.Status,
) (
muteDetails,
error,
) {
var details muteDetails
if requester == nil {
// Without auth, there will be no possible
// mute to exist. Always return as 'unmuted'.
return details, nil
}
// Look for a stored mute from account against thread.
threadMute, err := f.state.DB.GetThreadMutedByAccount(ctx,
status.ThreadID,
requester.ID,
)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
return details, gtserror.Newf("db error checking thread mute: %w", err)
}
// Set notif mute on thread mute.
details.notif = (threadMute != nil)
for next := status; ; {
// Load the mute details for 'next' status
// in current thread, into our details obj.
if err = f.loadOneStatusMuteDetails(ctx,
requester,
next,
&details,
); err != nil {
return details, err
}
if next.InReplyToURI == "" {
// Reached the top
// of the thread.
break
}
if next.InReplyToID == "" {
// Parent is not yet dereferenced.
return details, cache.SentinelError
}
// Check if parent is set.
inReplyTo := next.InReplyTo
if inReplyTo == nil {
// Fetch next parent in conversation.
inReplyTo, err = f.state.DB.GetStatusByID(
gtscontext.SetBarebones(ctx),
next.InReplyToID,
)
if err != nil {
return details, gtserror.Newf("error getting status parent %s: %w", next.InReplyToURI, err)
}
}
// Set next status.
next = inReplyTo
}
return details, nil
}
// loadOneStatusMuteDetails loads the mute details for
// any relevant accounts to given status to the requesting
// account into the passed muteDetails object pointer.
func (f *Filter) loadOneStatusMuteDetails(
ctx context.Context,
requester *gtsmodel.Account,
status *gtsmodel.Status,
details *muteDetails,
) error {
// Look for mutes against related status accounts
// by requester (e.g. author, mention targets etc).
userMutes, err := f.getStatusRelatedUserMutes(ctx,
requester,
status,
)
if err != nil {
return err
}
for _, mute := range userMutes {
// Set as muted!
details.mute = true
// Set notifications as
// muted if flag is set.
if *mute.Notifications {
details.notif = true
}
// Check for expiry data given.
if !mute.ExpiresAt.IsZero() {
// Update mute details expiry time if later.
if mute.ExpiresAt.After(details.muteExpiry) {
details.muteExpiry = mute.ExpiresAt
}
// Update notif details expiry time if later.
if mute.ExpiresAt.After(details.notifExpiry) {
details.notifExpiry = mute.ExpiresAt
}
}
}
return nil
}
// getStatusRelatedUserMutes fetches user mutes for any
// of the possible related accounts regarding this status,
// i.e. the author and any account mentioned.
func (f *Filter) getStatusRelatedUserMutes(
ctx context.Context,
requester *gtsmodel.Account,
status *gtsmodel.Status,
) (
[]*gtsmodel.UserMute,
error,
) {
if status.AccountID == requester.ID {
// Status is by requester, we don't take
// into account related attached user mutes.
return nil, nil
}
if !status.MentionsPopulated() {
var err error
// Populate status mention objects before further mention checks.
status.Mentions, err = f.state.DB.GetMentions(ctx, status.MentionIDs)
if err != nil {
return nil, gtserror.Newf("error populating status %s mentions: %w", status.URI, err)
}
}
// Preallocate a slice of worst possible case no. user mutes.
mutes := make([]*gtsmodel.UserMute, 0, 2+len(status.Mentions))
// Look for mute against author.
mute, err := f.state.DB.GetMute(
gtscontext.SetBarebones(ctx),
requester.ID,
status.AccountID,
)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
return nil, gtserror.Newf("db error getting status author mute: %w", err)
}
if mute != nil {
// Append author mute to total.
mutes = append(mutes, mute)
}
if status.BoostOfAccountID != "" {
// Look for mute against boost author.
mute, err := f.state.DB.GetMute(
gtscontext.SetBarebones(ctx),
requester.ID,
status.AccountID,
)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
return nil, gtserror.Newf("db error getting boost author mute: %w", err)
}
if mute != nil {
// Append author mute to total.
mutes = append(mutes, mute)
}
}
for _, mention := range status.Mentions {
// Look for mute against any target mentions.
if mention.TargetAccountID != requester.ID {
// Look for mute against target.
mute, err := f.state.DB.GetMute(
gtscontext.SetBarebones(ctx),
requester.ID,
mention.TargetAccountID,
)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
return nil, gtserror.Newf("db error getting mention target mute: %w", err)
}
if mute != nil {
// Append target mute to total.
mutes = append(mutes, mute)
}
}
}
return mutes, nil
}

View File

@ -0,0 +1,283 @@
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package mutes_test
import (
"testing"
"code.superseriousbusiness.org/gotosocial/internal/ap"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"code.superseriousbusiness.org/gotosocial/internal/id"
"code.superseriousbusiness.org/gotosocial/internal/util"
"github.com/stretchr/testify/suite"
)
type StatusMuteTestSuite struct {
FilterStandardTestSuite
}
func (suite *StatusMuteTestSuite) TestMutedStatusAuthor() {
ctx := suite.T().Context()
status := suite.testStatuses["admin_account_status_1"]
requester := suite.testAccounts["local_account_1"]
replyer := suite.testAccounts["local_account_2"]
// Generate a new reply
// to the above status.
replyID := id.NewULID()
reply := &gtsmodel.Status{
ID: replyID,
URI: replyer.URI + "/statuses/" + replyID,
ThreadID: status.ThreadID,
AccountID: replyer.ID,
AccountURI: replyer.URI,
InReplyToID: status.ID,
InReplyToURI: status.URI,
InReplyToAccountID: status.AccountID,
Local: util.Ptr(false),
Federated: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote,
}
// And insert reply into the database.
err := suite.db.PutStatus(ctx, reply)
suite.NoError(err)
// Ensure that neither status nor reply are muted to requester.
muted1, err1 := suite.filter.StatusMuted(ctx, requester, status)
muted2, err2 := suite.filter.StatusMuted(ctx, requester, reply)
suite.NoError(err1)
suite.NoError(err2)
suite.False(muted1)
suite.False(muted2)
// Ensure notifications for neither status nor reply are muted to requester.
muted1, err = suite.filter.StatusNotificationsMuted(ctx, requester, status)
muted2, err = suite.filter.StatusNotificationsMuted(ctx, requester, reply)
suite.NoError(err1)
suite.NoError(err2)
suite.False(muted1)
suite.False(muted2)
// Insert new user mute targetting first status author.
err = suite.state.DB.PutMute(ctx, &gtsmodel.UserMute{
ID: id.NewULID(),
AccountID: requester.ID,
TargetAccountID: status.AccountID,
Notifications: util.Ptr(false),
})
suite.NoError(err)
// Now ensure that both status and reply are muted to requester.
muted1, err1 = suite.filter.StatusMuted(ctx, requester, status)
muted2, err2 = suite.filter.StatusMuted(ctx, requester, reply)
suite.NoError(err1)
suite.NoError(err2)
suite.True(muted1)
suite.True(muted2)
// Though neither status nor reply should have notifications muted to requester.
muted1, err = suite.filter.StatusNotificationsMuted(ctx, requester, status)
muted2, err = suite.filter.StatusNotificationsMuted(ctx, requester, reply)
suite.NoError(err1)
suite.NoError(err2)
suite.False(muted1)
suite.False(muted2)
// Now delete account mutes to / from requesting account.
err = suite.state.DB.DeleteAccountMutes(ctx, requester.ID)
suite.NoError(err)
// Now ensure that both status and reply are unmuted again.
muted1, err1 = suite.filter.StatusMuted(ctx, requester, status)
muted2, err2 = suite.filter.StatusMuted(ctx, requester, reply)
suite.NoError(err1)
suite.NoError(err2)
suite.False(muted1)
suite.False(muted2)
}
func (suite *StatusMuteTestSuite) TestMutedStatusMentionee() {
ctx := suite.T().Context()
status := suite.testStatuses["admin_account_status_5"]
requester := suite.testAccounts["local_account_1"]
mentionee := suite.testAccounts["local_account_2"]
replyer := suite.testAccounts["local_account_3"]
// Generate a new reply
// to the above status.
replyID := id.NewULID()
reply := &gtsmodel.Status{
ID: replyID,
URI: replyer.URI + "/statuses/" + replyID,
ThreadID: status.ThreadID,
AccountID: replyer.ID,
AccountURI: replyer.URI,
InReplyToID: status.ID,
InReplyToURI: status.URI,
InReplyToAccountID: status.AccountID,
Local: util.Ptr(false),
Federated: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote,
}
// And insert reply into the database.
err := suite.db.PutStatus(ctx, reply)
suite.NoError(err)
// Ensure that neither status nor reply are muted to requester.
muted1, err1 := suite.filter.StatusMuted(ctx, requester, status)
muted2, err2 := suite.filter.StatusMuted(ctx, requester, reply)
suite.NoError(err1)
suite.NoError(err2)
suite.False(muted1)
suite.False(muted2)
// Ensure notifications for neither status nor reply are muted to requester.
muted1, err = suite.filter.StatusNotificationsMuted(ctx, requester, status)
muted2, err = suite.filter.StatusNotificationsMuted(ctx, requester, reply)
suite.NoError(err1)
suite.NoError(err2)
suite.False(muted1)
suite.False(muted2)
// Insert user visibility mute targetting status author.
err = suite.state.DB.PutMute(ctx, &gtsmodel.UserMute{
ID: id.NewULID(),
AccountID: requester.ID,
TargetAccountID: mentionee.ID,
Notifications: util.Ptr(false),
})
suite.NoError(err)
// Now ensure that both status and reply are muted to requester.
muted1, err1 = suite.filter.StatusMuted(ctx, requester, status)
muted2, err2 = suite.filter.StatusMuted(ctx, requester, reply)
suite.NoError(err1)
suite.NoError(err2)
suite.True(muted1)
suite.True(muted2)
// Though neither status nor reply should have notifications muted to requester.
muted1, err = suite.filter.StatusNotificationsMuted(ctx, requester, status)
muted2, err = suite.filter.StatusNotificationsMuted(ctx, requester, reply)
suite.NoError(err1)
suite.NoError(err2)
suite.False(muted1)
suite.False(muted2)
// Now delete account mutes to / from requesting account.
err = suite.state.DB.DeleteAccountMutes(ctx, requester.ID)
suite.NoError(err)
// Now ensure that both status and reply are unmuted again.
muted1, err1 = suite.filter.StatusMuted(ctx, requester, status)
muted2, err2 = suite.filter.StatusMuted(ctx, requester, reply)
suite.NoError(err1)
suite.NoError(err2)
suite.False(muted1)
suite.False(muted2)
}
func (suite *StatusMuteTestSuite) TestMutedStatusThread() {
ctx := suite.T().Context()
status := suite.testStatuses["admin_account_status_1"]
requester := suite.testAccounts["local_account_1"]
replyer := suite.testAccounts["local_account_2"]
// Generate a new reply
// to the above status.
replyID := id.NewULID()
reply := &gtsmodel.Status{
ID: replyID,
URI: replyer.URI + "/statuses/" + replyID,
ThreadID: status.ThreadID,
AccountID: replyer.ID,
AccountURI: replyer.URI,
InReplyToID: status.ID,
InReplyToURI: status.URI,
InReplyToAccountID: status.AccountID,
Local: util.Ptr(false),
Federated: util.Ptr(true),
ActivityStreamsType: ap.ObjectNote,
}
// And insert reply into the database.
err := suite.db.PutStatus(ctx, reply)
suite.NoError(err)
// Ensure that neither status nor reply are muted to requester.
muted1, err1 := suite.filter.StatusMuted(ctx, requester, status)
muted2, err2 := suite.filter.StatusMuted(ctx, requester, reply)
suite.NoError(err1)
suite.NoError(err2)
suite.False(muted1)
suite.False(muted2)
// Ensure notifications for neither status nor reply are muted to requester.
muted1, err = suite.filter.StatusNotificationsMuted(ctx, requester, status)
muted2, err = suite.filter.StatusNotificationsMuted(ctx, requester, reply)
suite.NoError(err1)
suite.NoError(err2)
suite.False(muted1)
suite.False(muted2)
threadMuteID := id.NewULID()
// Insert new notification mute targetting status thread.
err = suite.db.PutThreadMute(ctx, &gtsmodel.ThreadMute{
ID: threadMuteID,
AccountID: requester.ID,
ThreadID: status.ThreadID,
})
// Ensure status and reply are still not muted to requester.
muted1, err1 = suite.filter.StatusMuted(ctx, requester, status)
muted2, err2 = suite.filter.StatusMuted(ctx, requester, reply)
suite.NoError(err1)
suite.NoError(err2)
suite.False(muted1)
suite.False(muted2)
// Though now ensure notifications for both ARE muted to requester.
muted1, err = suite.filter.StatusNotificationsMuted(ctx, requester, status)
muted2, err = suite.filter.StatusNotificationsMuted(ctx, requester, reply)
suite.NoError(err1)
suite.NoError(err2)
suite.True(muted1)
suite.True(muted2)
// Now delete the mute from requester targetting thread.
err = suite.state.DB.DeleteThreadMute(ctx, threadMuteID)
suite.NoError(err)
// Andf ensure notifications for both are unmuted to the requester again.
muted1, err = suite.filter.StatusNotificationsMuted(ctx, requester, status)
muted2, err = suite.filter.StatusNotificationsMuted(ctx, requester, reply)
suite.NoError(err1)
suite.NoError(err2)
suite.False(muted1)
suite.False(muted2)
}
func TestStatusMuteTestSuite(t *testing.T) {
suite.Run(t, new(StatusMuteTestSuite))
}

View File

@ -1,80 +0,0 @@
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package usermute
import (
"time"
statusfilter "code.superseriousbusiness.org/gotosocial/internal/filter/status"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
)
type compiledUserMuteListEntry struct {
ExpiresAt time.Time
Notifications bool
}
func (e *compiledUserMuteListEntry) appliesInContext(filterContext statusfilter.FilterContext) bool {
switch filterContext {
case statusfilter.FilterContextHome:
return true
case statusfilter.FilterContextNotifications:
return e.Notifications
case statusfilter.FilterContextPublic:
return true
case statusfilter.FilterContextThread:
return true
case statusfilter.FilterContextAccount:
return false
}
return false
}
func (e *compiledUserMuteListEntry) expired(now time.Time) bool {
return !e.ExpiresAt.IsZero() && !e.ExpiresAt.After(now)
}
type CompiledUserMuteList struct {
byTargetAccountID map[string]compiledUserMuteListEntry
}
func NewCompiledUserMuteList(mutes []*gtsmodel.UserMute) (c *CompiledUserMuteList) {
c = &CompiledUserMuteList{byTargetAccountID: make(map[string]compiledUserMuteListEntry, len(mutes))}
for _, mute := range mutes {
c.byTargetAccountID[mute.TargetAccountID] = compiledUserMuteListEntry{
ExpiresAt: mute.ExpiresAt,
Notifications: *mute.Notifications,
}
}
return
}
func (c *CompiledUserMuteList) Len() int {
if c == nil {
return 0
}
return len(c.byTargetAccountID)
}
func (c *CompiledUserMuteList) Matches(accountID string, filterContext statusfilter.FilterContext, now time.Time) bool {
if c == nil {
return false
}
e, found := c.byTargetAccountID[accountID]
return found && e.appliesInContext(filterContext) && !e.expired(now)
}

View File

@ -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 {

View File

@ -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
}

View File

@ -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")
}

View File

@ -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

View File

@ -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

View File

@ -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),
)

View File

@ -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,
}
}

View File

@ -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)

View File

@ -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
}

View File

@ -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")

View File

@ -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,

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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 {

View File

@ -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,

View File

@ -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())

View File

@ -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

View File

@ -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,
)

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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,

View File

@ -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,

View File

@ -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

View File

@ -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,

View File

@ -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,

View File

@ -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

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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())
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -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)
}

View File

@ -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())
}

View File

@ -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",

View File

@ -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,

View File

@ -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;

View File

@ -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 = `<p>First paragraph of content warning</p><h4>Here's the title!</h4><p></p><p>Big boobs<br>Tee hee!<br><br>Some more text<br>And a bunch more<br><br>Hasta la victoria siempre!</p>`
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 := &gtsmodel.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 := &gtsmodel.Conversation{
@ -4220,7 +4122,6 @@ func (suite *InternalToFrontendTestSuite) TestConversationToAPI() {
convo,
requester,
filters,
mutes,
)
if err != nil {
suite.FailNow(err.Error())

View File

@ -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,

View File

@ -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

View File

@ -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.

View File

@ -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,

View File

@ -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),
)
}

View File

@ -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,
)

View File

@ -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
}