mirror of
https://github.com/ergochat/ergo.git
synced 2025-01-21 09:44:21 +01:00
149 lines
4.5 KiB
Go
149 lines
4.5 KiB
Go
// Copyright (c) 2024 Shivaram Lingamneni <slingamn@cs.stanford.edu>
|
|
// Released under the MIT license
|
|
// Some portions of this code are:
|
|
// Copyright (c) 2021-2024 Simon Ser <contact@emersion.fr>
|
|
// Originally released under the AGPLv3, relicensed to the Ergo project under the MIT license
|
|
|
|
package webpush
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/ergochat/irc-go/ircmsg"
|
|
webpush "github.com/ergochat/webpush-go/v2"
|
|
|
|
"github.com/ergochat/ergo/irc/utils"
|
|
)
|
|
|
|
// alias some public types and names from webpush-go
|
|
type VAPIDKeys = webpush.VAPIDKeys
|
|
type Keys = webpush.Keys
|
|
|
|
var (
|
|
GenerateVAPIDKeys = webpush.GenerateVAPIDKeys
|
|
)
|
|
|
|
// Urgency is a uint8 representation of urgency to save a few
|
|
// bytes on channel sizes.
|
|
type Urgency uint8
|
|
|
|
const (
|
|
// UrgencyVeryLow requires device state: on power and Wi-Fi
|
|
UrgencyVeryLow Urgency = iota // "very-low"
|
|
// UrgencyLow requires device state: on either power or Wi-Fi
|
|
UrgencyLow // "low"
|
|
// UrgencyNormal excludes device state: low battery
|
|
UrgencyNormal // "normal"
|
|
// UrgencyHigh admits device state: low battery
|
|
UrgencyHigh // "high"
|
|
)
|
|
|
|
var (
|
|
// PingMessage is a valid IRC message that we can send to test that the subscription
|
|
// is valid (i.e. responds to POSTs with a 20x). We do not expect that the client will
|
|
// actually connect to IRC and send PONG (although it might be nice to have a way to
|
|
// hint to a client that they should reconnect to renew their subscription?)
|
|
PingMessage = []byte("PING webpush")
|
|
)
|
|
|
|
func convertUrgency(u Urgency) webpush.Urgency {
|
|
switch u {
|
|
case UrgencyVeryLow:
|
|
return webpush.UrgencyVeryLow
|
|
case UrgencyLow:
|
|
return webpush.UrgencyLow
|
|
case UrgencyNormal:
|
|
return webpush.UrgencyNormal
|
|
case UrgencyHigh:
|
|
return webpush.UrgencyHigh
|
|
default:
|
|
return webpush.UrgencyNormal // shouldn't happen
|
|
}
|
|
}
|
|
|
|
var httpClient webpush.HTTPClient = makeExternalOnlyClient()
|
|
|
|
var (
|
|
Err404 = errors.New("endpoint returned a 404, indicating that the push subscription is no longer valid")
|
|
|
|
errInvalidKey = errors.New("invalid key format")
|
|
)
|
|
|
|
func DecodeSubscriptionKeys(keysParam string) (keys webpush.Keys, err error) {
|
|
// The keys parameter is tag-encoded, with each tag value being URL-safe base64 encoded:
|
|
// * One public key with the name p256dh set to the client's P-256 ECDH public key.
|
|
// * One shared key with the name auth set to a 16-byte client-generated authentication secret.
|
|
// since we don't have a separate tag parser implementation, wrap it in a fake IRC line for parsing:
|
|
fakeIRCLine := fmt.Sprintf("@%s PING", keysParam)
|
|
ircMsg, err := ircmsg.ParseLine(fakeIRCLine)
|
|
if err != nil {
|
|
return
|
|
}
|
|
_, auth := ircMsg.GetTag("auth")
|
|
_, p256 := ircMsg.GetTag("p256dh")
|
|
return webpush.DecodeSubscriptionKeys(auth, p256)
|
|
}
|
|
|
|
// MakePushMessage serializes a utils.SplitMessage as a web push message (the args are in
|
|
// logical order)
|
|
func MakePushMessage(command, nuh, accountName, target string, msg utils.SplitMessage) ([]byte, error) {
|
|
var messageForPush string
|
|
if msg.Is512() {
|
|
messageForPush = msg.Message
|
|
} else {
|
|
messageForPush = msg.Split[0].Message
|
|
}
|
|
return MakePushLine(msg.Time, accountName, nuh, command, target, messageForPush)
|
|
}
|
|
|
|
// MakePushLine serializes an arbitrary IRC line as a web push message (the args are in
|
|
// IRC syntax order)
|
|
func MakePushLine(time time.Time, accountName, source, command string, params ...string) ([]byte, error) {
|
|
pushMessage := ircmsg.MakeMessage(nil, source, command, params...)
|
|
pushMessage.SetTag("time", time.Format(utils.IRCv3TimestampFormat))
|
|
// "*" is canonical for the unset form of the unfolded account name, but check both:
|
|
if accountName != "*" && accountName != "" {
|
|
pushMessage.SetTag("account", accountName)
|
|
}
|
|
if line, err := pushMessage.LineBytesStrict(false, 512); err == nil {
|
|
// strip final \r\n
|
|
return line[:len(line)-2], nil
|
|
} else {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
func SendWebPush(ctx context.Context, endpoint string, keys Keys, vapidKeys *VAPIDKeys, urgency Urgency, subscriber string, msg []byte) error {
|
|
wpsub := webpush.Subscription{
|
|
Endpoint: endpoint,
|
|
Keys: keys,
|
|
}
|
|
|
|
options := webpush.Options{
|
|
HTTPClient: httpClient,
|
|
VAPIDKeys: vapidKeys,
|
|
Subscriber: subscriber,
|
|
TTL: 7 * 24 * 60 * 60, // seconds
|
|
Urgency: convertUrgency(urgency),
|
|
RecordSize: 2048,
|
|
}
|
|
|
|
resp, err := webpush.SendNotification(ctx, msg, &wpsub, &options)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resp.Body.Close()
|
|
|
|
if resp.StatusCode == http.StatusNotFound {
|
|
return Err404
|
|
} else if 200 <= resp.StatusCode && resp.StatusCode < 300 {
|
|
return nil
|
|
} else {
|
|
return fmt.Errorf("HTTP error: %v", resp.Status)
|
|
}
|
|
}
|