mirror of
				https://github.com/ergochat/ergo.git
				synced 2025-10-31 13:57:23 +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)
 | |
| 	}
 | |
| }
 | 
