diff --git a/internal/cache/db.go b/internal/cache/db.go index ac16e26c5..78cc01e06 100644 --- a/internal/cache/db.go +++ b/internal/cache/db.go @@ -1155,7 +1155,7 @@ func (c *Caches) initNotification() { c.DB.Notification.Init(structr.CacheConfig[*gtsmodel.Notification]{ Indices: []structr.IndexConfig{ {Fields: "ID"}, - {Fields: "NotificationType,TargetAccountID,OriginAccountID,StatusID", AllowZero: true}, + {Fields: "NotificationType,TargetAccountID,OriginAccountID,StatusOrEditID", AllowZero: true}, }, MaxSize: cap, IgnoreErr: ignoreErrors, diff --git a/internal/cache/size.go b/internal/cache/size.go index b08a62a85..7898f9dfd 100644 --- a/internal/cache/size.go +++ b/internal/cache/size.go @@ -573,7 +573,7 @@ func sizeofNotification() uintptr { CreatedAt: exampleTime, TargetAccountID: exampleID, OriginAccountID: exampleID, - StatusID: exampleID, + StatusOrEditID: exampleID, Read: func() *bool { ok := false; return &ok }(), })) } diff --git a/internal/db/bundb/notification.go b/internal/db/bundb/notification.go index 77d4861b2..2f4989c33 100644 --- a/internal/db/bundb/notification.go +++ b/internal/db/bundb/notification.go @@ -54,24 +54,28 @@ func (n *notificationDB) GetNotificationByID(ctx context.Context, id string) (*g func (n *notificationDB) GetNotification( ctx context.Context, - notificationType gtsmodel.NotificationType, - targetAccountID string, - originAccountID string, - statusID string, + notifType gtsmodel.NotificationType, + targetAcctID string, + originAcctID string, + statusOrEditID string, ) (*gtsmodel.Notification, error) { return n.getNotification( ctx, - "NotificationType,TargetAccountID,OriginAccountID,StatusID", + "NotificationType,TargetAccountID,OriginAccountID,StatusOrEditID", func(notif *gtsmodel.Notification) error { - return n.db.NewSelect(). + q := n.db.NewSelect(). Model(notif). - Where("? = ?", bun.Ident("notification_type"), notificationType). - Where("? = ?", bun.Ident("target_account_id"), targetAccountID). - Where("? = ?", bun.Ident("origin_account_id"), originAccountID). - Where("? = ?", bun.Ident("status_id"), statusID). - Scan(ctx) + Where("? = ?", bun.Ident("notification_type"), notifType). + Where("? = ?", bun.Ident("target_account_id"), targetAcctID). + Where("? = ?", bun.Ident("origin_account_id"), originAcctID) + + if statusOrEditID != "" { + q = q.Where("? = ?", bun.Ident("status_id"), statusOrEditID) + } + + return q.Scan(ctx) }, - notificationType, targetAccountID, originAccountID, statusID, + notifType, targetAcctID, originAcctID, statusOrEditID, ) } @@ -176,14 +180,29 @@ func (n *notificationDB) PopulateNotification(ctx context.Context, notif *gtsmod } } - if notif.StatusID != "" && notif.Status == nil { + if notif.StatusOrEditID != "" && notif.Status == nil { + // Try getting status by ID first. notif.Status, err = n.state.DB.GetStatusByID( gtscontext.SetBarebones(ctx), - notif.StatusID, + notif.StatusOrEditID, ) - if err != nil { + if err != nil && !errors.Is(err, db.ErrNoEntries) { + // Only append real db error. It might be an edit ID. errs.Appendf("error populating notif status: %w", err) } + + if notif.Status == nil { + // If it's still not set, try + // getting status by edit ID. + notif.Status, err = n.state.DB.GetStatusByEditID( + gtscontext.SetBarebones(ctx), + notif.StatusOrEditID, + ) + if err != nil { + // Append any error here as it's an issue. + errs.Appendf("error populating notif status: %w", err) + } + } } return errs.Combine() diff --git a/internal/db/bundb/notification_test.go b/internal/db/bundb/notification_test.go index a6dcdd407..b3104f905 100644 --- a/internal/db/bundb/notification_test.go +++ b/internal/db/bundb/notification_test.go @@ -70,7 +70,7 @@ func (suite *NotificationTestSuite) spamNotifs() { CreatedAt: time.Now(), TargetAccountID: targetAccountID, OriginAccountID: originAccountID, - StatusID: statusID, + StatusOrEditID: statusID, Read: util.Ptr(false), } @@ -263,7 +263,7 @@ func (suite *NotificationTestSuite) TestDeleteNotificationsPertainingToStatusID( } for _, n := range notif { - if n.StatusID == testStatus.ID { + if n.StatusOrEditID == testStatus.ID { suite.FailNowf("", "no notifications with status id %s should remain", testStatus.ID) } } diff --git a/internal/db/bundb/status.go b/internal/db/bundb/status.go index f33362a3d..cf4a2549a 100644 --- a/internal/db/bundb/status.go +++ b/internal/db/bundb/status.go @@ -732,3 +732,105 @@ func (s *statusDB) GetDirectStatusIDsBatch(ctx context.Context, minID string, ma } return statusIDs, nil } + +func (s *statusDB) GetStatusInteractions( + ctx context.Context, + statusID string, + localOnly bool, +) ([]gtsmodel.Interaction, error) { + // Prepare to get interactions. + interactions := []gtsmodel.Interaction{} + + // Gather faves. + faves, err := s.state.DB.GetStatusFaves(ctx, statusID) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + return nil, err + } + + for _, fave := range faves { + // Get account at least. + if fave.Account == nil { + fave.Account, err = s.state.DB.GetAccountByID(ctx, fave.AccountID) + if err != nil { + log.Errorf(ctx, "error getting account for fave: %v", err) + continue + } + } + + if localOnly && !fave.Account.IsLocal() { + // Skip not local. + continue + } + + interactions = append(interactions, fave) + } + + // Gather replies. + replies, err := s.state.DB.GetStatusReplies(ctx, statusID) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + return nil, err + } + + for _, reply := range replies { + // Get account at least. + if reply.Account == nil { + reply.Account, err = s.state.DB.GetAccountByID(ctx, reply.AccountID) + if err != nil { + log.Errorf(ctx, "error getting account for reply: %v", err) + continue + } + } + + if localOnly && !reply.Account.IsLocal() { + // Skip not local. + continue + } + + interactions = append(interactions, reply) + } + + // Gather boosts. + boosts, err := s.state.DB.GetStatusBoosts(ctx, statusID) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + return nil, err + } + + for _, boost := range boosts { + // Get account at least. + if boost.Account == nil { + boost.Account, err = s.state.DB.GetAccountByID(ctx, boost.AccountID) + if err != nil { + log.Errorf(ctx, "error getting account for boost: %v", err) + continue + } + } + + if localOnly && !boost.Account.IsLocal() { + // Skip not local. + continue + } + + interactions = append(interactions, boost) + } + + if len(interactions) == 0 { + return nil, db.ErrNoEntries + } + + return interactions, nil +} + +func (s *statusDB) GetStatusByEditID( + ctx context.Context, + editID string, +) (*gtsmodel.Status, error) { + edit, err := s.state.DB.GetStatusEditByID( + gtscontext.SetBarebones(ctx), + editID, + ) + if err != nil { + return nil, err + } + + return s.GetStatusByID(ctx, edit.StatusID) +} diff --git a/internal/db/notification.go b/internal/db/notification.go index 029a3bc62..4c282d279 100644 --- a/internal/db/notification.go +++ b/internal/db/notification.go @@ -39,8 +39,14 @@ type Notification interface { GetNotificationsByIDs(ctx context.Context, ids []string) ([]*gtsmodel.Notification, error) // GetNotification gets one notification according to the provided parameters, if it exists. - // Since not all notifications are about a status, statusID can be an empty string. - GetNotification(ctx context.Context, notificationType gtsmodel.NotificationType, targetAccountID string, originAccountID string, statusID string) (*gtsmodel.Notification, error) + // Since not all notifications are about a status or an edit, statusOrEditID can be empty. + GetNotification( + ctx context.Context, + notifType gtsmodel.NotificationType, + targetAcctID string, + originAcctID string, + statusOrEditID string, + ) (*gtsmodel.Notification, error) // PopulateNotification ensures that the notification's struct fields are populated. PopulateNotification(ctx context.Context, notif *gtsmodel.Notification) error diff --git a/internal/db/status.go b/internal/db/status.go index adb31d391..d1bdb6106 100644 --- a/internal/db/status.go +++ b/internal/db/status.go @@ -94,4 +94,12 @@ type Status interface { // MaxDirectStatusID, and expects to eventually return the status with that ID. // It is used only by the conversation advanced migration. GetDirectStatusIDsBatch(ctx context.Context, minID string, maxIDInclusive string, count int) ([]string, error) + + // GetStatusInteractions gets all abstract "interactions" of a status (likes, replies, boosts). + // If localOnly is true, will return only interactions performed by accounts on this instance. + // Aside from that, interactions are not filtered or deduplicated, it's up to the caller to do that. + GetStatusInteractions(ctx context.Context, statusID string, localOnly bool) ([]gtsmodel.Interaction, error) + + // GetStatusByEditID gets one status corresponding to the given edit ID. + GetStatusByEditID(ctx context.Context, editID string) (*gtsmodel.Status, error) } diff --git a/internal/gtsmodel/interaction.go b/internal/gtsmodel/interaction.go index 92dd1a4e0..0b9ee693e 100644 --- a/internal/gtsmodel/interaction.go +++ b/internal/gtsmodel/interaction.go @@ -95,3 +95,10 @@ func (ir *InteractionRequest) IsAccepted() bool { func (ir *InteractionRequest) IsRejected() bool { return !ir.RejectedAt.IsZero() } + +// Interaction abstractly represents +// one interaction with a status, via +// liking, replying to, or boosting it. +type Interaction interface { + GetAccount() *Account +} diff --git a/internal/gtsmodel/notification.go b/internal/gtsmodel/notification.go index d160e0883..cc892bdd5 100644 --- a/internal/gtsmodel/notification.go +++ b/internal/gtsmodel/notification.go @@ -32,8 +32,8 @@ type Notification struct { TargetAccount *Account `bun:"-"` // Account corresponding to TargetAccountID. Can be nil, always check first + select using ID if necessary. OriginAccountID string `bun:"type:CHAR(26),nullzero,notnull"` // ID of the account that performed the action that created the notification. OriginAccount *Account `bun:"-"` // Account corresponding to OriginAccountID. Can be nil, always check first + select using ID if necessary. - StatusID string `bun:"type:CHAR(26),nullzero"` // If the notification pertains to a status, what is the database ID of that status? - Status *Status `bun:"-"` // Status corresponding to StatusID. Can be nil, always check first + select using ID if necessary. + StatusOrEditID string `bun:"status_id,type:CHAR(26),nullzero"` // If the notification pertains to a status or a status edit event, what is the database ID of the status or status edit? + Status *Status `bun:"-"` // Status corresponding to StatusOrEditID. Can be nil, always check first + select using ID if necessary. Read *bool `bun:",nullzero,notnull,default:false"` // Notification has been seen/read } diff --git a/internal/gtsmodel/status.go b/internal/gtsmodel/status.go index eca50416e..884caac0c 100644 --- a/internal/gtsmodel/status.go +++ b/internal/gtsmodel/status.go @@ -89,6 +89,13 @@ func (s *Status) GetAccountID() string { return s.AccountID } +// GetAccount returns the account that owns +// this status. May be nil if status not populated. +// Fulfils Interaction interface. +func (s *Status) GetAccount() *Account { + return s.Account +} + // GetBoostOfID implements timeline.Timelineable{}. func (s *Status) GetBoostOfID() string { return s.BoostOfID diff --git a/internal/gtsmodel/statusfave.go b/internal/gtsmodel/statusfave.go index 9d6c6335b..6fe4ed821 100644 --- a/internal/gtsmodel/statusfave.go +++ b/internal/gtsmodel/statusfave.go @@ -35,3 +35,10 @@ type StatusFave struct { PreApproved bool `bun:"-"` // If true, then fave targets a status on our instance, has permission to do the interaction, and an Accept should be sent out for it immediately. Field not stored in the DB. ApprovedByURI string `bun:",nullzero"` // URI of an Accept Activity that approves this Like. } + +// GetAccount returns the account that owns +// this fave. May be nil if fave not populated. +// Fulfils Interaction interface. +func (f *StatusFave) GetAccount() *Account { + return f.Account +} 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 { diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index ffd971040..a22e504c0 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -2055,60 +2055,43 @@ func (c *Converter) NotificationToAPINotification( filters []*gtsmodel.Filter, mutes *usermute.CompiledUserMuteList, ) (*apimodel.Notification, error) { - if n.TargetAccount == nil { - tAccount, err := c.state.DB.GetAccountByID(ctx, n.TargetAccountID) - if err != nil { - return nil, fmt.Errorf("NotificationToapi: error getting target account with id %s from the db: %s", n.TargetAccountID, err) - } - n.TargetAccount = tAccount - } - - if n.OriginAccount == nil { - ogAccount, err := c.state.DB.GetAccountByID(ctx, n.OriginAccountID) - if err != nil { - return nil, fmt.Errorf("NotificationToapi: error getting origin account with id %s from the db: %s", n.OriginAccountID, err) - } - n.OriginAccount = ogAccount + // Ensure notif populated. + if err := c.state.DB.PopulateNotification(ctx, n); err != nil { + return nil, gtserror.Newf("error populating notification: %w", err) } + // Get account that triggered this notif. apiAccount, err := c.AccountToAPIAccountPublic(ctx, n.OriginAccount) if err != nil { - return nil, fmt.Errorf("NotificationToapi: error converting account to api: %s", err) + return nil, gtserror.Newf("error converting account to api: %w", err) } + // Get status that triggered this notif, if set. var apiStatus *apimodel.Status - if n.StatusID != "" { - if n.Status == nil { - status, err := c.state.DB.GetStatusByID(ctx, n.StatusID) - if err != nil { - return nil, fmt.Errorf("NotificationToapi: error getting status with id %s from the db: %s", n.StatusID, err) - } - n.Status = status + if n.Status != nil { + apiStatus, err = c.StatusToAPIStatus( + ctx, n.Status, + n.TargetAccount, + statusfilter.FilterContextNotifications, + filters, mutes, + ) + if err != nil && !errors.Is(err, statusfilter.ErrHideStatus) { + return nil, gtserror.Newf("error converting status to api: %w", err) } - if n.Status.Account == nil { - if n.Status.AccountID == n.TargetAccount.ID { - n.Status.Account = n.TargetAccount - } else if n.Status.AccountID == n.OriginAccount.ID { - n.Status.Account = n.OriginAccount - } + if apiStatus == nil { + // Notif filtered for this + // status, nothing to do. + return nil, err } - var err error - apiStatus, err = c.StatusToAPIStatus(ctx, n.Status, n.TargetAccount, statusfilter.FilterContextNotifications, filters, mutes) - if err != nil { - if errors.Is(err, statusfilter.ErrHideStatus) { - return nil, err - } - return nil, fmt.Errorf("NotificationToapi: error converting status to api: %s", err) + if apiStatus.Reblog != nil { + // Use the actual reblog status + // for the notifications endpoint. + apiStatus = apiStatus.Reblog.Status } } - if apiStatus != nil && apiStatus.Reblog != nil { - // use the actual reblog status for the notifications endpoint - apiStatus = apiStatus.Reblog.Status - } - return &apimodel.Notification{ ID: n.ID, Type: n.NotificationType.String(), diff --git a/internal/webpush/realsender_test.go b/internal/webpush/realsender_test.go index b0144d4d4..cf9cf4cac 100644 --- a/internal/webpush/realsender_test.go +++ b/internal/webpush/realsender_test.go @@ -260,7 +260,7 @@ func (suite *RealSenderStandardTestSuite) TestSendPolicyMismatch() { NotificationType: gtsmodel.NotificationFavourite, TargetAccountID: suite.testAccounts["local_account_1"].ID, OriginAccountID: suite.testAccounts["remote_account_1"].ID, - StatusID: "01F8MHAMCHF6Y650WCRSCP4WMY", + StatusOrEditID: "01F8MHAMCHF6Y650WCRSCP4WMY", Read: util.Ptr(false), } if err := suite.db.PutNotification(context.Background(), notification); !suite.NoError(err) { diff --git a/testrig/testmodels.go b/testrig/testmodels.go index 5f0c2f032..30010c58c 100644 --- a/testrig/testmodels.go +++ b/testrig/testmodels.go @@ -3193,7 +3193,7 @@ func NewTestNotifications() map[string]*gtsmodel.Notification { CreatedAt: TimeMustParse("2022-05-14T13:21:09+02:00"), TargetAccountID: "01F8MH1H7YV1Z7D2C8K2730QBF", OriginAccountID: "01F8MH17FWEB39HZJ76B6VXSKF", - StatusID: "01F8MHAMCHF6Y650WCRSCP4WMY", + StatusOrEditID: "01F8MHAMCHF6Y650WCRSCP4WMY", Read: util.Ptr(false), }, "local_account_2_like": { @@ -3202,7 +3202,7 @@ func NewTestNotifications() map[string]*gtsmodel.Notification { CreatedAt: TimeMustParse("2022-01-13T12:45:01+02:00"), TargetAccountID: "01F8MH17FWEB39HZJ76B6VXSKF", OriginAccountID: "01F8MH5NBDF2MV7CTC4Q5128HF", - StatusID: "01F8MH75CBF9JFX4ZAD54N0W0R", + StatusOrEditID: "01F8MH75CBF9JFX4ZAD54N0W0R", Read: util.Ptr(false), }, "new_signup": { @@ -3211,7 +3211,7 @@ func NewTestNotifications() map[string]*gtsmodel.Notification { CreatedAt: TimeMustParse("2022-06-04T13:12:00Z"), TargetAccountID: "01F8MH17FWEB39HZJ76B6VXSKF", OriginAccountID: "01F8MH0BBE4FHXPH513MBVFHB0", - StatusID: "", + StatusOrEditID: "", Read: util.Ptr(false), }, }