From 5b765d734ee70f0a8a0790444d60969a727567f8 Mon Sep 17 00:00:00 2001 From: Vyr Cossont Date: Thu, 23 Jan 2025 16:47:30 -0800 Subject: [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 --- internal/cache/cache.go | 2 ++ internal/cache/db.go | 53 +++++++++++++++++++++++++++++++++++++++++--- internal/cache/invalidate.go | 10 +++++++++ internal/cache/size.go | 18 ++++++++++++++- 4 files changed, 79 insertions(+), 4 deletions(-) (limited to 'internal/cache') diff --git a/internal/cache/cache.go b/internal/cache/cache.go index 6f925e24f..5771b4e95 100644 --- a/internal/cache/cache.go +++ b/internal/cache/cache.go @@ -117,6 +117,8 @@ func (c *Caches) Init() { c.initUserMute() c.initUserMuteIDs() c.initWebfinger() + c.initWebPushSubscription() + c.initWebPushSubscriptionIDs() c.initVisibility() c.initStatusesFilterableFields() } diff --git a/internal/cache/db.go b/internal/cache/db.go index 1052446c4..180d81907 100644 --- a/internal/cache/db.go +++ b/internal/cache/db.go @@ -258,6 +258,15 @@ type DBCaches struct { // UserMuteIDs provides access to the user mute IDs database cache. UserMuteIDs SliceCache[string] + + // VAPIDKeyPair caches the server's VAPID key pair. + VAPIDKeyPair atomic.Pointer[gtsmodel.VAPIDKeyPair] + + // WebPushSubscription provides access to the gtsmodel WebPushSubscription database cache. + WebPushSubscription StructCache[*gtsmodel.WebPushSubscription] + + // WebPushSubscriptionIDs provides access to the Web Push subscription IDs database cache. + WebPushSubscriptionIDs SliceCache[string] } // NOTE: @@ -1579,9 +1588,10 @@ func (c *Caches) initToken() { {Fields: "Refresh"}, {Fields: "ClientID", Multiple: true}, }, - MaxSize: cap, - IgnoreErr: ignoreErrors, - Copy: copyF, + MaxSize: cap, + IgnoreErr: ignoreErrors, + Copy: copyF, + Invalidate: c.OnInvalidateToken, }) } @@ -1691,3 +1701,40 @@ func (c *Caches) initUserMuteIDs() { c.DB.UserMuteIDs.Init(0, cap) } + +func (c *Caches) initWebPushSubscription() { + cap := calculateResultCacheMax( + sizeofWebPushSubscription(), // model in-mem size. + config.GetCacheWebPushSubscriptionMemRatio(), + ) + + log.Infof(nil, "cache size = %d", cap) + + copyF := func(s1 *gtsmodel.WebPushSubscription) *gtsmodel.WebPushSubscription { + s2 := new(gtsmodel.WebPushSubscription) + *s2 = *s1 + return s2 + } + + c.DB.WebPushSubscription.Init(structr.CacheConfig[*gtsmodel.WebPushSubscription]{ + Indices: []structr.IndexConfig{ + {Fields: "ID"}, + {Fields: "TokenID"}, + {Fields: "AccountID", Multiple: true}, + }, + MaxSize: cap, + IgnoreErr: ignoreErrors, + Invalidate: c.OnInvalidateWebPushSubscription, + Copy: copyF, + }) +} + +func (c *Caches) initWebPushSubscriptionIDs() { + cap := calculateSliceCacheMax( + config.GetCacheWebPushSubscriptionIDsMemRatio(), + ) + + log.Infof(nil, "cache size = %d", cap) + + c.DB.WebPushSubscriptionIDs.Init(0, cap) +} diff --git a/internal/cache/invalidate.go b/internal/cache/invalidate.go index 42d7b7399..555c73cd7 100644 --- a/internal/cache/invalidate.go +++ b/internal/cache/invalidate.go @@ -283,6 +283,11 @@ func (c *Caches) OnInvalidateStatusFave(fave *gtsmodel.StatusFave) { c.DB.StatusFaveIDs.Invalidate(fave.StatusID) } +func (c *Caches) OnInvalidateToken(token *gtsmodel.Token) { + // Invalidate token's push subscription. + c.DB.WebPushSubscription.Invalidate("ID", token.ID) +} + func (c *Caches) OnInvalidateUser(user *gtsmodel.User) { // Invalidate local account ID cached visibility. c.Visibility.Invalidate("ItemID", user.AccountID) @@ -296,3 +301,8 @@ func (c *Caches) OnInvalidateUserMute(mute *gtsmodel.UserMute) { // Invalidate source account's user mute lists. c.DB.UserMuteIDs.Invalidate(mute.AccountID) } + +func (c *Caches) OnInvalidateWebPushSubscription(subscription *gtsmodel.WebPushSubscription) { + // Invalidate source account's Web Push subscription list. + c.DB.WebPushSubscriptionIDs.Invalidate(subscription.AccountID) +} diff --git a/internal/cache/size.go b/internal/cache/size.go index 24101683a..c96a3cd2e 100644 --- a/internal/cache/size.go +++ b/internal/cache/size.go @@ -66,6 +66,14 @@ you'll make society more equitable for all if you're not careful! :hammer_sickle // be a serialized string of almost any type, so we pick a // nice serialized key size on the upper end of normal. sizeofResultKey = 2 * sizeofIDStr + + // exampleWebPushAuth is a Base64-encoded 16-byte random auth secret. + // This secret is consumed as Base64 by webpush-go. + exampleWebPushAuth = "ZVxqlt5fzVgmSz2aqiA2XQ==" + + // exampleWebPushP256dh is a Base64-encoded DH P-256 public key. + // This secret is consumed as Base64 by webpush-go. + exampleWebPushP256dh = "OrpejO16gV97uBXew/T0I7YoUv/CX8fz0z4g8RrQ+edXJqQPjX3XVSo2P0HhcCpCOR1+Dzj5LFcK9jYNqX7SBg==" ) var ( @@ -576,7 +584,7 @@ func sizeofMove() uintptr { func sizeofNotification() uintptr { return uintptr(size.Of(>smodel.Notification{ ID: exampleID, - NotificationType: gtsmodel.NotificationFave, + NotificationType: gtsmodel.NotificationFavourite, CreatedAt: exampleTime, TargetAccountID: exampleID, OriginAccountID: exampleID, @@ -821,3 +829,11 @@ func sizeofUserMute() uintptr { Notifications: util.Ptr(false), })) } + +func sizeofWebPushSubscription() uintptr { + return uintptr(size.Of(>smodel.WebPushSubscription{ + TokenID: exampleID, + Auth: exampleWebPushAuth, + P256dh: exampleWebPushP256dh, + })) +} -- cgit v1.2.3