// pushdevice.go remembers WHICH browsers a person turned push on in,
// and nothing more (RFC 0011 §6). The subscription itself lives in
// notify as subscriber credentials — quarry stores no endpoint, no
// keys, and could not send a push if it wanted to. What is here is the
// one thing notify cannot answer for a preferences page: a list a
// person recognizes, so they can turn one off.
package oltp
import (
"crypto/sha256"
"encoding/hex"
"time"
)
const pushDeviceSchema = `
CREATE TABLE IF NOT EXISTS push_device (
account TEXT NOT NULL,
endpoint_hash TEXT NOT NULL, -- sha-256 of the push endpoint, never the endpoint
ua TEXT NOT NULL,
at TEXT NOT NULL,
PRIMARY KEY (account, endpoint_hash)
);`
// PushDevice is one browser a person enabled push in.
type PushDevice struct {
EndpointHash string
UA string
At string
}
// PushEndpointHash is the identity a device row is keyed by. A push
// endpoint is a bearer capability — anyone holding it can make
// somebody's phone buzz — so quarry keeps only its digest, which is
// enough to match, delete and de-duplicate and useless to a reader of
// the database.
func PushEndpointHash(endpoint string) string {
sum := sha256.Sum256([]byte(endpoint))
return hex.EncodeToString(sum[:])
}
// AddPushDevice records (or refreshes) one device for an account.
// Idempotent on the endpoint: a page that re-subscribes on every load
// must not grow the list.
func (d *DB) AddPushDevice(account, endpoint, ua string) error {
if account == "" || endpoint == "" {
return nil
}
_, err := d.db.Exec(`INSERT INTO push_device (account, endpoint_hash, ua, at)
VALUES (?, ?, ?, ?)
ON CONFLICT (account, endpoint_hash) DO UPDATE SET ua = excluded.ua, at = excluded.at`,
account, PushEndpointHash(endpoint), ua, time.Now().UTC().Format(time.RFC3339Nano))
return err
}
// DeletePushDevice forgets one device. Removing one that is not there
// is success — the end state asked for is what already holds.
func (d *DB) DeletePushDevice(account, endpoint string) error {
if account == "" || endpoint == "" {
return nil
}
_, err := d.db.Exec(`DELETE FROM push_device WHERE account = ? AND endpoint_hash = ?`,
account, PushEndpointHash(endpoint))
return err
}
// PushDevices lists an account's devices, newest first.
func (d *DB) PushDevices(account string) ([]PushDevice, error) {
if account == "" {
return nil, nil
}
rows, err := d.db.Query(`SELECT endpoint_hash, ua, at FROM push_device
WHERE account = ? ORDER BY at DESC`, account)
if err != nil {
return nil, err
}
defer rows.Close()
var out []PushDevice
for rows.Next() {
var p PushDevice
if err := rows.Scan(&p.EndpointHash, &p.UA, &p.At); err != nil {
return nil, err
}
out = append(out, p)
}
return out, rows.Err()
}