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/processing/account/delete.go | 6 +- internal/processing/admin/admin_test.go | 1 + internal/processing/processor.go | 10 +++ internal/processing/processor_test.go | 1 + internal/processing/push/create.go | 65 +++++++++++++++++ internal/processing/push/delete.go | 39 +++++++++++ internal/processing/push/get.go | 47 +++++++++++++ internal/processing/push/push.go | 85 +++++++++++++++++++++++ internal/processing/push/update.go | 63 +++++++++++++++++ internal/processing/timeline/notification.go | 2 +- internal/processing/workers/fromclientapi_test.go | 82 ++++++++++++++++++++++ internal/processing/workers/fromfediapi_test.go | 4 +- internal/processing/workers/surface.go | 2 + internal/processing/workers/surfacenotify.go | 9 ++- internal/processing/workers/surfacenotify_test.go | 1 + internal/processing/workers/workers.go | 3 + 16 files changed, 414 insertions(+), 6 deletions(-) create mode 100644 internal/processing/push/create.go create mode 100644 internal/processing/push/delete.go create mode 100644 internal/processing/push/get.go create mode 100644 internal/processing/push/push.go create mode 100644 internal/processing/push/update.go (limited to 'internal/processing') diff --git a/internal/processing/account/delete.go b/internal/processing/account/delete.go index c8d1ba5f9..2618fdfc5 100644 --- a/internal/processing/account/delete.go +++ b/internal/processing/account/delete.go @@ -96,7 +96,7 @@ func (p *Processor) Delete( } // deleteUserAndTokensForAccount deletes the gtsmodel.User and -// any OAuth tokens and applications for the given account. +// any OAuth tokens, applications, and Web Push subscriptions for the given account. // // Callers to this function should already have checked that // this is a local account, or else it won't have a user associated @@ -129,6 +129,10 @@ func (p *Processor) deleteUserAndTokensForAccount(ctx context.Context, account * } } + if err := p.state.DB.DeleteWebPushSubscriptionsByAccountID(ctx, account.ID); err != nil { + return gtserror.Newf("db error deleting Web Push subscriptions: %w", err) + } + columns, err := stubbifyUser(user) if err != nil { return gtserror.Newf("error stubbifying user: %w", err) diff --git a/internal/processing/admin/admin_test.go b/internal/processing/admin/admin_test.go index f0839f2f6..ad9d9b2ae 100644 --- a/internal/processing/admin/admin_test.go +++ b/internal/processing/admin/admin_test.go @@ -119,6 +119,7 @@ func (suite *AdminStandardTestSuite) SetupTest() { suite.mediaManager, &suite.state, suite.emailSender, + testrig.NewNoopWebPushSender(), visibility.NewFilter(&suite.state), interaction.NewFilter(&suite.state), ) diff --git a/internal/processing/processor.go b/internal/processing/processor.go index 8dabfba96..0bba23089 100644 --- a/internal/processing/processor.go +++ b/internal/processing/processor.go @@ -39,6 +39,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/processing/markers" "github.com/superseriousbusiness/gotosocial/internal/processing/media" "github.com/superseriousbusiness/gotosocial/internal/processing/polls" + "github.com/superseriousbusiness/gotosocial/internal/processing/push" "github.com/superseriousbusiness/gotosocial/internal/processing/report" "github.com/superseriousbusiness/gotosocial/internal/processing/search" "github.com/superseriousbusiness/gotosocial/internal/processing/status" @@ -51,6 +52,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/subscriptions" "github.com/superseriousbusiness/gotosocial/internal/text" "github.com/superseriousbusiness/gotosocial/internal/typeutils" + "github.com/superseriousbusiness/gotosocial/internal/webpush" ) // Processor groups together processing functions and @@ -88,6 +90,7 @@ type Processor struct { markers markers.Processor media media.Processor polls polls.Processor + push push.Processor report report.Processor search search.Processor status status.Processor @@ -146,6 +149,10 @@ func (p *Processor) Polls() *polls.Processor { return &p.polls } +func (p *Processor) Push() *push.Processor { + return &p.push +} + func (p *Processor) Report() *report.Processor { return &p.report } @@ -188,6 +195,7 @@ func NewProcessor( mediaManager *mm.Manager, state *state.State, emailSender email.Sender, + webPushSender webpush.Sender, visFilter *visibility.Filter, intFilter *interaction.Filter, ) *Processor { @@ -221,6 +229,7 @@ func NewProcessor( processor.list = list.New(state, converter) processor.markers = markers.New(state, converter) processor.polls = polls.New(&common, state, converter) + processor.push = push.New(state, converter) processor.report = report.New(state, converter) processor.tags = tags.New(state, converter) processor.timeline = timeline.New(state, converter, visFilter) @@ -241,6 +250,7 @@ func NewProcessor( converter, visFilter, emailSender, + webPushSender, &processor.account, &processor.media, &processor.stream, diff --git a/internal/processing/processor_test.go b/internal/processing/processor_test.go index f152f3fad..84ab9ef48 100644 --- a/internal/processing/processor_test.go +++ b/internal/processing/processor_test.go @@ -135,6 +135,7 @@ func (suite *ProcessingStandardTestSuite) SetupTest() { suite.mediaManager, &suite.state, suite.emailSender, + testrig.NewNoopWebPushSender(), visibility.NewFilter(&suite.state), interaction.NewFilter(&suite.state), ) diff --git a/internal/processing/push/create.go b/internal/processing/push/create.go new file mode 100644 index 000000000..42a67dc19 --- /dev/null +++ b/internal/processing/push/create.go @@ -0,0 +1,65 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package push + +import ( + "context" + + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/id" +) + +// CreateOrReplace creates a Web Push subscription for the given access token, +// or entirely replaces the previously existing subscription for that token. +func (p *Processor) CreateOrReplace( + ctx context.Context, + accountID string, + accessToken string, + request *apimodel.WebPushSubscriptionCreateRequest, +) (*apimodel.WebPushSubscription, gtserror.WithCode) { + tokenID, errWithCode := p.getTokenID(ctx, accessToken) + if errWithCode != nil { + return nil, errWithCode + } + + // Clear any previous subscription. + if err := p.state.DB.DeleteWebPushSubscriptionByTokenID(ctx, tokenID); err != nil { + err := gtserror.Newf("couldn't delete Web Push subscription for token ID %s: %w", tokenID, err) + return nil, gtserror.NewErrorInternalError(err) + } + + // Insert a new one. + subscription := >smodel.WebPushSubscription{ + ID: id.NewULID(), + AccountID: accountID, + TokenID: tokenID, + Endpoint: request.Subscription.Endpoint, + Auth: request.Subscription.Keys.Auth, + P256dh: request.Subscription.Keys.P256dh, + NotificationFlags: alertsToNotificationFlags(request.Data.Alerts), + } + + if err := p.state.DB.PutWebPushSubscription(ctx, subscription); err != nil { + err := gtserror.Newf("couldn't create Web Push subscription for token ID %s: %w", tokenID, err) + return nil, gtserror.NewErrorInternalError(err) + } + + return p.apiSubscription(ctx, subscription) +} diff --git a/internal/processing/push/delete.go b/internal/processing/push/delete.go new file mode 100644 index 000000000..6f5c61444 --- /dev/null +++ b/internal/processing/push/delete.go @@ -0,0 +1,39 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package push + +import ( + "context" + + "github.com/superseriousbusiness/gotosocial/internal/gtserror" +) + +// Delete deletes the Web Push subscription for the given access token, if there is one. +func (p *Processor) Delete(ctx context.Context, accessToken string) gtserror.WithCode { + tokenID, errWithCode := p.getTokenID(ctx, accessToken) + if errWithCode != nil { + return errWithCode + } + + if err := p.state.DB.DeleteWebPushSubscriptionByTokenID(ctx, tokenID); err != nil { + err := gtserror.Newf("couldn't delete Web Push subscription for token ID %s: %w", tokenID, err) + return gtserror.NewErrorInternalError(err) + } + + return nil +} diff --git a/internal/processing/push/get.go b/internal/processing/push/get.go new file mode 100644 index 000000000..542f08862 --- /dev/null +++ b/internal/processing/push/get.go @@ -0,0 +1,47 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package push + +import ( + "context" + "errors" + + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" +) + +// Get returns the Web Push subscription for the given access token. +func (p *Processor) Get(ctx context.Context, accessToken string) (*apimodel.WebPushSubscription, gtserror.WithCode) { + tokenID, errWithCode := p.getTokenID(ctx, accessToken) + if errWithCode != nil { + return nil, errWithCode + } + + subscription, err := p.state.DB.GetWebPushSubscriptionByTokenID(ctx, tokenID) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + err := gtserror.Newf("couldn't get Web Push subscription for token ID %s: %w", tokenID, err) + return nil, gtserror.NewErrorInternalError(err) + } + if subscription == nil { + err := errors.New("no Web Push subscription exists for this access token") + return nil, gtserror.NewErrorNotFound(err) + } + + return p.apiSubscription(ctx, subscription) +} diff --git a/internal/processing/push/push.go b/internal/processing/push/push.go new file mode 100644 index 000000000..f46280386 --- /dev/null +++ b/internal/processing/push/push.go @@ -0,0 +1,85 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package push + +import ( + "context" + + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/state" + "github.com/superseriousbusiness/gotosocial/internal/typeutils" +) + +type Processor struct { + state *state.State + converter *typeutils.Converter +} + +func New(state *state.State, converter *typeutils.Converter) Processor { + return Processor{ + state: state, + converter: converter, + } +} + +// getTokenID returns the token ID for a given access token. +// Since all push API calls require authentication, this should always be available. +func (p *Processor) getTokenID(ctx context.Context, accessToken string) (string, gtserror.WithCode) { + token, err := p.state.DB.GetTokenByAccess(ctx, accessToken) + if err != nil { + err := gtserror.Newf("couldn't find token ID for access token: %w", err) + return "", gtserror.NewErrorInternalError(err) + } + + return token.ID, nil +} + +// apiSubscription is a shortcut to return the API version of the given Web Push subscription, +// or return an appropriate error if conversion fails. +func (p *Processor) apiSubscription(ctx context.Context, subscription *gtsmodel.WebPushSubscription) (*apimodel.WebPushSubscription, gtserror.WithCode) { + apiSubscription, err := p.converter.WebPushSubscriptionToAPIWebPushSubscription(ctx, subscription) + if err != nil { + err := gtserror.Newf("error converting Web Push subscription %s to API representation: %w", subscription.ID, err) + return nil, gtserror.NewErrorInternalError(err) + } + + return apiSubscription, nil +} + +// alertsToNotificationFlags turns the alerts section of a push subscription API request into a packed bitfield. +func alertsToNotificationFlags(alerts *apimodel.WebPushSubscriptionAlerts) gtsmodel.WebPushSubscriptionNotificationFlags { + var n gtsmodel.WebPushSubscriptionNotificationFlags + + n.Set(gtsmodel.NotificationFollow, alerts.Follow) + n.Set(gtsmodel.NotificationFollowRequest, alerts.FollowRequest) + n.Set(gtsmodel.NotificationFavourite, alerts.Favourite) + n.Set(gtsmodel.NotificationMention, alerts.Mention) + n.Set(gtsmodel.NotificationReblog, alerts.Reblog) + n.Set(gtsmodel.NotificationPoll, alerts.Poll) + n.Set(gtsmodel.NotificationStatus, alerts.Status) + n.Set(gtsmodel.NotificationUpdate, alerts.Update) + n.Set(gtsmodel.NotificationAdminSignup, alerts.AdminSignup) + n.Set(gtsmodel.NotificationAdminReport, alerts.AdminReport) + n.Set(gtsmodel.NotificationPendingFave, alerts.PendingFavourite) + n.Set(gtsmodel.NotificationPendingReply, alerts.PendingReply) + n.Set(gtsmodel.NotificationPendingReblog, alerts.PendingReblog) + + return n +} diff --git a/internal/processing/push/update.go b/internal/processing/push/update.go new file mode 100644 index 000000000..370536f9b --- /dev/null +++ b/internal/processing/push/update.go @@ -0,0 +1,63 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package push + +import ( + "context" + "errors" + + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" +) + +// Update updates the Web Push subscription for the given access token. +func (p *Processor) Update( + ctx context.Context, + accessToken string, + request *apimodel.WebPushSubscriptionUpdateRequest, +) (*apimodel.WebPushSubscription, gtserror.WithCode) { + tokenID, errWithCode := p.getTokenID(ctx, accessToken) + if errWithCode != nil { + return nil, errWithCode + } + + // Get existing subscription. + subscription, err := p.state.DB.GetWebPushSubscriptionByTokenID(ctx, tokenID) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + err := gtserror.Newf("couldn't get Web Push subscription for token ID %s: %w", tokenID, err) + return nil, gtserror.NewErrorInternalError(err) + } + if subscription == nil { + err := errors.New("no Web Push subscription exists for this access token") + return nil, gtserror.NewErrorNotFound(err) + } + + // Update it. + subscription.NotificationFlags = alertsToNotificationFlags(request.Data.Alerts) + if err = p.state.DB.UpdateWebPushSubscription( + ctx, + subscription, + "notification_flags", + ); err != nil { + err := gtserror.Newf("couldn't update Web Push subscription for token ID %s: %w", tokenID, err) + return nil, gtserror.NewErrorInternalError(err) + } + + return p.apiSubscription(ctx, subscription) +} diff --git a/internal/processing/timeline/notification.go b/internal/processing/timeline/notification.go index a242c7b74..09636e7eb 100644 --- a/internal/processing/timeline/notification.go +++ b/internal/processing/timeline/notification.go @@ -184,7 +184,7 @@ func (p *Processor) notifVisible( // If this is a new local account sign-up, // skip normal visibility checking because // origin account won't be confirmed yet. - if n.NotificationType == gtsmodel.NotificationSignup { + if n.NotificationType == gtsmodel.NotificationAdminSignup { return true, nil } diff --git a/internal/processing/workers/fromclientapi_test.go b/internal/processing/workers/fromclientapi_test.go index d955f0529..acb25673d 100644 --- a/internal/processing/workers/fromclientapi_test.go +++ b/internal/processing/workers/fromclientapi_test.go @@ -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) diff --git a/internal/processing/workers/fromfediapi_test.go b/internal/processing/workers/fromfediapi_test.go index 88d0e6071..70886d698 100644 --- a/internal/processing/workers/fromfediapi_test.go +++ b/internal/processing/workers/fromfediapi_test.go @@ -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) diff --git a/internal/processing/workers/surface.go b/internal/processing/workers/surface.go index 4f6597b9a..4dc58c433 100644 --- a/internal/processing/workers/surface.go +++ b/internal/processing/workers/surface.go @@ -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 } diff --git a/internal/processing/workers/surfacenotify.go b/internal/processing/workers/surfacenotify.go index 1520d2ec0..fdbd5e3c1 100644 --- a/internal/processing/workers/surfacenotify.go +++ b/internal/processing/workers/surfacenotify.go @@ -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 } diff --git a/internal/processing/workers/surfacenotify_test.go b/internal/processing/workers/surfacenotify_test.go index 52ee89e8b..6444314e2 100644 --- a/internal/processing/workers/surfacenotify_test.go +++ b/internal/processing/workers/surfacenotify_test.go @@ -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(), } diff --git a/internal/processing/workers/workers.go b/internal/processing/workers/workers.go index ad673481b..9f37f554e 100644 --- a/internal/processing/workers/workers.go +++ b/internal/processing/workers/workers.go @@ -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, } -- cgit v1.2.3