Skip to content
quarry Artifact [81acc30f84]
Login

Artifact [81acc30f84]

Artifact 81acc30f84a90bb88476a17dad861e4b4dfd9583a98c49dc7ab931bd51f176b1:


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