@@ -24,9 +24,12 @@ import (
24
24
// Dispatcher is an interface that can be used to dispatch
25
25
// push notifications over Web Push.
26
26
type Dispatcher interface {
27
+ // Dispatch sends a notification to all subscriptions for a user. Any
28
+ // notifications that fail to send are silently dropped.
27
29
Dispatch (ctx context.Context , userID uuid.UUID , notification codersdk.WebpushMessage ) error
30
+ // Test sends a test notification to a subscription to ensure it is valid.
31
+ Test (ctx context.Context , req codersdk.WebpushSubscription ) error
28
32
PublicKey () string
29
- PrivateKey () string
30
33
}
31
34
32
35
// New creates a new Dispatcher to dispatch notifications via Web Push.
@@ -93,24 +96,15 @@ func (n *Webpusher) Dispatch(ctx context.Context, userID uuid.UUID, notification
93
96
for _ , subscription := range subscriptions {
94
97
subscription := subscription
95
98
eg .Go (func () error {
96
- n .log .Debug (ctx , "dispatching via push" , slog .F ("subscription" , subscription .Endpoint ))
97
- cpy := slices .Clone (notificationJSON ) // Need to copy as webpush.SendNotificationWithContext modifies the slice.
98
- resp , err := webpush .SendNotificationWithContext (ctx , cpy , & webpush.Subscription {
99
- Endpoint : subscription .Endpoint ,
100
- Keys : webpush.Keys {
101
- Auth : subscription .EndpointAuthKey ,
102
- P256dh : subscription .EndpointP256dhKey ,
103
- },
104
- }, & webpush.Options {
105
- VAPIDPublicKey : n .VAPIDPublicKey ,
106
- VAPIDPrivateKey : n .VAPIDPrivateKey ,
99
+ statusCode , body , err := n .webpushSend (ctx , notificationJSON , subscription .Endpoint , webpush.Keys {
100
+ Auth : subscription .EndpointAuthKey ,
101
+ P256dh : subscription .EndpointP256dhKey ,
107
102
})
108
103
if err != nil {
109
104
return xerrors .Errorf ("send notification: %w" , err )
110
105
}
111
- defer resp .Body .Close ()
112
106
113
- if resp . StatusCode == http .StatusGone {
107
+ if statusCode == http .StatusGone {
114
108
// The subscription is no longer valid, remove it.
115
109
mu .Lock ()
116
110
cleanupSubscriptions = append (cleanupSubscriptions , subscription .ID )
@@ -119,10 +113,9 @@ func (n *Webpusher) Dispatch(ctx context.Context, userID uuid.UUID, notification
119
113
}
120
114
121
115
// 200, 201, and 202 are common for successful delivery.
122
- if resp . StatusCode > http .StatusAccepted {
116
+ if statusCode > http .StatusAccepted {
123
117
// It's likely the subscription failed to deliver for some reason.
124
- body , _ := io .ReadAll (resp .Body )
125
- return xerrors .Errorf ("web push dispatch failed with status code %d: %s" , resp .StatusCode , string (body ))
118
+ return xerrors .Errorf ("web push dispatch failed with status code %d: %s" , statusCode , string (body ))
126
119
}
127
120
128
121
return nil
@@ -145,12 +138,55 @@ func (n *Webpusher) Dispatch(ctx context.Context, userID uuid.UUID, notification
145
138
return nil
146
139
}
147
140
148
- func (n * Webpusher ) PublicKey () string {
149
- return n .VAPIDPublicKey
141
+ func (n * Webpusher ) webpushSend (ctx context.Context , msg []byte , endpoint string , keys webpush.Keys ) (int , []byte , error ) {
142
+ // Copy the message to avoid modifying the original.
143
+ cpy := slices .Clone (msg )
144
+ resp , err := webpush .SendNotificationWithContext (ctx , cpy , & webpush.Subscription {
145
+ Endpoint : endpoint ,
146
+ Keys : keys ,
147
+ }, & webpush.Options {
148
+ VAPIDPublicKey : n .VAPIDPublicKey ,
149
+ VAPIDPrivateKey : n .VAPIDPrivateKey ,
150
+ })
151
+ if err != nil {
152
+ return - 1 , nil , xerrors .Errorf ("send notification: %w" , err )
153
+ }
154
+ defer resp .Body .Close ()
155
+ body , err := io .ReadAll (resp .Body )
156
+ if err != nil {
157
+ return - 1 , nil , xerrors .Errorf ("read response body: %w" , err )
158
+ }
159
+
160
+ return resp .StatusCode , body , nil
161
+ }
162
+
163
+ func (n * Webpusher ) Test (ctx context.Context , req codersdk.WebpushSubscription ) error {
164
+ notificationJSON , err := json .Marshal (codersdk.WebpushMessage {
165
+ Title : "Test" ,
166
+ Body : "This is a test notification" ,
167
+ })
168
+ if err != nil {
169
+ return xerrors .Errorf ("marshal notification: %w" , err )
170
+ }
171
+ statusCode , body , err := n .webpushSend (ctx , notificationJSON , req .Endpoint , webpush.Keys {
172
+ Auth : req .AuthKey ,
173
+ P256dh : req .P256DHKey ,
174
+ })
175
+ if err != nil {
176
+ return xerrors .Errorf ("send test notification: %w" , err )
177
+ }
178
+
179
+ // 200, 201, and 202 are common for successful delivery.
180
+ if statusCode > http .StatusAccepted {
181
+ // It's likely the subscription failed to deliver for some reason.
182
+ return xerrors .Errorf ("web push dispatch failed with status code %d: %s" , statusCode , string (body ))
183
+ }
184
+
185
+ return nil
150
186
}
151
187
152
- func (n * Webpusher ) PrivateKey () string {
153
- return n .VAPIDPrivateKey
188
+ func (n * Webpusher ) PublicKey () string {
189
+ return n .VAPIDPublicKey
154
190
}
155
191
156
192
// NoopWebpusher is a Dispatcher that does nothing except return an error.
@@ -164,11 +200,11 @@ func (n *NoopWebpusher) Dispatch(context.Context, uuid.UUID, codersdk.WebpushMes
164
200
return xerrors .New (n .Msg )
165
201
}
166
202
167
- func (* NoopWebpusher ) PublicKey () string {
168
- return ""
203
+ func (n * NoopWebpusher ) Test (context. Context , codersdk. WebpushSubscription ) error {
204
+ return xerrors . New ( n . Msg )
169
205
}
170
206
171
- func (* NoopWebpusher ) PrivateKey () string {
207
+ func (* NoopWebpusher ) PublicKey () string {
172
208
return ""
173
209
}
174
210
0 commit comments