[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:
Vyr Cossont
2025-01-23 16:47:30 -08:00
committed by GitHub
parent 9333bbc4d0
commit 5b765d734e
134 changed files with 21525 additions and 125 deletions

View File

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