// SPDX-License-Identifier: AGPL-3.0-or-later package storage import ( "database/sql" "errors" "time" _ "modernc.org/sqlite" ) // ErrNotFound is returned by Get when no device row matches the given id. var ErrNotFound = errors.New("device not found") type Device struct { ID string FCMToken string PublicKey []byte // 32-byte X25519 CreatedAt time.Time LastSeenAt time.Time } type Store struct { db *sql.DB } func Open(path string) (*Store, error) { db, err := sql.Open("sqlite", path) if err != nil { return nil, err } if err := db.Ping(); err != nil { return nil, err } if _, err := db.Exec(schema); err != nil { return nil, err } return &Store{db: db}, nil } const schema = ` CREATE TABLE IF NOT EXISTS devices ( id TEXT PRIMARY KEY, fcm_token TEXT NOT NULL, public_key BLOB NOT NULL, created_at INTEGER NOT NULL, last_seen_at INTEGER NOT NULL ); ` // Get returns the device row for id, or ErrNotFound if no row exists. func (s *Store) Get(id string) (*Device, error) { var d Device var created, lastSeen int64 err := s.db.QueryRow( `SELECT id, fcm_token, public_key, created_at, last_seen_at FROM devices WHERE id = ?`, id, ).Scan(&d.ID, &d.FCMToken, &d.PublicKey, &created, &lastSeen) if errors.Is(err, sql.ErrNoRows) { return nil, ErrNotFound } if err != nil { return nil, err } d.CreatedAt = time.Unix(created, 0) d.LastSeenAt = time.Unix(lastSeen, 0) return &d, nil } func (s *Store) Upsert(d Device) error { _, err := s.db.Exec( `INSERT INTO devices (id, fcm_token, public_key, created_at, last_seen_at) VALUES (?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET fcm_token = excluded.fcm_token, public_key = excluded.public_key, last_seen_at = excluded.last_seen_at`, d.ID, d.FCMToken, d.PublicKey, d.CreatedAt.Unix(), d.LastSeenAt.Unix(), ) return err } func (s *Store) ListAll() ([]Device, error) { rows, err := s.db.Query(`SELECT id, fcm_token, public_key, created_at, last_seen_at FROM devices`) if err != nil { return nil, err } defer rows.Close() var devices []Device for rows.Next() { var d Device var created, lastSeen int64 if err := rows.Scan(&d.ID, &d.FCMToken, &d.PublicKey, &created, &lastSeen); err != nil { return nil, err } d.CreatedAt = time.Unix(created, 0) d.LastSeenAt = time.Unix(lastSeen, 0) devices = append(devices, d) } return devices, rows.Err() } func (s *Store) Delete(id string) error { res, err := s.db.Exec(`DELETE FROM devices WHERE id = ?`, id) if err != nil { return err } n, _ := res.RowsAffected() if n == 0 { return ErrNotFound } return nil } func (s *Store) Close() error { return s.db.Close() }