diff options
author | 2025-01-23 16:47:30 -0800 | |
---|---|---|
committer | 2025-01-23 16:47:30 -0800 | |
commit | 5b765d734ee70f0a8a0790444d60969a727567f8 (patch) | |
tree | f76e05a6e5b22df17160be595c40e964bdbe5f22 /internal/processing | |
parent | [feature] Serve bot accounts over AP as Service instead of Person (#3672) (diff) | |
download | gotosocial-5b765d734ee70f0a8a0790444d60969a727567f8.tar.xz |
[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
Diffstat (limited to 'internal/processing')
-rw-r--r-- | internal/processing/account/delete.go | 6 | ||||
-rw-r--r-- | internal/processing/admin/admin_test.go | 1 | ||||
-rw-r--r-- | internal/processing/processor.go | 10 | ||||
-rw-r--r-- | internal/processing/processor_test.go | 1 | ||||
-rw-r--r-- | internal/processing/push/create.go | 65 | ||||
-rw-r--r-- | internal/processing/push/delete.go | 39 | ||||
-rw-r--r-- | internal/processing/push/get.go | 47 | ||||
-rw-r--r-- | internal/processing/push/push.go | 85 | ||||
-rw-r--r-- | internal/processing/push/update.go | 63 | ||||
-rw-r--r-- | internal/processing/timeline/notification.go | 2 | ||||
-rw-r--r-- | internal/processing/workers/fromclientapi_test.go | 82 | ||||
-rw-r--r-- | internal/processing/workers/fromfediapi_test.go | 4 | ||||
-rw-r--r-- | internal/processing/workers/surface.go | 2 | ||||
-rw-r--r-- | internal/processing/workers/surfacenotify.go | 9 | ||||
-rw-r--r-- | internal/processing/workers/surfacenotify_test.go | 1 | ||||
-rw-r--r-- | internal/processing/workers/workers.go | 3 |
16 files changed, 414 insertions, 6 deletions
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 <http://www.gnu.org/licenses/>. + +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 <http://www.gnu.org/licenses/>. + +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 <http://www.gnu.org/licenses/>. + +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 <http://www.gnu.org/licenses/>. + +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 <http://www.gnu.org/licenses/>. + +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, } |