diff options
| author | 2025-05-11 13:38:13 +0000 | |
|---|---|---|
| committer | 2025-05-11 13:38:13 +0000 | |
| commit | 8480a758081e84384a366a29ecee3c3103687512 (patch) | |
| tree | d35f56a4b88905fb5f9c3a9f788ac2cf628c92fe /internal/processing/workers | |
| parent | [bugfix] Fix a11y property warning from authorization page (#4166) (diff) | |
| download | gotosocial-8480a758081e84384a366a29ecee3c3103687512.tar.xz | |
[feature] Notify accounts when a status they've interacted with has been edited (#4157)
This pull request adds sending notifications to local accounts that have interacted with a status, if we receive or create a new edit for that status.
closes https://codeberg.org/superseriousbusiness/gotosocial/issues/3991
Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4157
Co-authored-by: tobi <tobi.smethurst@protonmail.com>
Co-committed-by: tobi <tobi.smethurst@protonmail.com>
Diffstat (limited to 'internal/processing/workers')
| -rw-r--r-- | internal/processing/workers/fromclientapi.go | 8 | ||||
| -rw-r--r-- | internal/processing/workers/fromclientapi_test.go | 90 | ||||
| -rw-r--r-- | internal/processing/workers/fromfediapi.go | 8 | ||||
| -rw-r--r-- | internal/processing/workers/fromfediapi_test.go | 8 | ||||
| -rw-r--r-- | internal/processing/workers/surfacenotify.go | 69 |
5 files changed, 169 insertions, 14 deletions
diff --git a/internal/processing/workers/fromclientapi.go b/internal/processing/workers/fromclientapi.go index dbbeff220..5d9ebf41a 100644 --- a/internal/processing/workers/fromclientapi.go +++ b/internal/processing/workers/fromclientapi.go @@ -748,6 +748,14 @@ 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 { + log.Errorf(ctx, "error notifying status edit: %v", err) + } + } + // Push message that the status has been edited to streams. if err := p.surface.timelineStatusUpdate(ctx, status); err != nil { log.Errorf(ctx, "error streaming status edit: %v", err) diff --git a/internal/processing/workers/fromclientapi_test.go b/internal/processing/workers/fromclientapi_test.go index c643e0c70..a1027f3e0 100644 --- a/internal/processing/workers/fromclientapi_test.go +++ b/internal/processing/workers/fromclientapi_test.go @@ -2149,6 +2149,96 @@ func (suite *FromClientAPITestSuite) TestProcessUpdateStatusWithFollowedHashtag( suite.checkNotWebPushed(testStructs.WebPushSender, receivingAccount.ID) } +// Test that when someone edits a status that's been interacted with, +// the interacter gets a notification that the status has been edited. +func (suite *FromClientAPITestSuite) TestProcessUpdateStatusInteractedWith() { + testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath) + defer testrig.TearDownTestStructs(testStructs) + + var ( + ctx = context.Background() + postingAccount = suite.testAccounts["local_account_1"] + receivingAccount = suite.testAccounts["admin_account"] + streams = suite.openStreams(ctx, + testStructs.Processor, + receivingAccount, + nil, + ) + notifStream = streams[stream.TimelineNotifications] + ) + + // Copy the test status. + // + // This is one that the receiving account + // has interacted with (by replying). + testStatus := new(gtsmodel.Status) + *testStatus = *suite.testStatuses["local_account_1_status_1"] + + // Create + store an edit. + edit := >smodel.StatusEdit{ + // Just set the ID + status ID, other + // fields don't matter for this test. + ID: "01JTR74W15VS6A6MK15N5JVJ55", + StatusID: testStatus.ID, + } + + if err := testStructs.State.DB.PutStatusEdit(ctx, edit); err != nil { + suite.FailNow(err.Error()) + } + + // Set edit on status as + // it would be for real. + testStatus.EditIDs = []string{edit.ID} + testStatus.Edits = []*gtsmodel.StatusEdit{edit} + + // Update the status. + if err := testStructs.Processor.Workers().ProcessFromClientAPI( + ctx, + &messages.FromClientAPI{ + APObjectType: ap.ObjectNote, + APActivityType: ap.ActivityUpdate, + GTSModel: testStatus, + Origin: postingAccount, + }, + ); err != nil { + suite.FailNow(err.Error()) + } + + // Wait for a notification to appear for the status. + var notif *gtsmodel.Notification + if !testrig.WaitFor(func() bool { + var err error + notif, err = testStructs.State.DB.GetNotification( + ctx, + gtsmodel.NotificationUpdate, + receivingAccount.ID, + postingAccount.ID, + edit.ID, + ) + return err == nil + }) { + suite.FailNow("timed out waiting for edited status notification") + } + + apiNotif, err := testStructs.TypeConverter.NotificationToAPINotification(ctx, notif, nil, nil) + if err != nil { + suite.FailNow(err.Error()) + } + + notifJSON, err := json.Marshal(apiNotif) + if err != nil { + suite.FailNow(err.Error()) + } + + // Check notif in stream. + suite.checkStreamed( + notifStream, + true, + string(notifJSON), + stream.EventTypeNotification, + ) +} + func (suite *FromClientAPITestSuite) TestProcessStatusDelete() { testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath) defer testrig.TearDownTestStructs(testStructs) diff --git a/internal/processing/workers/fromfediapi.go b/internal/processing/workers/fromfediapi.go index 86d868530..d1e5bb2f7 100644 --- a/internal/processing/workers/fromfediapi.go +++ b/internal/processing/workers/fromfediapi.go @@ -1010,6 +1010,14 @@ 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 { + log.Errorf(ctx, "error notifying status edit: %v", err) + } + } + // Push message that the status has been edited to streams. if err := p.surface.timelineStatusUpdate(ctx, status); err != nil { log.Errorf(ctx, "error streaming status edit: %v", err) diff --git a/internal/processing/workers/fromfediapi_test.go b/internal/processing/workers/fromfediapi_test.go index 203863e12..d197f4122 100644 --- a/internal/processing/workers/fromfediapi_test.go +++ b/internal/processing/workers/fromfediapi_test.go @@ -102,7 +102,7 @@ func (suite *FromFediAPITestSuite) TestProcessFederationAnnounce() { suite.Equal(gtsmodel.NotificationReblog, notif.NotificationType) suite.Equal(boostedStatus.AccountID, notif.TargetAccountID) suite.Equal(announceStatus.AccountID, notif.OriginAccountID) - suite.Equal(announceStatus.ID, notif.StatusID) + suite.Equal(announceStatus.ID, notif.StatusOrEditID) suite.False(*notif.Read) } @@ -173,7 +173,7 @@ func (suite *FromFediAPITestSuite) TestProcessReplyMention() { suite.Equal(gtsmodel.NotificationMention, notif.NotificationType) suite.Equal(replyingStatus.InReplyToAccountID, notif.TargetAccountID) suite.Equal(replyingStatus.AccountID, notif.OriginAccountID) - suite.Equal(replyingStatus.ID, notif.StatusID) + suite.Equal(replyingStatus.ID, notif.StatusOrEditID) suite.False(*notif.Read) ctx, _ := context.WithTimeout(context.Background(), time.Second*5) @@ -245,7 +245,7 @@ func (suite *FromFediAPITestSuite) TestProcessFave() { suite.Equal(gtsmodel.NotificationFavourite, notif.NotificationType) suite.Equal(fave.TargetAccountID, notif.TargetAccountID) suite.Equal(fave.AccountID, notif.OriginAccountID) - suite.Equal(fave.StatusID, notif.StatusID) + suite.Equal(fave.StatusID, notif.StatusOrEditID) suite.False(*notif.Read) ctx, _ := context.WithTimeout(context.Background(), time.Second*5) @@ -318,7 +318,7 @@ func (suite *FromFediAPITestSuite) TestProcessFaveWithDifferentReceivingAccount( suite.Equal(gtsmodel.NotificationFavourite, notif.NotificationType) suite.Equal(fave.TargetAccountID, notif.TargetAccountID) suite.Equal(fave.AccountID, notif.OriginAccountID) - suite.Equal(fave.StatusID, notif.StatusID) + suite.Equal(fave.StatusID, notif.StatusOrEditID) suite.False(*notif.Read) // 2. no notification should be streamed to the account that received the fave message, because they weren't the target diff --git a/internal/processing/workers/surfacenotify.go b/internal/processing/workers/surfacenotify.go index 8e034c23a..11c3fd059 100644 --- a/internal/processing/workers/surfacenotify.go +++ b/internal/processing/workers/surfacenotify.go @@ -30,6 +30,7 @@ import ( "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" "code.superseriousbusiness.org/gotosocial/internal/id" "code.superseriousbusiness.org/gotosocial/internal/util" + "code.superseriousbusiness.org/gotosocial/internal/util/xslices" ) // notifyPendingReply notifies the account replied-to @@ -555,19 +556,67 @@ func (s *Surface) notifySignup(ctx context.Context, newUser *gtsmodel.User) erro return errs.Combine() } +func (s *Surface) notifyStatusEdit( + ctx context.Context, + status *gtsmodel.Status, + editID string, +) error { + // Get local-only interactions (we can't/don't notify remotes). + interactions, err := s.State.DB.GetStatusInteractions(ctx, status.ID, true) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + return gtserror.Newf("db error getting status interactions: %w", err) + } + + // Deduplicate interactions by account ID, + // we don't need to notify someone twice + // if they've both boosted *and* replied + // to an edited status, for example. + interactions = xslices.DeduplicateFunc( + interactions, + func(v gtsmodel.Interaction) string { + return v.GetAccount().ID + }, + ) + + // Notify each account that's + // interacted with the status. + var errs gtserror.MultiError + for _, i := range interactions { + targetAcct := i.GetAccount() + if targetAcct.ID == status.AccountID { + // Don't notify an account + // if they've interacted + // with their *own* status. + continue + } + + if err := s.Notify(ctx, + gtsmodel.NotificationUpdate, + targetAcct, + status.Account, + editID, + ); err != nil { + errs.Appendf("error notifying status edit: %w", err) + continue + } + } + + return errs.Combine() +} + func getNotifyLockURI( notificationType gtsmodel.NotificationType, targetAccount *gtsmodel.Account, originAccount *gtsmodel.Account, - statusID string, + statusOrEditID string, ) string { builder := strings.Builder{} builder.WriteString("notification:?") builder.WriteString("type=" + notificationType.String()) - builder.WriteString("&target=" + targetAccount.URI) - builder.WriteString("&origin=" + originAccount.URI) - if statusID != "" { - builder.WriteString("&statusID=" + statusID) + builder.WriteString("&targetAcct=" + targetAccount.URI) + builder.WriteString("&originAcct=" + originAccount.URI) + if statusOrEditID != "" { + builder.WriteString("&statusOrEditID=" + statusOrEditID) } return builder.String() } @@ -582,13 +631,13 @@ func getNotifyLockURI( // for non-local first. // // targetAccount and originAccount must be -// set, but statusID can be an empty string. +// set, but statusOrEditID can be empty. func (s *Surface) Notify( ctx context.Context, notificationType gtsmodel.NotificationType, targetAccount *gtsmodel.Account, originAccount *gtsmodel.Account, - statusID string, + statusOrEditID string, ) error { if targetAccount.IsRemote() { // nothing to do. @@ -601,7 +650,7 @@ func (s *Surface) Notify( notificationType, targetAccount, originAccount, - statusID, + statusOrEditID, ) unlock := s.State.ProcessingLocks.Lock(lockURI) @@ -617,7 +666,7 @@ func (s *Surface) Notify( notificationType, targetAccount.ID, originAccount.ID, - statusID, + statusOrEditID, ); err == nil { // Notification exists; // nothing to do. @@ -636,7 +685,7 @@ func (s *Surface) Notify( TargetAccount: targetAccount, OriginAccountID: originAccount.ID, OriginAccount: originAccount, - StatusID: statusID, + StatusOrEditID: statusOrEditID, } if err := s.State.DB.PutNotification(ctx, notif); err != nil { |
