summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLibravatar Vyr Cossont <VyrCossont@users.noreply.github.com>2025-02-03 02:25:53 -0800
committerLibravatar GitHub <noreply@github.com>2025-02-03 10:25:53 +0000
commit27844b7da2567491661f9ddd2d4662f9f1b3ce40 (patch)
tree1832fe4ec4d3a0fbf6d01bcb5f39acf6885cc1f5
parent[chore]: Bump github.com/tdewolff/minify/v2 from 2.21.2 to 2.21.3 (#3727) (diff)
downloadgotosocial-27844b7da2567491661f9ddd2d4662f9f1b3ce40.tar.xz
[feature] Implement Web Push notification policy (#3721)
* Web Push: add policy column to subscriptions * Web Push: add policy to API * Web Push: test notification policy * go-fmt unrelated file (how did this get thru?)
-rw-r--r--docs/api/swagger.yaml20
-rw-r--r--internal/api/client/push/pushsubscriptionpost.go11
-rw-r--r--internal/api/client/push/pushsubscriptionpost_test.go22
-rw-r--r--internal/api/client/push/pushsubscriptionput.go22
-rw-r--r--internal/api/client/push/pushsubscriptionput_test.go13
-rw-r--r--internal/api/model/webpushsubscription.go11
-rw-r--r--internal/db/bundb/migrations/20250131184755_add_web_push_subscription_policy.go83
-rw-r--r--internal/gtsmodel/webpushsubscription.go22
-rw-r--r--internal/processing/push/create.go2
-rw-r--r--internal/processing/push/update.go4
-rw-r--r--internal/typeutils/frontendtointernal.go14
-rw-r--r--internal/typeutils/internaltoas_test.go4
-rw-r--r--internal/typeutils/internaltofrontend.go16
-rw-r--r--internal/webpush/realsender.go65
-rw-r--r--internal/webpush/realsender_test.go65
-rw-r--r--testrig/testmodels.go1
16 files changed, 340 insertions, 35 deletions
diff --git a/docs/api/swagger.yaml b/docs/api/swagger.yaml
index 5ff5346bc..65d332227 100644
--- a/docs/api/swagger.yaml
+++ b/docs/api/swagger.yaml
@@ -9922,6 +9922,16 @@ paths:
in: formData
name: data[alerts][pending.reblog]
type: boolean
+ - default: all
+ description: Which accounts to receive push notifications from.
+ enum:
+ - all
+ - followed
+ - follower
+ - none
+ in: formData
+ name: data[policy]
+ type: string
produces:
- application/json
responses:
@@ -10019,6 +10029,16 @@ paths:
in: formData
name: data[alerts][pending.reblog]
type: boolean
+ - default: all
+ description: Which accounts to receive push notifications from.
+ enum:
+ - all
+ - followed
+ - follower
+ - none
+ in: formData
+ name: data[policy]
+ type: string
produces:
- application/json
responses:
diff --git a/internal/api/client/push/pushsubscriptionpost.go b/internal/api/client/push/pushsubscriptionpost.go
index a7e299894..cc1be185f 100644
--- a/internal/api/client/push/pushsubscriptionpost.go
+++ b/internal/api/client/push/pushsubscriptionpost.go
@@ -147,6 +147,17 @@ import (
// type: boolean
// default: false
// description: Receive a push notification when a boost is pending?
+// -
+// name: data[policy]
+// in: formData
+// type: string
+// enum:
+// - all
+// - followed
+// - follower
+// - none
+// default: all
+// description: Which accounts to receive push notifications from.
//
// security:
// - OAuth2 Bearer:
diff --git a/internal/api/client/push/pushsubscriptionpost_test.go b/internal/api/client/push/pushsubscriptionpost_test.go
index bdd22d729..e7e8582df 100644
--- a/internal/api/client/push/pushsubscriptionpost_test.go
+++ b/internal/api/client/push/pushsubscriptionpost_test.go
@@ -44,6 +44,7 @@ func (suite *PushTestSuite) postSubscription(
p256dh *string,
alertsMention *bool,
alertsStatus *bool,
+ policy *string,
requestJson *string,
expectedHTTPStatus int,
) (*apimodel.WebPushSubscription, error) {
@@ -80,6 +81,9 @@ func (suite *PushTestSuite) postSubscription(
if alertsStatus != nil {
ctx.Request.Form["data[alerts][status]"] = []string{strconv.FormatBool(*alertsStatus)}
}
+ if policy != nil {
+ ctx.Request.Form["data[policy]"] = []string{*policy}
+ }
}
// trigger the handler
@@ -119,6 +123,7 @@ func (suite *PushTestSuite) TestPostSubscription() {
p256dh := "BMYVItYVOX+AHBdtA62Q0i6c+F7MV2Gia3aoDr8mvHkuPBNIOuTLDfmFcnBqoZcQk6BtLcIONbxhHpy2R+mYIUY="
alertsMention := true
alertsStatus := false
+ policy := "followed"
subscription, err := suite.postSubscription(
accountFixtureName,
tokenFixtureName,
@@ -127,6 +132,7 @@ func (suite *PushTestSuite) TestPostSubscription() {
&p256dh,
&alertsMention,
&alertsStatus,
+ &policy,
nil,
200,
)
@@ -138,6 +144,7 @@ func (suite *PushTestSuite) TestPostSubscription() {
suite.False(subscription.Alerts.Status)
// Omitted event types should default to off.
suite.False(subscription.Alerts.Favourite)
+ suite.Equal(apimodel.WebPushNotificationPolicyFollowed, subscription.Policy)
}
}
@@ -159,6 +166,7 @@ func (suite *PushTestSuite) TestPostSubscriptionMinimal() {
nil,
nil,
nil,
+ nil,
200,
)
if suite.NoError(err) {
@@ -169,6 +177,8 @@ func (suite *PushTestSuite) TestPostSubscriptionMinimal() {
suite.False(subscription.Alerts.Mention)
suite.False(subscription.Alerts.Status)
suite.False(subscription.Alerts.Favourite)
+ // Policy should default to all.
+ suite.Equal(apimodel.WebPushNotificationPolicyAll, subscription.Policy)
}
}
@@ -192,6 +202,7 @@ func (suite *PushTestSuite) TestPostInvalidSubscription() {
&alertsMention,
&alertsStatus,
nil,
+ nil,
422,
)
suite.NoError(err)
@@ -215,7 +226,8 @@ func (suite *PushTestSuite) TestPostSubscriptionJSON() {
"alerts": {
"mention": true,
"status": false
- }
+ },
+ "policy": "followed"
}
}`
subscription, err := suite.postSubscription(
@@ -226,6 +238,7 @@ func (suite *PushTestSuite) TestPostSubscriptionJSON() {
nil,
nil,
nil,
+ nil,
&requestJson,
200,
)
@@ -237,6 +250,7 @@ func (suite *PushTestSuite) TestPostSubscriptionJSON() {
suite.False(subscription.Alerts.Status)
// Omitted event types should default to off.
suite.False(subscription.Alerts.Favourite)
+ suite.Equal(apimodel.WebPushNotificationPolicyFollowed, subscription.Policy)
}
}
@@ -263,6 +277,7 @@ func (suite *PushTestSuite) TestPostSubscriptionJSONMinimal() {
nil,
nil,
nil,
+ nil,
&requestJson,
200,
)
@@ -274,6 +289,8 @@ func (suite *PushTestSuite) TestPostSubscriptionJSONMinimal() {
suite.False(subscription.Alerts.Mention)
suite.False(subscription.Alerts.Status)
suite.False(subscription.Alerts.Favourite)
+ // Policy should default to all.
+ suite.Equal(apimodel.WebPushNotificationPolicyAll, subscription.Policy)
}
}
@@ -306,6 +323,7 @@ func (suite *PushTestSuite) TestPostInvalidSubscriptionJSON() {
nil,
nil,
nil,
+ nil,
&requestJson,
422,
)
@@ -323,6 +341,7 @@ func (suite *PushTestSuite) TestPostExistingSubscription() {
p256dh := "BMYVItYVOX+AHBdtA62Q0i6c+F7MV2Gia3aoDr8mvHkuPBNIOuTLDfmFcnBqoZcQk6BtLcIONbxhHpy2R+mYIUY="
alertsMention := true
alertsStatus := false
+ policy := "followed"
subscription, err := suite.postSubscription(
accountFixtureName,
tokenFixtureName,
@@ -331,6 +350,7 @@ func (suite *PushTestSuite) TestPostExistingSubscription() {
&p256dh,
&alertsMention,
&alertsStatus,
+ &policy,
nil,
200,
)
diff --git a/internal/api/client/push/pushsubscriptionput.go b/internal/api/client/push/pushsubscriptionput.go
index 06575f4ee..4d1c5765e 100644
--- a/internal/api/client/push/pushsubscriptionput.go
+++ b/internal/api/client/push/pushsubscriptionput.go
@@ -25,6 +25,7 @@ import (
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
+ "github.com/superseriousbusiness/gotosocial/internal/util"
)
// PushSubscriptionPUTHandler swagger:operation PUT /api/v1/push/subscription pushSubscriptionPut
@@ -122,6 +123,17 @@ import (
// type: boolean
// default: false
// description: Receive a push notification when a boost is pending?
+// -
+// name: data[policy]
+// in: formData
+// type: string
+// enum:
+// - all
+// - followed
+// - follower
+// - none
+// default: all
+// description: Which accounts to receive push notifications from.
//
// security:
// - OAuth2 Bearer:
@@ -181,7 +193,8 @@ func (m *Module) PushSubscriptionPUTHandler(c *gin.Context) {
apiutil.JSON(c, http.StatusOK, apiSubscription)
}
-// validateNormalizeUpdate copies form fields to their canonical JSON equivalents.
+// validateNormalizeUpdate copies form fields to their canonical JSON equivalents
+// and sets defaults for fields that have them.
func validateNormalizeUpdate(request *apimodel.WebPushSubscriptionUpdateRequest) error {
if request.Data == nil {
request.Data = &apimodel.WebPushSubscriptionRequestData{}
@@ -228,5 +241,12 @@ func validateNormalizeUpdate(request *apimodel.WebPushSubscriptionUpdateRequest)
request.Data.Alerts.Reblog = *request.DataAlertsPendingReblog
}
+ if request.DataPolicy != nil {
+ request.Data.Policy = request.DataPolicy
+ }
+ if request.Data.Policy == nil {
+ request.Data.Policy = util.Ptr(apimodel.WebPushNotificationPolicyAll)
+ }
+
return nil
}
diff --git a/internal/api/client/push/pushsubscriptionput_test.go b/internal/api/client/push/pushsubscriptionput_test.go
index 924e3d475..d9f0e395e 100644
--- a/internal/api/client/push/pushsubscriptionput_test.go
+++ b/internal/api/client/push/pushsubscriptionput_test.go
@@ -41,6 +41,7 @@ func (suite *PushTestSuite) putSubscription(
tokenFixtureName string,
alertsMention *bool,
alertsStatus *bool,
+ policy *string,
requestJson *string,
expectedHTTPStatus int,
) (*apimodel.WebPushSubscription, error) {
@@ -68,6 +69,9 @@ func (suite *PushTestSuite) putSubscription(
if alertsStatus != nil {
ctx.Request.Form["data[alerts][status]"] = []string{strconv.FormatBool(*alertsStatus)}
}
+ if policy != nil {
+ ctx.Request.Form["data[policy]"] = []string{*policy}
+ }
}
// trigger the handler
@@ -104,11 +108,13 @@ func (suite *PushTestSuite) TestPutSubscription() {
alertsMention := true
alertsStatus := false
+ policy := "followed"
subscription, err := suite.putSubscription(
accountFixtureName,
tokenFixtureName,
&alertsMention,
&alertsStatus,
+ &policy,
nil,
200,
)
@@ -120,6 +126,7 @@ func (suite *PushTestSuite) TestPutSubscription() {
suite.False(subscription.Alerts.Status)
// Omitted event types should default to off.
suite.False(subscription.Alerts.Favourite)
+ suite.Equal(apimodel.WebPushNotificationPolicyFollowed, subscription.Policy)
}
}
@@ -134,7 +141,8 @@ func (suite *PushTestSuite) TestPutSubscriptionJSON() {
"alerts": {
"mention": true,
"status": false
- }
+ },
+ "policy": "followed"
}
}`
subscription, err := suite.putSubscription(
@@ -142,6 +150,7 @@ func (suite *PushTestSuite) TestPutSubscriptionJSON() {
tokenFixtureName,
nil,
nil,
+ nil,
&requestJson,
200,
)
@@ -153,6 +162,7 @@ func (suite *PushTestSuite) TestPutSubscriptionJSON() {
suite.False(subscription.Alerts.Status)
// Omitted event types should default to off.
suite.False(subscription.Alerts.Favourite)
+ suite.Equal(apimodel.WebPushNotificationPolicyFollowed, subscription.Policy)
}
}
@@ -170,6 +180,7 @@ func (suite *PushTestSuite) TestPutMissingSubscription() {
&alertsMention,
&alertsStatus,
nil,
+ nil,
404,
)
suite.NoError(err)
diff --git a/internal/api/model/webpushsubscription.go b/internal/api/model/webpushsubscription.go
index a28bb7294..38d6cf7ed 100644
--- a/internal/api/model/webpushsubscription.go
+++ b/internal/api/model/webpushsubscription.go
@@ -138,6 +138,8 @@ type WebPushSubscriptionUpdateRequest struct {
DataAlertsPendingFavourite *bool `form:"data[alerts][pending.favourite]" json:"-"`
DataAlertsPendingReply *bool `form:"data[alerts][pending.reply]" json:"-"`
DataAlertsPendingReblog *bool `form:"data[alerts][pending.reblog]" json:"-"`
+
+ DataPolicy *WebPushNotificationPolicy `form:"data[policy]" json:"-"`
}
// WebPushSubscriptionRequestData is the part of a Web Push subscription that can be changed after creation.
@@ -146,6 +148,9 @@ type WebPushSubscriptionUpdateRequest struct {
type WebPushSubscriptionRequestData struct {
// Alerts selects the specific events that this Web Push subscription will receive.
Alerts *WebPushSubscriptionAlerts `form:"-" json:"alerts"`
+
+ // Policy selects which accounts will trigger Web Push notifications.
+ Policy *WebPushNotificationPolicy `form:"-" json:"policy"`
}
// WebPushNotificationPolicy names sets of accounts that can generate notifications.
@@ -154,4 +159,10 @@ type WebPushNotificationPolicy string
const (
// WebPushNotificationPolicyAll allows all accounts to send notifications to the subscribing user.
WebPushNotificationPolicyAll WebPushNotificationPolicy = "all"
+ // WebPushNotificationPolicyFollowed allows accounts followed by the subscribing user to send notifications.
+ WebPushNotificationPolicyFollowed WebPushNotificationPolicy = "followed"
+ // WebPushNotificationPolicyFollower allows accounts following the subscribing user to send notifications.
+ WebPushNotificationPolicyFollower WebPushNotificationPolicy = "follower"
+ // WebPushNotificationPolicyNone doesn't allow any acounts to send notifications to the subscribing user.
+ WebPushNotificationPolicyNone WebPushNotificationPolicy = "none"
)
diff --git a/internal/db/bundb/migrations/20250131184755_add_web_push_subscription_policy.go b/internal/db/bundb/migrations/20250131184755_add_web_push_subscription_policy.go
new file mode 100644
index 000000000..d833a669f
--- /dev/null
+++ b/internal/db/bundb/migrations/20250131184755_add_web_push_subscription_policy.go
@@ -0,0 +1,83 @@
+// 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 migrations
+
+import (
+ "context"
+ "reflect"
+ "strings"
+
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/uptrace/bun"
+ "github.com/uptrace/bun/dialect"
+)
+
+func init() {
+ up := func(ctx context.Context, db *bun.DB) error {
+ return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
+ model := &gtsmodel.WebPushSubscription{}
+
+ // Get the column definition for the new policy column.
+ modelType := reflect.TypeOf(model)
+ columnDef, err := getBunColumnDef(tx, modelType, "Policy")
+ if err != nil {
+ return err
+ }
+
+ // Add the policy column.
+ switch tx.Dialect().Name() {
+ case dialect.SQLite:
+ // Doesn't support Bun feature AlterColumnExists.
+ if _, err = tx.
+ NewAddColumn().
+ Model(model).
+ ColumnExpr(columnDef).
+ Exec(ctx); // nocollapse
+ err != nil && !strings.Contains(err.Error(), "duplicate column name") {
+ // Return errors that aren't about this column already existing.
+ return err
+ }
+
+ case dialect.PG:
+ // Supports Bun feature AlterColumnExists.
+ if _, err = tx.
+ NewAddColumn().
+ Model(model).
+ ColumnExpr(columnDef).
+ IfNotExists().
+ Exec(ctx); // nocollapse
+ err != nil {
+ return err
+ }
+
+ default:
+ panic("unsupported db type")
+ }
+
+ return nil
+ })
+ }
+
+ down := func(ctx context.Context, db *bun.DB) error {
+ return nil
+ }
+
+ if err := Migrations.Register(up, down); err != nil {
+ panic(err)
+ }
+}
diff --git a/internal/gtsmodel/webpushsubscription.go b/internal/gtsmodel/webpushsubscription.go
index 4aeef654a..1e310bc50 100644
--- a/internal/gtsmodel/webpushsubscription.go
+++ b/internal/gtsmodel/webpushsubscription.go
@@ -39,12 +39,15 @@ type WebPushSubscription struct {
// P256dh is a Base64-encoded Diffie-Hellman public key on the P-256 elliptic curve.
P256dh string `bun:",nullzero,notnull"`
- // NotificationFlags controls which notifications are delivered to a given subscription.
- // Corresponds to model.PushSubscriptionAlerts.
+ // NotificationFlags controls which notifications are delivered to this subscription.
NotificationFlags WebPushSubscriptionNotificationFlags `bun:",notnull"`
+
+ // Policy controls which accounts are allowed to trigger notifications for this subscription.
+ Policy WebPushNotificationPolicy `bun:",nullzero,notnull,default:1"`
}
// WebPushSubscriptionNotificationFlags is a bitfield representation of a set of NotificationType.
+// Corresponds to apimodel.WebPushSubscriptionAlerts.
type WebPushSubscriptionNotificationFlags int64
// WebPushSubscriptionNotificationFlagsFromSlice packs a slice of NotificationType into a WebPushSubscriptionNotificationFlags.
@@ -80,3 +83,18 @@ func (n *WebPushSubscriptionNotificationFlags) Set(notificationType Notification
*n &= ^(1 << notificationType)
}
}
+
+// WebPushNotificationPolicy represents the notification policy of a Web Push subscription.
+// Corresponds to apimodel.WebPushNotificationPolicy.
+type WebPushNotificationPolicy enumType
+
+const (
+ // WebPushNotificationPolicyAll allows all accounts to send notifications to the subscribing user.
+ WebPushNotificationPolicyAll WebPushNotificationPolicy = 1
+ // WebPushNotificationPolicyFollowed allows accounts followed by the subscribing user to send notifications.
+ WebPushNotificationPolicyFollowed WebPushNotificationPolicy = 2
+ // WebPushNotificationPolicyFollower allows accounts following the subscribing user to send notifications.
+ WebPushNotificationPolicyFollower WebPushNotificationPolicy = 3
+ // WebPushNotificationPolicyNone doesn't allow any accounts to send notifications to the subscribing user.
+ WebPushNotificationPolicyNone WebPushNotificationPolicy = 4
+)
diff --git a/internal/processing/push/create.go b/internal/processing/push/create.go
index 42a67dc19..dc15ccf12 100644
--- a/internal/processing/push/create.go
+++ b/internal/processing/push/create.go
@@ -24,6 +24,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id"
+ "github.com/superseriousbusiness/gotosocial/internal/typeutils"
)
// CreateOrReplace creates a Web Push subscription for the given access token,
@@ -54,6 +55,7 @@ func (p *Processor) CreateOrReplace(
Auth: request.Subscription.Keys.Auth,
P256dh: request.Subscription.Keys.P256dh,
NotificationFlags: alertsToNotificationFlags(request.Data.Alerts),
+ Policy: typeutils.APIWebPushNotificationPolicyToWebPushNotificationPolicy(*request.Data.Policy),
}
if err := p.state.DB.PutWebPushSubscription(ctx, subscription); err != nil {
diff --git a/internal/processing/push/update.go b/internal/processing/push/update.go
index 370536f9b..94529455a 100644
--- a/internal/processing/push/update.go
+++ b/internal/processing/push/update.go
@@ -24,6 +24,7 @@ import (
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/typeutils"
)
// Update updates the Web Push subscription for the given access token.
@@ -50,10 +51,13 @@ func (p *Processor) Update(
// Update it.
subscription.NotificationFlags = alertsToNotificationFlags(request.Data.Alerts)
+ subscription.Policy = typeutils.APIWebPushNotificationPolicyToWebPushNotificationPolicy(*request.Data.Policy)
+
if err = p.state.DB.UpdateWebPushSubscription(
ctx,
subscription,
"notification_flags",
+ "policy",
); err != nil {
err := gtserror.Newf("couldn't update Web Push subscription for token ID %s: %w", tokenID, err)
return nil, gtserror.NewErrorInternalError(err)
diff --git a/internal/typeutils/frontendtointernal.go b/internal/typeutils/frontendtointernal.go
index 82957ee05..b341aa6ae 100644
--- a/internal/typeutils/frontendtointernal.go
+++ b/internal/typeutils/frontendtointernal.go
@@ -231,3 +231,17 @@ func APIInteractionPolicyToInteractionPolicy(
},
}, nil
}
+
+func APIWebPushNotificationPolicyToWebPushNotificationPolicy(policy apimodel.WebPushNotificationPolicy) gtsmodel.WebPushNotificationPolicy {
+ switch policy {
+ case apimodel.WebPushNotificationPolicyAll:
+ return gtsmodel.WebPushNotificationPolicyAll
+ case apimodel.WebPushNotificationPolicyFollowed:
+ return gtsmodel.WebPushNotificationPolicyFollowed
+ case apimodel.WebPushNotificationPolicyFollower:
+ return gtsmodel.WebPushNotificationPolicyFollower
+ case apimodel.WebPushNotificationPolicyNone:
+ return gtsmodel.WebPushNotificationPolicyNone
+ }
+ return 0
+}
diff --git a/internal/typeutils/internaltoas_test.go b/internal/typeutils/internaltoas_test.go
index ce949d577..9eeaa3c0d 100644
--- a/internal/typeutils/internaltoas_test.go
+++ b/internal/typeutils/internaltoas_test.go
@@ -1235,9 +1235,9 @@ func (suite *InternalToASTestSuite) TestInteractionReqToASAcceptAnnounce() {
req := &gtsmodel.InteractionRequest{
ID: "01J1AKMZ8JE5NW0ZSFTRC1JJNE",
CreatedAt: testrig.TimeMustParse("2022-06-09T13:12:00Z"),
- StatusID: "01JJYCVKCXB9JTQD1XW2KB8MT3",
+ StatusID: "01JJYCVKCXB9JTQD1XW2KB8MT3",
Status: &gtsmodel.Status{URI: "http://localhost:8080/users/the_mighty_zork/statuses/01JJYCVKCXB9JTQD1XW2KB8MT3"},
- TargetAccountID: acceptingAccount.ID,
+ TargetAccountID: acceptingAccount.ID,
TargetAccount: acceptingAccount,
InteractingAccountID: interactingAccount.ID,
InteractingAccount: interactingAccount,
diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go
index 487e8434e..d966c054c 100644
--- a/internal/typeutils/internaltofrontend.go
+++ b/internal/typeutils/internaltofrontend.go
@@ -3019,6 +3019,20 @@ func (c *Converter) InteractionReqToAPIInteractionReq(
}, nil
}
+func webPushNotificationPolicyToAPIWebPushNotificationPolicy(policy gtsmodel.WebPushNotificationPolicy) apimodel.WebPushNotificationPolicy {
+ switch policy {
+ case gtsmodel.WebPushNotificationPolicyAll:
+ return apimodel.WebPushNotificationPolicyAll
+ case gtsmodel.WebPushNotificationPolicyFollowed:
+ return apimodel.WebPushNotificationPolicyFollowed
+ case gtsmodel.WebPushNotificationPolicyFollower:
+ return apimodel.WebPushNotificationPolicyFollower
+ case gtsmodel.WebPushNotificationPolicyNone:
+ return apimodel.WebPushNotificationPolicyNone
+ }
+ return ""
+}
+
func (c *Converter) WebPushSubscriptionToAPIWebPushSubscription(
ctx context.Context,
subscription *gtsmodel.WebPushSubscription,
@@ -3047,7 +3061,7 @@ func (c *Converter) WebPushSubscriptionToAPIWebPushSubscription(
PendingReply: subscription.NotificationFlags.Get(gtsmodel.NotificationPendingReply),
PendingReblog: subscription.NotificationFlags.Get(gtsmodel.NotificationPendingReblog),
},
- Policy: apimodel.WebPushNotificationPolicyAll,
+ Policy: webPushNotificationPolicyToAPIWebPushNotificationPolicy(subscription.Policy),
Standard: true,
}, nil
}
diff --git a/internal/webpush/realsender.go b/internal/webpush/realsender.go
index 4faf57fb2..919cea788 100644
--- a/internal/webpush/realsender.go
+++ b/internal/webpush/realsender.go
@@ -67,8 +67,7 @@ func (r *realSender) Send(
relevantSubscriptions := slices.DeleteFunc(
subscriptions,
func(subscription *gtsmodel.WebPushSubscription) bool {
- // Remove subscriptions that don't want this type of notification.
- return !subscription.NotificationFlags.Get(notification.NotificationType)
+ return r.shouldSkipSubscription(ctx, notification, subscription)
},
)
if len(relevantSubscriptions) == 0 {
@@ -117,6 +116,68 @@ func (r *realSender) Send(
return nil
}
+// shouldSkipSubscription returns true if this subscription is not relevant to this notification.
+func (r *realSender) shouldSkipSubscription(
+ ctx context.Context,
+ notification *gtsmodel.Notification,
+ subscription *gtsmodel.WebPushSubscription,
+) bool {
+ // Remove subscriptions that don't want this type of notification.
+ if !subscription.NotificationFlags.Get(notification.NotificationType) {
+ return true
+ }
+
+ // Check against subscription's notification policy.
+ switch subscription.Policy {
+ case gtsmodel.WebPushNotificationPolicyAll:
+ // Allow notifications from any account.
+ return false
+
+ case gtsmodel.WebPushNotificationPolicyFollowed:
+ // Allow if the subscription account follows the notifying account.
+ isFollowing, err := r.state.DB.IsFollowing(ctx, subscription.AccountID, notification.OriginAccountID)
+ if err != nil {
+ log.Errorf(
+ ctx,
+ "error checking whether account %s follows account %s: %v",
+ subscription.AccountID,
+ notification.OriginAccountID,
+ err,
+ )
+ return true
+ }
+ return !isFollowing
+
+ case gtsmodel.WebPushNotificationPolicyFollower:
+ // Allow if the notifying account follows the subscription account.
+ isFollowing, err := r.state.DB.IsFollowing(ctx, notification.OriginAccountID, subscription.AccountID)
+ if err != nil {
+ log.Errorf(
+ ctx,
+ "error checking whether account %s follows account %s: %v",
+ notification.OriginAccountID,
+ subscription.AccountID,
+ err,
+ )
+ return true
+ }
+ return !isFollowing
+
+ case gtsmodel.WebPushNotificationPolicyNone:
+ // This subscription doesn't want any push notifications.
+ return true
+
+ default:
+ log.Errorf(
+ ctx,
+ "unknown Web Push notification policy for subscription with token ID %s: %d",
+ subscription.TokenID,
+ subscription.Policy,
+ )
+ return true
+ }
+}
+
// sendToSubscription sends a notification to a single Web Push subscription.
func (r *realSender) sendToSubscription(
ctx context.Context,
diff --git a/internal/webpush/realsender_test.go b/internal/webpush/realsender_test.go
index 8446fc47d..d5172c00e 100644
--- a/internal/webpush/realsender_test.go
+++ b/internal/webpush/realsender_test.go
@@ -23,7 +23,6 @@ import (
"net/http"
"testing"
"time"
-
// for go:linkname
_ "unsafe"
@@ -43,6 +42,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/subscriptions"
"github.com/superseriousbusiness/gotosocial/internal/transport"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
+ "github.com/superseriousbusiness/gotosocial/internal/util"
"github.com/superseriousbusiness/gotosocial/internal/webpush"
"github.com/superseriousbusiness/gotosocial/testrig"
)
@@ -62,16 +62,7 @@ type RealSenderStandardTestSuite struct {
webPushSender webpush.Sender
// standard suite models
- testTokens map[string]*gtsmodel.Token
- testClients map[string]*gtsmodel.Client
- testApplications map[string]*gtsmodel.Application
- testUsers map[string]*gtsmodel.User
testAccounts map[string]*gtsmodel.Account
- testAttachments map[string]*gtsmodel.MediaAttachment
- testStatuses map[string]*gtsmodel.Status
- testTags map[string]*gtsmodel.Tag
- testMentions map[string]*gtsmodel.Mention
- testEmojis map[string]*gtsmodel.Emoji
testNotifications map[string]*gtsmodel.Notification
testWebPushSubscriptions map[string]*gtsmodel.WebPushSubscription
@@ -81,16 +72,7 @@ type RealSenderStandardTestSuite struct {
}
func (suite *RealSenderStandardTestSuite) SetupSuite() {
- suite.testTokens = testrig.NewTestTokens()
- suite.testClients = testrig.NewTestClients()
- suite.testApplications = testrig.NewTestApplications()
- suite.testUsers = testrig.NewTestUsers()
suite.testAccounts = testrig.NewTestAccounts()
- suite.testAttachments = testrig.NewTestAttachments()
- suite.testStatuses = testrig.NewTestStatuses()
- suite.testTags = testrig.NewTestTags()
- suite.testMentions = testrig.NewTestMentions()
- suite.testEmojis = testrig.NewTestEmojis()
suite.testNotifications = testrig.NewTestNotifications()
suite.testWebPushSubscriptions = testrig.NewTestWebPushSubscriptions()
}
@@ -184,14 +166,16 @@ func (rc *notifyingReadCloser) Close() error {
// Simulate sending a push notification with the suite's fake web client.
func (suite *RealSenderStandardTestSuite) simulatePushNotification(
+ notificationID string,
statusCode int,
+ expectSend bool,
expectDeletedSubscription bool,
) error {
// Don't let the test run forever if the push notification was not sent for some reason.
- ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
- notification, err := suite.state.DB.GetNotificationByID(ctx, suite.testNotifications["local_account_1_like"].ID)
+ notification, err := suite.state.DB.GetNotificationByID(ctx, notificationID)
if !suite.NoError(err) {
suite.FailNow("Couldn't fetch notification to send")
}
@@ -221,6 +205,14 @@ func (suite *RealSenderStandardTestSuite) simulatePushNotification(
case <-ctx.Done():
contextExpired = true
}
+
+ // In some cases we expect the notification *not* to be sent.
+ if !expectSend {
+ suite.False(bodyClosed)
+ suite.True(contextExpired)
+ return nil
+ }
+
suite.True(bodyClosed)
suite.False(contextExpired)
@@ -240,25 +232,48 @@ func (suite *RealSenderStandardTestSuite) simulatePushNotification(
// Test a successful response to sending a push notification.
func (suite *RealSenderStandardTestSuite) TestSendSuccess() {
- suite.NoError(suite.simulatePushNotification(http.StatusOK, false))
+ notificationID := suite.testNotifications["local_account_1_like"].ID
+ suite.NoError(suite.simulatePushNotification(notificationID, http.StatusOK, true, false))
}
// Test a rate-limiting response to sending a push notification.
// This should not delete the subscription.
func (suite *RealSenderStandardTestSuite) TestRateLimited() {
- suite.NoError(suite.simulatePushNotification(http.StatusTooManyRequests, false))
+ notificationID := suite.testNotifications["local_account_1_like"].ID
+ suite.NoError(suite.simulatePushNotification(notificationID, http.StatusTooManyRequests, true, false))
}
// Test a non-special-cased client error response to sending a push notification.
// This should delete the subscription.
func (suite *RealSenderStandardTestSuite) TestClientError() {
- suite.NoError(suite.simulatePushNotification(http.StatusBadRequest, true))
+ notificationID := suite.testNotifications["local_account_1_like"].ID
+ suite.NoError(suite.simulatePushNotification(notificationID, http.StatusBadRequest, true, true))
}
// Test a server error response to sending a push notification.
// This should not delete the subscription.
func (suite *RealSenderStandardTestSuite) TestServerError() {
- suite.NoError(suite.simulatePushNotification(http.StatusInternalServerError, false))
+ notificationID := suite.testNotifications["local_account_1_like"].ID
+ suite.NoError(suite.simulatePushNotification(notificationID, http.StatusInternalServerError, true, false))
+}
+
+// Don't send a push notification if it doesn't match policy.
+func (suite *RealSenderStandardTestSuite) TestSendPolicyMismatch() {
+ // Setup: create a new notification from an account that the subscribed account doesn't follow.
+ notification := &gtsmodel.Notification{
+ ID: "01JJZ2Y9Z8E1XKT90EHZ5KZBDW",
+ NotificationType: gtsmodel.NotificationFavourite,
+ TargetAccountID: suite.testAccounts["local_account_1"].ID,
+ OriginAccountID: suite.testAccounts["remote_account_1"].ID,
+ StatusID: "01F8MHAMCHF6Y650WCRSCP4WMY",
+ Read: util.Ptr(false),
+ }
+ if err := suite.db.PutNotification(context.Background(), notification); !suite.NoError(err) {
+ suite.FailNow(err.Error())
+ return
+ }
+
+ suite.NoError(suite.simulatePushNotification(notification.ID, 0, false, false))
}
func TestRealSenderStandardTestSuite(t *testing.T) {
diff --git a/testrig/testmodels.go b/testrig/testmodels.go
index 9a54aba70..806e64891 100644
--- a/testrig/testmodels.go
+++ b/testrig/testmodels.go
@@ -3610,6 +3610,7 @@ func NewTestWebPushSubscriptions() map[string]*gtsmodel.WebPushSubscription {
gtsmodel.NotificationPendingReply,
gtsmodel.NotificationPendingReblog,
}),
+ Policy: gtsmodel.WebPushNotificationPolicyFollowed,
},
}
}