Add push reminders for sleep, pee, poo and meals
A closed PWA has no timers, so reminders are evaluated on the server: the event log is already there (clients sync on every mutation), and a ticker re-checks each enabled rule once a minute and pushes the ones that are due. Two rule shapes. "sleep" measures from the last sleep-end and fires only while the puppy is awake. "pee"/"poo"/"eat" measure from the newest event of that type and stay quiet while the puppy is asleep — otherwise they nag all night, and suppressing them means an overdue rule instead fires promptly on waking, which is when it actually matters. Sleep state is derived exactly the way currentSleepState() does in app.js, tie-break included, so both sides always agree. Rules read the event's own timestamp rather than when it synced, so a pee logged offline at 03:10 cancels the reminder retroactively. Every push carries a tag, so a repeat replaces the previous notification instead of stacking another one on the lock screen. last_fired is server-owned and not writable by a client, so a stale device can't force a re-fire. Web Push is implemented directly rather than pulled in as a dependency: RFC 8291 encryption in the RFC 8188 aes128gcm coding with an RFC 8292 VAPID token, stdlib only, checked against the RFC 8291 test vector. The key is generated into vapid.json beside the DB or supplied via -vapid-key; without one the server logs a warning, skips registering the routes, and the client hides the UI. Subscriptions a push service reports as 404/410 are dropped. PNG icons are added because iOS gates push on a Home Screen install and rejects SVG for apple-touch-icon, and Android has no notification icon without them.
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/ecdh"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// The worked example from RFC 8291 §5. Reproducing it exactly pins every step
|
||||
// of the derivation — the ECDH, both HKDF extractions, the record padding and
|
||||
// the RFC 8188 header layout — against the spec rather than against ourselves.
|
||||
func TestEncryptPayloadRFC8291Vector(t *testing.T) {
|
||||
const (
|
||||
plaintext = "When I grow up, I want to be a watermelon"
|
||||
authSecret = "BTBZMqHH6r4Tts7J_aSIgg"
|
||||
receiverPub = "BCVxsr7N_eNgVRqvHtD0zTZsEc6-VV-JvLexhqUzORcxaOzi6-AYWXvTBHm4bjyPjs7Vd8pZGH6SRpkNtoIAiw4"
|
||||
senderPriv = "yfWPiYE-n46HLnH0KqZOF1fJJU3MYrct3AELtAQ-oRw"
|
||||
saltB64 = "DGv6ra1nlYgDCS1FRnbzlw"
|
||||
wantCiphered = "DGv6ra1nlYgDCS1FRnbzlwAAEABBBP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27ml" +
|
||||
"mlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A_yl95bQpu6cVPT" +
|
||||
"pK4Mqgkf1CXztLVBSt2Ks3oZwbuwXPXLWyouBWLVWGNWQexSgSxsj_Qulcy4a-fN"
|
||||
)
|
||||
|
||||
var sub Subscription
|
||||
sub.Endpoint = "https://push.example.net/push/JzLQ3raZJfFBR0aqvOMsLrt54w4rJUsV"
|
||||
sub.Keys.P256dh = receiverPub
|
||||
sub.Keys.Auth = authSecret
|
||||
|
||||
salt, err := b64.DecodeString(saltB64)
|
||||
if err != nil {
|
||||
t.Fatalf("decode salt: %v", err)
|
||||
}
|
||||
seed, err := b64.DecodeString(senderPriv)
|
||||
if err != nil {
|
||||
t.Fatalf("decode sender key: %v", err)
|
||||
}
|
||||
eph, err := ecdh.P256().NewPrivateKey(seed)
|
||||
if err != nil {
|
||||
t.Fatalf("sender key: %v", err)
|
||||
}
|
||||
|
||||
got, err := encryptPayload(sub, []byte(plaintext), salt, eph)
|
||||
if err != nil {
|
||||
t.Fatalf("encryptPayload: %v", err)
|
||||
}
|
||||
if b64.EncodeToString(got) != wantCiphered {
|
||||
t.Errorf("ciphertext mismatch\n got: %s\nwant: %s", b64.EncodeToString(got), wantCiphered)
|
||||
}
|
||||
}
|
||||
|
||||
// A VAPID key must survive the round trip through its on-disk form, since the
|
||||
// public half is pinned by every subscription made while it was in use.
|
||||
func TestVAPIDKeyRoundTrip(t *testing.T) {
|
||||
key, err := newVAPIDKey()
|
||||
if err != nil {
|
||||
t.Fatalf("generate: %v", err)
|
||||
}
|
||||
raw, err := key.marshal()
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
back, err := parseVAPIDKey(raw)
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
if back.Public != key.Public {
|
||||
t.Errorf("public key changed across round trip: %s != %s", back.Public, key.Public)
|
||||
}
|
||||
if _, err := back.authHeader("https://push.example.net/push/abc", vapidSubject); err != nil {
|
||||
t.Errorf("authHeader: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user