3
0
mirror of https://github.com/ergochat/ergo.git synced 2025-01-21 09:44:21 +01:00
ergo/irc/webpush/webpush.go
2025-01-13 21:47:21 -05:00

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)
}
}