mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-11-18 12:47:29 -06:00
[feature] Push notifications (#3587)
* Update push subscription API model to be Mastodon 4.0 compatible * Add webpush-go dependency # Conflicts: # go.sum * Single-row table for storing instance's VAPID key pair * Generate VAPID key pair during startup * Add VAPID public key to instance info API * Return VAPID public key when registering an app * Store Web Push subscriptions in DB * Add Web Push sender (similar to email sender) * Add no-op push senders to most processor tests * Test Web Push notifications from workers * Delete Web Push subscriptions when account is deleted * Implement push subscription API * Linter fixes * Update Swagger * Fix enum to int migration * Fix GetVAPIDKeyPair * Create web push subscriptions table with indexes * Log Web Push server error messages * Send instance URL as Web Push JWT subject * Accept any 2xx code as a success * Fix malformed VAPID sub claim * Use packed notification flags * Remove unused date columns * Add notification type for update notifications Not used yet * Make GetVAPIDKeyPair idempotent and remove PutVAPIDKeyPair * Post-rebase fixes * go mod tidy * Special-case 400 errors other than 408/429 Most client errors should remove the subscription. * Improve titles, trim body to reasonable length * Disallow cleartext HTTP for Web Push servers * Fix lint * Remove redundant index on unique column Also removes redundant unique and notnull tags on ID column since these are implied by pk * Make realsender.go more readable * Use Tobi's style for wrapping errors * Restore treating all 5xx codes as temporary problems * Always load target account settings * Stub `policy` and `standard` * webpush.Sender: take type converter as ctor param * Move webpush.MockSender and noopSender into testrig
This commit is contained in:
parent
9333bbc4d0
commit
5b765d734e
134 changed files with 21525 additions and 125 deletions
|
|
@ -179,6 +179,28 @@ func (suite *FromClientAPITestSuite) checkStreamed(
|
|||
}
|
||||
}
|
||||
|
||||
// checkWebPushed asserts that the target account got a single Web Push notification with a given type.
|
||||
func (suite *FromClientAPITestSuite) checkWebPushed(
|
||||
sender *testrig.WebPushMockSender,
|
||||
accountID string,
|
||||
notificationType gtsmodel.NotificationType,
|
||||
) {
|
||||
pushedNotifications := sender.Sent[accountID]
|
||||
if suite.Len(pushedNotifications, 1) {
|
||||
pushedNotification := pushedNotifications[0]
|
||||
suite.Equal(notificationType, pushedNotification.NotificationType)
|
||||
}
|
||||
}
|
||||
|
||||
// checkNotWebPushed asserts that the target account got no Web Push notifications.
|
||||
func (suite *FromClientAPITestSuite) checkNotWebPushed(
|
||||
sender *testrig.WebPushMockSender,
|
||||
accountID string,
|
||||
) {
|
||||
pushedNotifications := sender.Sent[accountID]
|
||||
suite.Len(pushedNotifications, 0)
|
||||
}
|
||||
|
||||
func (suite *FromClientAPITestSuite) statusJSON(
|
||||
ctx context.Context,
|
||||
typeConverter *typeutils.Converter,
|
||||
|
|
@ -341,6 +363,9 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithNotification() {
|
|||
string(notifJSON),
|
||||
stream.EventTypeNotification,
|
||||
)
|
||||
|
||||
// Check for a Web Push status notification.
|
||||
suite.checkWebPushed(testStructs.WebPushSender, receivingAccount.ID, gtsmodel.NotificationStatus)
|
||||
}
|
||||
|
||||
func (suite *FromClientAPITestSuite) TestProcessCreateStatusReply() {
|
||||
|
|
@ -409,6 +434,9 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusReply() {
|
|||
statusJSON,
|
||||
stream.EventTypeUpdate,
|
||||
)
|
||||
|
||||
// Check for absence of Web Push notifications.
|
||||
suite.checkNotWebPushed(testStructs.WebPushSender, receivingAccount.ID)
|
||||
}
|
||||
|
||||
func (suite *FromClientAPITestSuite) TestProcessCreateStatusReplyMuted() {
|
||||
|
|
@ -470,6 +498,9 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusReplyMuted() {
|
|||
|
||||
suite.ErrorIs(err, db.ErrNoEntries)
|
||||
suite.Nil(notif)
|
||||
|
||||
// Check for absence of Web Push notifications.
|
||||
suite.checkNotWebPushed(testStructs.WebPushSender, receivingAccount.ID)
|
||||
}
|
||||
|
||||
func (suite *FromClientAPITestSuite) TestProcessCreateStatusBoostMuted() {
|
||||
|
|
@ -531,6 +562,9 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusBoostMuted() {
|
|||
|
||||
suite.ErrorIs(err, db.ErrNoEntries)
|
||||
suite.Nil(notif)
|
||||
|
||||
// Check for absence of Web Push notifications.
|
||||
suite.checkNotWebPushed(testStructs.WebPushSender, receivingAccount.ID)
|
||||
}
|
||||
|
||||
func (suite *FromClientAPITestSuite) TestProcessCreateStatusListRepliesPolicyListOnlyOK() {
|
||||
|
|
@ -607,6 +641,9 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusListRepliesPolicyLis
|
|||
statusJSON,
|
||||
stream.EventTypeUpdate,
|
||||
)
|
||||
|
||||
// Check for absence of Web Push notifications.
|
||||
suite.checkNotWebPushed(testStructs.WebPushSender, receivingAccount.ID)
|
||||
}
|
||||
|
||||
func (suite *FromClientAPITestSuite) TestProcessCreateStatusListRepliesPolicyListOnlyNo() {
|
||||
|
|
@ -689,6 +726,9 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusListRepliesPolicyLis
|
|||
"",
|
||||
"",
|
||||
)
|
||||
|
||||
// Check for absence of Web Push notifications.
|
||||
suite.checkNotWebPushed(testStructs.WebPushSender, receivingAccount.ID)
|
||||
}
|
||||
|
||||
func (suite *FromClientAPITestSuite) TestProcessCreateStatusReplyListRepliesPolicyNone() {
|
||||
|
|
@ -765,6 +805,9 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusReplyListRepliesPoli
|
|||
"",
|
||||
"",
|
||||
)
|
||||
|
||||
// Check for absence of Web Push notifications.
|
||||
suite.checkNotWebPushed(testStructs.WebPushSender, receivingAccount.ID)
|
||||
}
|
||||
|
||||
func (suite *FromClientAPITestSuite) TestProcessCreateStatusBoost() {
|
||||
|
|
@ -829,6 +872,9 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusBoost() {
|
|||
statusJSON,
|
||||
stream.EventTypeUpdate,
|
||||
)
|
||||
|
||||
// Check for absence of Web Push notifications.
|
||||
suite.checkNotWebPushed(testStructs.WebPushSender, receivingAccount.ID)
|
||||
}
|
||||
|
||||
func (suite *FromClientAPITestSuite) TestProcessCreateStatusBoostNoReblogs() {
|
||||
|
|
@ -981,6 +1027,9 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWhichBeginsConversat
|
|||
conversationJSON,
|
||||
stream.EventTypeConversation,
|
||||
)
|
||||
|
||||
// Check for a Web Push mention notification.
|
||||
suite.checkWebPushed(testStructs.WebPushSender, receivingAccount.ID, gtsmodel.NotificationMention)
|
||||
}
|
||||
|
||||
// A public message to a local user should not result in a conversation notification.
|
||||
|
|
@ -1050,6 +1099,9 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWhichShouldNotCreate
|
|||
"",
|
||||
"",
|
||||
)
|
||||
|
||||
// Check for a Web Push mention notification.
|
||||
suite.checkWebPushed(testStructs.WebPushSender, receivingAccount.ID, gtsmodel.NotificationMention)
|
||||
}
|
||||
|
||||
// A public status with a hashtag followed by a local user who does not otherwise follow the author
|
||||
|
|
@ -1123,6 +1175,9 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithFollowedHashtag(
|
|||
"",
|
||||
stream.EventTypeUpdate,
|
||||
)
|
||||
|
||||
// Check for absence of Web Push notifications.
|
||||
suite.checkNotWebPushed(testStructs.WebPushSender, receivingAccount.ID)
|
||||
}
|
||||
|
||||
// A public status with a hashtag followed by a local user who does not otherwise follow the author
|
||||
|
|
@ -1204,6 +1259,9 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithFollowedHashtagA
|
|||
"",
|
||||
"",
|
||||
)
|
||||
|
||||
// Check for absence of Web Push notifications.
|
||||
suite.checkNotWebPushed(testStructs.WebPushSender, receivingAccount.ID)
|
||||
}
|
||||
|
||||
// A boost of a public status with a hashtag followed by a local user
|
||||
|
|
@ -1306,6 +1364,9 @@ func (suite *FromClientAPITestSuite) TestProcessCreateBoostWithFollowedHashtag()
|
|||
"",
|
||||
stream.EventTypeUpdate,
|
||||
)
|
||||
|
||||
// Check for absence of Web Push notifications.
|
||||
suite.checkNotWebPushed(testStructs.WebPushSender, receivingAccount.ID)
|
||||
}
|
||||
|
||||
// A boost of a public status with a hashtag followed by a local user
|
||||
|
|
@ -1416,6 +1477,9 @@ func (suite *FromClientAPITestSuite) TestProcessCreateBoostWithFollowedHashtagAn
|
|||
"",
|
||||
"",
|
||||
)
|
||||
|
||||
// Check for absence of Web Push notifications.
|
||||
suite.checkNotWebPushed(testStructs.WebPushSender, receivingAccount.ID)
|
||||
}
|
||||
|
||||
// A boost of a public status with a hashtag followed by a local user
|
||||
|
|
@ -1526,6 +1590,9 @@ func (suite *FromClientAPITestSuite) TestProcessCreateBoostWithFollowedHashtagAn
|
|||
"",
|
||||
"",
|
||||
)
|
||||
|
||||
// Check for absence of Web Push notifications.
|
||||
suite.checkNotWebPushed(testStructs.WebPushSender, receivingAccount.ID)
|
||||
}
|
||||
|
||||
// A public status with a hashtag followed by a local user who follows the author and has them on an exclusive list
|
||||
|
|
@ -1598,6 +1665,9 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithAuthorOnExclusiv
|
|||
"",
|
||||
"",
|
||||
)
|
||||
|
||||
// Check for absence of Web Push notifications.
|
||||
suite.checkNotWebPushed(testStructs.WebPushSender, receivingAccount.ID)
|
||||
}
|
||||
|
||||
// A public status with a hashtag followed by a local user who follows the author and has them on an exclusive list
|
||||
|
|
@ -1712,6 +1782,9 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithAuthorOnExclusiv
|
|||
"",
|
||||
"",
|
||||
)
|
||||
|
||||
// Check for absence of Web Push notifications.
|
||||
suite.checkNotWebPushed(testStructs.WebPushSender, receivingAccount.ID)
|
||||
}
|
||||
|
||||
// A public status with a hashtag followed by a local user who follows the author and has them on an exclusive list
|
||||
|
|
@ -1837,6 +1910,9 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithAuthorOnExclusiv
|
|||
"",
|
||||
"",
|
||||
)
|
||||
|
||||
// Check for a Web Push status notification.
|
||||
suite.checkWebPushed(testStructs.WebPushSender, receivingAccount.ID, gtsmodel.NotificationStatus)
|
||||
}
|
||||
|
||||
// Updating a public status with a hashtag followed by a local user who does not otherwise follow the author
|
||||
|
|
@ -1910,6 +1986,9 @@ func (suite *FromClientAPITestSuite) TestProcessUpdateStatusWithFollowedHashtag(
|
|||
"",
|
||||
stream.EventTypeStatusUpdate,
|
||||
)
|
||||
|
||||
// Check for absence of Web Push notifications.
|
||||
suite.checkNotWebPushed(testStructs.WebPushSender, receivingAccount.ID)
|
||||
}
|
||||
|
||||
func (suite *FromClientAPITestSuite) TestProcessStatusDelete() {
|
||||
|
|
@ -1963,6 +2042,9 @@ func (suite *FromClientAPITestSuite) TestProcessStatusDelete() {
|
|||
stream.EventTypeDelete,
|
||||
)
|
||||
|
||||
// Check for absence of Web Push notifications.
|
||||
suite.checkNotWebPushed(testStructs.WebPushSender, receivingAccount.ID)
|
||||
|
||||
// Boost should no longer be in the database.
|
||||
if !testrig.WaitFor(func() bool {
|
||||
_, err := testStructs.State.DB.GetStatusByID(ctx, boostOfDeletedStatus.ID)
|
||||
|
|
|
|||
|
|
@ -240,7 +240,7 @@ func (suite *FromFediAPITestSuite) TestProcessFave() {
|
|||
notif := >smodel.Notification{}
|
||||
err = testStructs.State.DB.GetWhere(context.Background(), where, notif)
|
||||
suite.NoError(err)
|
||||
suite.Equal(gtsmodel.NotificationFave, notif.NotificationType)
|
||||
suite.Equal(gtsmodel.NotificationFavourite, notif.NotificationType)
|
||||
suite.Equal(fave.TargetAccountID, notif.TargetAccountID)
|
||||
suite.Equal(fave.AccountID, notif.OriginAccountID)
|
||||
suite.Equal(fave.StatusID, notif.StatusID)
|
||||
|
|
@ -313,7 +313,7 @@ func (suite *FromFediAPITestSuite) TestProcessFaveWithDifferentReceivingAccount(
|
|||
notif := >smodel.Notification{}
|
||||
err = testStructs.State.DB.GetWhere(context.Background(), where, notif)
|
||||
suite.NoError(err)
|
||||
suite.Equal(gtsmodel.NotificationFave, notif.NotificationType)
|
||||
suite.Equal(gtsmodel.NotificationFavourite, notif.NotificationType)
|
||||
suite.Equal(fave.TargetAccountID, notif.TargetAccountID)
|
||||
suite.Equal(fave.AccountID, notif.OriginAccountID)
|
||||
suite.Equal(fave.StatusID, notif.StatusID)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/processing/stream"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/webpush"
|
||||
)
|
||||
|
||||
// Surface wraps functions for 'surfacing' the result
|
||||
|
|
@ -38,5 +39,6 @@ type Surface struct {
|
|||
Stream *stream.Processor
|
||||
VisFilter *visibility.Filter
|
||||
EmailSender email.Sender
|
||||
WebPushSender webpush.Sender
|
||||
Conversations *conversations.Processor
|
||||
}
|
||||
|
|
|
|||
|
|
@ -250,7 +250,7 @@ func (s *Surface) notifyFave(
|
|||
// notify status author
|
||||
// of fave by account.
|
||||
if err := s.Notify(ctx,
|
||||
gtsmodel.NotificationFave,
|
||||
gtsmodel.NotificationFavourite,
|
||||
fave.TargetAccount,
|
||||
fave.Account,
|
||||
fave.StatusID,
|
||||
|
|
@ -521,7 +521,7 @@ func (s *Surface) notifySignup(ctx context.Context, newUser *gtsmodel.User) erro
|
|||
var errs gtserror.MultiError
|
||||
for _, mod := range modAccounts {
|
||||
if err := s.Notify(ctx,
|
||||
gtsmodel.NotificationSignup,
|
||||
gtsmodel.NotificationAdminSignup,
|
||||
mod,
|
||||
newUser.Account,
|
||||
"",
|
||||
|
|
@ -647,5 +647,10 @@ func (s *Surface) Notify(
|
|||
}
|
||||
s.Stream.Notify(ctx, targetAccount, apiNotif)
|
||||
|
||||
// Send Web Push notification to the user.
|
||||
if err = s.WebPushSender.Send(ctx, notif, filters, compiledMutes); err != nil {
|
||||
return gtserror.Newf("error sending Web Push notifications: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ func (suite *SurfaceNotifyTestSuite) TestSpamNotifs() {
|
|||
Stream: testStructs.Processor.Stream(),
|
||||
VisFilter: visibility.NewFilter(testStructs.State),
|
||||
EmailSender: testStructs.EmailSender,
|
||||
WebPushSender: testStructs.WebPushSender,
|
||||
Conversations: testStructs.Processor.Conversations(),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/processing/stream"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/webpush"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/workers"
|
||||
)
|
||||
|
||||
|
|
@ -44,6 +45,7 @@ func New(
|
|||
converter *typeutils.Converter,
|
||||
visFilter *visibility.Filter,
|
||||
emailSender email.Sender,
|
||||
webPushSender webpush.Sender,
|
||||
account *account.Processor,
|
||||
media *media.Processor,
|
||||
stream *stream.Processor,
|
||||
|
|
@ -65,6 +67,7 @@ func New(
|
|||
Stream: stream,
|
||||
VisFilter: visFilter,
|
||||
EmailSender: emailSender,
|
||||
WebPushSender: webPushSender,
|
||||
Conversations: conversations,
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue