Web Push: add policy to API

This commit is contained in:
Vyr Cossont 2025-01-31 12:34:20 -08:00
commit c0e53662cf
10 changed files with 121 additions and 4 deletions

View file

@ -9922,6 +9922,16 @@ paths:
in: formData in: formData
name: data[alerts][pending.reblog] name: data[alerts][pending.reblog]
type: boolean 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: produces:
- application/json - application/json
responses: responses:
@ -10019,6 +10029,16 @@ paths:
in: formData in: formData
name: data[alerts][pending.reblog] name: data[alerts][pending.reblog]
type: boolean 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: produces:
- application/json - application/json
responses: responses:

View file

@ -147,6 +147,17 @@ import (
// type: boolean // type: boolean
// default: false // default: false
// description: Receive a push notification when a boost is pending? // 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: // security:
// - OAuth2 Bearer: // - OAuth2 Bearer:

View file

@ -44,6 +44,7 @@ func (suite *PushTestSuite) postSubscription(
p256dh *string, p256dh *string,
alertsMention *bool, alertsMention *bool,
alertsStatus *bool, alertsStatus *bool,
policy *string,
requestJson *string, requestJson *string,
expectedHTTPStatus int, expectedHTTPStatus int,
) (*apimodel.WebPushSubscription, error) { ) (*apimodel.WebPushSubscription, error) {
@ -80,6 +81,9 @@ func (suite *PushTestSuite) postSubscription(
if alertsStatus != nil { if alertsStatus != nil {
ctx.Request.Form["data[alerts][status]"] = []string{strconv.FormatBool(*alertsStatus)} ctx.Request.Form["data[alerts][status]"] = []string{strconv.FormatBool(*alertsStatus)}
} }
if policy != nil {
ctx.Request.Form["data[policy]"] = []string{*policy}
}
} }
// trigger the handler // trigger the handler
@ -119,6 +123,7 @@ func (suite *PushTestSuite) TestPostSubscription() {
p256dh := "BMYVItYVOX+AHBdtA62Q0i6c+F7MV2Gia3aoDr8mvHkuPBNIOuTLDfmFcnBqoZcQk6BtLcIONbxhHpy2R+mYIUY=" p256dh := "BMYVItYVOX+AHBdtA62Q0i6c+F7MV2Gia3aoDr8mvHkuPBNIOuTLDfmFcnBqoZcQk6BtLcIONbxhHpy2R+mYIUY="
alertsMention := true alertsMention := true
alertsStatus := false alertsStatus := false
policy := "followed"
subscription, err := suite.postSubscription( subscription, err := suite.postSubscription(
accountFixtureName, accountFixtureName,
tokenFixtureName, tokenFixtureName,
@ -127,6 +132,7 @@ func (suite *PushTestSuite) TestPostSubscription() {
&p256dh, &p256dh,
&alertsMention, &alertsMention,
&alertsStatus, &alertsStatus,
&policy,
nil, nil,
200, 200,
) )
@ -138,6 +144,7 @@ func (suite *PushTestSuite) TestPostSubscription() {
suite.False(subscription.Alerts.Status) suite.False(subscription.Alerts.Status)
// Omitted event types should default to off. // Omitted event types should default to off.
suite.False(subscription.Alerts.Favourite) suite.False(subscription.Alerts.Favourite)
suite.Equal(apimodel.WebPushNotificationPolicyFollowed, subscription.Policy)
} }
} }
@ -159,6 +166,7 @@ func (suite *PushTestSuite) TestPostSubscriptionMinimal() {
nil, nil,
nil, nil,
nil, nil,
nil,
200, 200,
) )
if suite.NoError(err) { if suite.NoError(err) {
@ -169,6 +177,8 @@ func (suite *PushTestSuite) TestPostSubscriptionMinimal() {
suite.False(subscription.Alerts.Mention) suite.False(subscription.Alerts.Mention)
suite.False(subscription.Alerts.Status) suite.False(subscription.Alerts.Status)
suite.False(subscription.Alerts.Favourite) 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, &alertsMention,
&alertsStatus, &alertsStatus,
nil, nil,
nil,
422, 422,
) )
suite.NoError(err) suite.NoError(err)
@ -215,7 +226,8 @@ func (suite *PushTestSuite) TestPostSubscriptionJSON() {
"alerts": { "alerts": {
"mention": true, "mention": true,
"status": false "status": false
} },
"policy": "followed"
} }
}` }`
subscription, err := suite.postSubscription( subscription, err := suite.postSubscription(
@ -226,6 +238,7 @@ func (suite *PushTestSuite) TestPostSubscriptionJSON() {
nil, nil,
nil, nil,
nil, nil,
nil,
&requestJson, &requestJson,
200, 200,
) )
@ -237,6 +250,7 @@ func (suite *PushTestSuite) TestPostSubscriptionJSON() {
suite.False(subscription.Alerts.Status) suite.False(subscription.Alerts.Status)
// Omitted event types should default to off. // Omitted event types should default to off.
suite.False(subscription.Alerts.Favourite) suite.False(subscription.Alerts.Favourite)
suite.Equal(apimodel.WebPushNotificationPolicyFollowed, subscription.Policy)
} }
} }
@ -263,6 +277,7 @@ func (suite *PushTestSuite) TestPostSubscriptionJSONMinimal() {
nil, nil,
nil, nil,
nil, nil,
nil,
&requestJson, &requestJson,
200, 200,
) )
@ -274,6 +289,8 @@ func (suite *PushTestSuite) TestPostSubscriptionJSONMinimal() {
suite.False(subscription.Alerts.Mention) suite.False(subscription.Alerts.Mention)
suite.False(subscription.Alerts.Status) suite.False(subscription.Alerts.Status)
suite.False(subscription.Alerts.Favourite) 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,
nil, nil,
nil,
&requestJson, &requestJson,
422, 422,
) )
@ -323,6 +341,7 @@ func (suite *PushTestSuite) TestPostExistingSubscription() {
p256dh := "BMYVItYVOX+AHBdtA62Q0i6c+F7MV2Gia3aoDr8mvHkuPBNIOuTLDfmFcnBqoZcQk6BtLcIONbxhHpy2R+mYIUY=" p256dh := "BMYVItYVOX+AHBdtA62Q0i6c+F7MV2Gia3aoDr8mvHkuPBNIOuTLDfmFcnBqoZcQk6BtLcIONbxhHpy2R+mYIUY="
alertsMention := true alertsMention := true
alertsStatus := false alertsStatus := false
policy := "followed"
subscription, err := suite.postSubscription( subscription, err := suite.postSubscription(
accountFixtureName, accountFixtureName,
tokenFixtureName, tokenFixtureName,
@ -331,6 +350,7 @@ func (suite *PushTestSuite) TestPostExistingSubscription() {
&p256dh, &p256dh,
&alertsMention, &alertsMention,
&alertsStatus, &alertsStatus,
&policy,
nil, nil,
200, 200,
) )

View file

@ -25,6 +25,7 @@ import (
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
"github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/util"
) )
// PushSubscriptionPUTHandler swagger:operation PUT /api/v1/push/subscription pushSubscriptionPut // PushSubscriptionPUTHandler swagger:operation PUT /api/v1/push/subscription pushSubscriptionPut
@ -122,6 +123,17 @@ import (
// type: boolean // type: boolean
// default: false // default: false
// description: Receive a push notification when a boost is pending? // 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: // security:
// - OAuth2 Bearer: // - OAuth2 Bearer:
@ -181,7 +193,8 @@ func (m *Module) PushSubscriptionPUTHandler(c *gin.Context) {
apiutil.JSON(c, http.StatusOK, apiSubscription) 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 { func validateNormalizeUpdate(request *apimodel.WebPushSubscriptionUpdateRequest) error {
if request.Data == nil { if request.Data == nil {
request.Data = &apimodel.WebPushSubscriptionRequestData{} request.Data = &apimodel.WebPushSubscriptionRequestData{}
@ -228,5 +241,12 @@ func validateNormalizeUpdate(request *apimodel.WebPushSubscriptionUpdateRequest)
request.Data.Alerts.Reblog = *request.DataAlertsPendingReblog 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 return nil
} }

View file

@ -41,6 +41,7 @@ func (suite *PushTestSuite) putSubscription(
tokenFixtureName string, tokenFixtureName string,
alertsMention *bool, alertsMention *bool,
alertsStatus *bool, alertsStatus *bool,
policy *string,
requestJson *string, requestJson *string,
expectedHTTPStatus int, expectedHTTPStatus int,
) (*apimodel.WebPushSubscription, error) { ) (*apimodel.WebPushSubscription, error) {
@ -68,6 +69,9 @@ func (suite *PushTestSuite) putSubscription(
if alertsStatus != nil { if alertsStatus != nil {
ctx.Request.Form["data[alerts][status]"] = []string{strconv.FormatBool(*alertsStatus)} ctx.Request.Form["data[alerts][status]"] = []string{strconv.FormatBool(*alertsStatus)}
} }
if policy != nil {
ctx.Request.Form["data[policy]"] = []string{*policy}
}
} }
// trigger the handler // trigger the handler
@ -104,11 +108,13 @@ func (suite *PushTestSuite) TestPutSubscription() {
alertsMention := true alertsMention := true
alertsStatus := false alertsStatus := false
policy := "followed"
subscription, err := suite.putSubscription( subscription, err := suite.putSubscription(
accountFixtureName, accountFixtureName,
tokenFixtureName, tokenFixtureName,
&alertsMention, &alertsMention,
&alertsStatus, &alertsStatus,
&policy,
nil, nil,
200, 200,
) )
@ -120,6 +126,7 @@ func (suite *PushTestSuite) TestPutSubscription() {
suite.False(subscription.Alerts.Status) suite.False(subscription.Alerts.Status)
// Omitted event types should default to off. // Omitted event types should default to off.
suite.False(subscription.Alerts.Favourite) suite.False(subscription.Alerts.Favourite)
suite.Equal(apimodel.WebPushNotificationPolicyFollowed, subscription.Policy)
} }
} }
@ -134,7 +141,8 @@ func (suite *PushTestSuite) TestPutSubscriptionJSON() {
"alerts": { "alerts": {
"mention": true, "mention": true,
"status": false "status": false
} },
"policy": "followed"
} }
}` }`
subscription, err := suite.putSubscription( subscription, err := suite.putSubscription(
@ -142,6 +150,7 @@ func (suite *PushTestSuite) TestPutSubscriptionJSON() {
tokenFixtureName, tokenFixtureName,
nil, nil,
nil, nil,
nil,
&requestJson, &requestJson,
200, 200,
) )
@ -153,6 +162,7 @@ func (suite *PushTestSuite) TestPutSubscriptionJSON() {
suite.False(subscription.Alerts.Status) suite.False(subscription.Alerts.Status)
// Omitted event types should default to off. // Omitted event types should default to off.
suite.False(subscription.Alerts.Favourite) suite.False(subscription.Alerts.Favourite)
suite.Equal(apimodel.WebPushNotificationPolicyFollowed, subscription.Policy)
} }
} }
@ -170,6 +180,7 @@ func (suite *PushTestSuite) TestPutMissingSubscription() {
&alertsMention, &alertsMention,
&alertsStatus, &alertsStatus,
nil, nil,
nil,
404, 404,
) )
suite.NoError(err) suite.NoError(err)

View file

@ -24,6 +24,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id" "github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
) )
// CreateOrReplace creates a Web Push subscription for the given access token, // CreateOrReplace creates a Web Push subscription for the given access token,
@ -54,6 +55,7 @@ func (p *Processor) CreateOrReplace(
Auth: request.Subscription.Keys.Auth, Auth: request.Subscription.Keys.Auth,
P256dh: request.Subscription.Keys.P256dh, P256dh: request.Subscription.Keys.P256dh,
NotificationFlags: alertsToNotificationFlags(request.Data.Alerts), NotificationFlags: alertsToNotificationFlags(request.Data.Alerts),
Policy: typeutils.APIWebPushNotificationPolicyToWebPushNotificationPolicy(*request.Data.Policy),
} }
if err := p.state.DB.PutWebPushSubscription(ctx, subscription); err != nil { if err := p.state.DB.PutWebPushSubscription(ctx, subscription); err != nil {

View file

@ -24,6 +24,7 @@ import (
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
) )
// Update updates the Web Push subscription for the given access token. // Update updates the Web Push subscription for the given access token.
@ -50,10 +51,13 @@ func (p *Processor) Update(
// Update it. // Update it.
subscription.NotificationFlags = alertsToNotificationFlags(request.Data.Alerts) subscription.NotificationFlags = alertsToNotificationFlags(request.Data.Alerts)
subscription.Policy = typeutils.APIWebPushNotificationPolicyToWebPushNotificationPolicy(*request.Data.Policy)
if err = p.state.DB.UpdateWebPushSubscription( if err = p.state.DB.UpdateWebPushSubscription(
ctx, ctx,
subscription, subscription,
"notification_flags", "notification_flags",
"policy",
); err != nil { ); err != nil {
err := gtserror.Newf("couldn't update Web Push subscription for token ID %s: %w", tokenID, err) err := gtserror.Newf("couldn't update Web Push subscription for token ID %s: %w", tokenID, err)
return nil, gtserror.NewErrorInternalError(err) return nil, gtserror.NewErrorInternalError(err)

View file

@ -231,3 +231,17 @@ func APIInteractionPolicyToInteractionPolicy(
}, },
}, nil }, 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
}

View file

@ -3019,6 +3019,20 @@ func (c *Converter) InteractionReqToAPIInteractionReq(
}, nil }, 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( func (c *Converter) WebPushSubscriptionToAPIWebPushSubscription(
ctx context.Context, ctx context.Context,
subscription *gtsmodel.WebPushSubscription, subscription *gtsmodel.WebPushSubscription,
@ -3047,7 +3061,7 @@ func (c *Converter) WebPushSubscriptionToAPIWebPushSubscription(
PendingReply: subscription.NotificationFlags.Get(gtsmodel.NotificationPendingReply), PendingReply: subscription.NotificationFlags.Get(gtsmodel.NotificationPendingReply),
PendingReblog: subscription.NotificationFlags.Get(gtsmodel.NotificationPendingReblog), PendingReblog: subscription.NotificationFlags.Get(gtsmodel.NotificationPendingReblog),
}, },
Policy: apimodel.WebPushNotificationPolicyAll, Policy: webPushNotificationPolicyToAPIWebPushNotificationPolicy(subscription.Policy),
Standard: true, Standard: true,
}, nil }, nil
} }

View file

@ -3610,6 +3610,7 @@ func NewTestWebPushSubscriptions() map[string]*gtsmodel.WebPushSubscription {
gtsmodel.NotificationPendingReply, gtsmodel.NotificationPendingReply,
gtsmodel.NotificationPendingReblog, gtsmodel.NotificationPendingReblog,
}), }),
Policy: gtsmodel.WebPushNotificationPolicyAll,
}, },
} }
} }