181 lines
4.8 KiB
Go
181 lines
4.8 KiB
Go
|
// Package webpush provides Web Push Notification functionality.
|
||
|
package webpush
|
||
|
|
||
|
import (
|
||
|
"encoding/json"
|
||
|
"fmt"
|
||
|
"net/http"
|
||
|
|
||
|
"code.nonshy.com/nonshy/website/pkg/config"
|
||
|
"code.nonshy.com/nonshy/website/pkg/controller/api"
|
||
|
"code.nonshy.com/nonshy/website/pkg/log"
|
||
|
"code.nonshy.com/nonshy/website/pkg/models"
|
||
|
"code.nonshy.com/nonshy/website/pkg/session"
|
||
|
"code.nonshy.com/nonshy/website/pkg/templates"
|
||
|
webpush "github.com/SherClockHolmes/webpush-go"
|
||
|
)
|
||
|
|
||
|
// VAPIDPublicKey returns the site's public key as an endpoint.
|
||
|
func VAPIDPublicKey(w http.ResponseWriter, r *http.Request) {
|
||
|
w.WriteHeader(http.StatusOK)
|
||
|
w.Write([]byte(config.Current.WebPush.VAPIDPublicKey))
|
||
|
}
|
||
|
|
||
|
// UnregisterAll resets a user's stored push notification subscriptions.
|
||
|
func UnregisterAll() http.HandlerFunc {
|
||
|
var next = "/settings#notifications"
|
||
|
|
||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||
|
currentUser, err := session.CurrentUser(r)
|
||
|
if err != nil {
|
||
|
session.FlashError(w, r, "You must be logged in to do that!")
|
||
|
templates.Redirect(w, next)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
if err := models.DeletePushNotificationSubscriptions(currentUser); err != nil {
|
||
|
session.FlashError(w, r, "Error removing your subscriptions: %s", err)
|
||
|
} else {
|
||
|
session.Flash(w, r, "Your push notification subscriptions have been reset!")
|
||
|
}
|
||
|
templates.Redirect(w, next)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Register endpoint for push notification.
|
||
|
func Register() http.HandlerFunc {
|
||
|
type Request struct {
|
||
|
Endpoint string `json:"endpoint"`
|
||
|
ExpirationTime float64 `json:"expirationTime"`
|
||
|
Keys struct {
|
||
|
Auth string `json:"auth"`
|
||
|
P256DH string `json:"p256dh"`
|
||
|
} `json:"keys"`
|
||
|
}
|
||
|
|
||
|
type Response struct {
|
||
|
OK bool `json:"OK"`
|
||
|
Error string `json:"error,omitempty"`
|
||
|
}
|
||
|
|
||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||
|
currentUser, err := session.CurrentUser(r)
|
||
|
if err != nil {
|
||
|
api.SendJSON(w, http.StatusUnauthorized, Response{
|
||
|
Error: "You must be logged in to do that!",
|
||
|
})
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Parse request payload.
|
||
|
var req Request
|
||
|
if err := api.ParseJSON(r, &req); err != nil {
|
||
|
api.SendJSON(w, http.StatusBadRequest, Response{
|
||
|
Error: fmt.Sprintf("Error with request payload: %s", err),
|
||
|
})
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Validate it looked correct.
|
||
|
if req.Endpoint == "" || req.Keys.Auth == "" || req.Keys.P256DH == "" {
|
||
|
api.SendJSON(w, http.StatusBadRequest, Response{
|
||
|
Error: "Subscription fields were missing.",
|
||
|
})
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Serialize and store it in the database.
|
||
|
buf, err := json.Marshal(req)
|
||
|
if err != nil {
|
||
|
api.SendJSON(w, http.StatusInternalServerError, Response{
|
||
|
Error: "Couldn't reserialize your subscription!",
|
||
|
})
|
||
|
return
|
||
|
}
|
||
|
|
||
|
_, err = models.RegisterPushNotification(currentUser, string(buf))
|
||
|
if err != nil {
|
||
|
api.SendJSON(w, http.StatusInternalServerError, Response{
|
||
|
Error: "Couldn't create the registration in the database!",
|
||
|
})
|
||
|
return
|
||
|
}
|
||
|
|
||
|
api.SendJSON(w, http.StatusCreated, Response{
|
||
|
OK: true,
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Payload sent in push notifications.
|
||
|
type Payload struct {
|
||
|
Topic string `json:"-"`
|
||
|
Title string `json:"title"`
|
||
|
Body string `json:"body"`
|
||
|
}
|
||
|
|
||
|
// SendNotification sends a push notification to a user, broadcast to all of their subscriptions.
|
||
|
func SendNotification(user *models.User, body Payload) error {
|
||
|
// Send to all of their subscriptions.
|
||
|
subs, err := models.GetPushNotificationSubscriptions(user)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
payload, err := json.Marshal(body)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
for _, sub := range subs {
|
||
|
if err := SendRawNotification(user, payload, body.Topic, sub.Subscription); err != nil {
|
||
|
log.Error("SendNotification: %s", err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// SendNotificationToSubscription sends to a specific push subscriber.
|
||
|
func SendNotificationToSubscription(user *models.User, subscription string, body Payload) error {
|
||
|
payload, err := json.Marshal(body)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
return SendRawNotification(user, payload, body.Topic, subscription)
|
||
|
}
|
||
|
|
||
|
// SendRawNotification sends out a push message.
|
||
|
func SendRawNotification(user *models.User, message []byte, topic, subscription string) error {
|
||
|
// Decode the subscription.
|
||
|
var (
|
||
|
s = &webpush.Subscription{}
|
||
|
err = json.Unmarshal([]byte(subscription), s)
|
||
|
options = &webpush.Options{
|
||
|
Topic: topic,
|
||
|
Subscriber: user.Email,
|
||
|
VAPIDPublicKey: config.Current.WebPush.VAPIDPublicKey,
|
||
|
VAPIDPrivateKey: config.Current.WebPush.VAPIDPrivateKey,
|
||
|
TTL: 30,
|
||
|
}
|
||
|
)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
resp, err := webpush.SendNotification(message, s, options)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("webpush.SendNotification: %s", err)
|
||
|
}
|
||
|
defer resp.Body.Close()
|
||
|
|
||
|
// Handle error response codes.
|
||
|
if resp.StatusCode >= 400 {
|
||
|
log.Error("Got StatusCode %d when sending push notification; removing the subscription from DB", resp.StatusCode)
|
||
|
models.DeletePushNotification(user, subscription)
|
||
|
}
|
||
|
|
||
|
return err
|
||
|
}
|