[feature] Status thread mute/unmute functionality (#2278)

* add db models + functions for keeping track of threads

* give em the old linty testy

* create, remove, check mutes

* swagger

* testerino

* test mute/unmute via api

* add info log about new index creation

* thread + allow muting of any remote statuses that mention a local account

* IsStatusThreadMutedBy -> IsThreadMutedByAccount

* use common processing functions in status processor

* set = NULL

* favee!

* get rekt darlings, darlings get rekt

* testrig please, have mercy muy liege
This commit is contained in:
tobi 2023-10-25 16:04:53 +02:00 committed by GitHub
commit c7b6cd7770
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 1750 additions and 198 deletions

View file

@ -260,6 +260,11 @@ func (p *clientAPI) CreateLike(ctx context.Context, cMsg messages.FromClientAPI)
return gtserror.Newf("%T not parseable as *gtsmodel.StatusFave", cMsg.GTSModel)
}
// Ensure fave populated.
if err := p.state.DB.PopulateStatusFave(ctx, fave); err != nil {
return gtserror.Newf("error populating status fave: %w", err)
}
if err := p.surface.notifyFave(ctx, fave); err != nil {
return gtserror.Newf("error notifying fave: %w", err)
}

View file

@ -75,6 +75,7 @@ func (suite *FromClientAPITestSuite) newStatus(
newStatus.InReplyToAccountID = replyToStatus.AccountID
newStatus.InReplyToID = replyToStatus.ID
newStatus.InReplyToURI = replyToStatus.URI
newStatus.ThreadID = replyToStatus.ThreadID
// Mention the replied-to account.
mention := &gtsmodel.Mention{
@ -324,6 +325,114 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusReply() {
)
}
func (suite *FromClientAPITestSuite) TestProcessCreateStatusReplyMuted() {
var (
ctx = context.Background()
postingAccount = suite.testAccounts["admin_account"]
receivingAccount = suite.testAccounts["local_account_1"]
// Admin account posts a reply to zork.
// Normally zork would get a notification
// for this, but zork mutes this thread.
status = suite.newStatus(
ctx,
postingAccount,
gtsmodel.VisibilityPublic,
suite.testStatuses["local_account_1_status_1"],
nil,
)
threadMute = &gtsmodel.ThreadMute{
ID: "01HD3KRMBB1M85QRWHD912QWRE",
ThreadID: suite.testStatuses["local_account_1_status_1"].ThreadID,
AccountID: receivingAccount.ID,
}
)
// Store the thread mute before processing new status.
if err := suite.db.PutThreadMute(ctx, threadMute); err != nil {
suite.FailNow(err.Error())
}
// Process the new status.
if err := suite.processor.Workers().ProcessFromClientAPI(
ctx,
messages.FromClientAPI{
APObjectType: ap.ObjectNote,
APActivityType: ap.ActivityCreate,
GTSModel: status,
OriginAccount: postingAccount,
},
); err != nil {
suite.FailNow(err.Error())
}
// Ensure no notification received.
notif, err := suite.db.GetNotification(
ctx,
gtsmodel.NotificationMention,
receivingAccount.ID,
postingAccount.ID,
status.ID,
)
suite.ErrorIs(err, db.ErrNoEntries)
suite.Nil(notif)
}
func (suite *FromClientAPITestSuite) TestProcessCreateStatusBoostMuted() {
var (
ctx = context.Background()
postingAccount = suite.testAccounts["admin_account"]
receivingAccount = suite.testAccounts["local_account_1"]
// Admin account boosts a status by zork.
// Normally zork would get a notification
// for this, but zork mutes this thread.
status = suite.newStatus(
ctx,
postingAccount,
gtsmodel.VisibilityPublic,
nil,
suite.testStatuses["local_account_1_status_1"],
)
threadMute = &gtsmodel.ThreadMute{
ID: "01HD3KRMBB1M85QRWHD912QWRE",
ThreadID: suite.testStatuses["local_account_1_status_1"].ThreadID,
AccountID: receivingAccount.ID,
}
)
// Store the thread mute before processing new status.
if err := suite.db.PutThreadMute(ctx, threadMute); err != nil {
suite.FailNow(err.Error())
}
// Process the new status.
if err := suite.processor.Workers().ProcessFromClientAPI(
ctx,
messages.FromClientAPI{
APObjectType: ap.ActivityAnnounce,
APActivityType: ap.ActivityCreate,
GTSModel: status,
OriginAccount: postingAccount,
},
); err != nil {
suite.FailNow(err.Error())
}
// Ensure no notification received.
notif, err := suite.db.GetNotification(
ctx,
gtsmodel.NotificationReblog,
receivingAccount.ID,
postingAccount.ID,
status.ID,
)
suite.ErrorIs(err, db.ErrNoEntries)
suite.Nil(notif)
}
func (suite *FromClientAPITestSuite) TestProcessCreateStatusListRepliesPolicyListOnlyOK() {
// We're modifying the test list so take a copy.
testList := new(gtsmodel.List)

View file

@ -315,6 +315,11 @@ func (p *fediAPI) CreateLike(ctx context.Context, fMsg messages.FromFediAPI) err
return gtserror.Newf("%T not parseable as *gtsmodel.StatusFave", fMsg.GTSModel)
}
// Ensure fave populated.
if err := p.state.DB.PopulateStatusFave(ctx, fave); err != nil {
return gtserror.Newf("error populating status fave: %w", err)
}
if err := p.surface.notifyFave(ctx, fave); err != nil {
return gtserror.Newf("error notifying fave: %w", err)
}

View file

@ -28,15 +28,39 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/id"
)
// notifyMentions notifies each targeted account in
// the given mentions that they have a new mention.
// notifyMentions iterates through mentions on the
// given status, and notifies each mentioned account
// that they have a new mention.
func (s *surface) notifyMentions(
ctx context.Context,
mentions []*gtsmodel.Mention,
status *gtsmodel.Status,
) error {
errs := gtserror.NewMultiError(len(mentions))
var (
mentions = status.Mentions
errs = gtserror.NewMultiError(len(mentions))
)
for _, mention := range mentions {
// Ensure thread not muted
// by mentioned account.
muted, err := s.state.DB.IsThreadMutedByAccount(
ctx,
status.ThreadID,
mention.TargetAccountID,
)
if err != nil {
errs.Append(err)
continue
}
if muted {
// This mentioned account
// has muted the thread.
// Don't pester them.
continue
}
if err := s.notify(
ctx,
gtsmodel.NotificationMention,
@ -114,6 +138,24 @@ func (s *surface) notifyFave(
return nil
}
// Ensure favee hasn't
// muted the thread.
muted, err := s.state.DB.IsThreadMutedByAccount(
ctx,
fave.Status.ThreadID,
fave.TargetAccountID,
)
if err != nil {
return err
}
if muted {
// Boostee doesn't want
// notifs for this thread.
return nil
}
return s.notify(
ctx,
gtsmodel.NotificationFave,
@ -134,11 +176,35 @@ func (s *surface) notifyAnnounce(
return nil
}
if status.BoostOf == nil {
// No boosted status
// set, nothing to do.
return nil
}
if status.BoostOfAccountID == status.AccountID {
// Self-boost, nothing to do.
return nil
}
// Ensure boostee hasn't
// muted the thread.
muted, err := s.state.DB.IsThreadMutedByAccount(
ctx,
status.BoostOf.ThreadID,
status.BoostOfAccountID,
)
if err != nil {
return err
}
if muted {
// Boostee doesn't want
// notifs for this thread.
return nil
}
return s.notify(
ctx,
gtsmodel.NotificationReblog,

View file

@ -67,7 +67,7 @@ func (s *surface) timelineAndNotifyStatus(ctx context.Context, status *gtsmodel.
}
// Notify each local account that's mentioned by this status.
if err := s.notifyMentions(ctx, status.Mentions); err != nil {
if err := s.notifyMentions(ctx, status); err != nil {
return gtserror.Newf("error notifying status mentions for status %s: %w", status.ID, err)
}