BirdWatch-Relay/internal/storage/sqlite.go
2026-05-03 22:47:38 -05:00

118 lines
2.6 KiB
Go

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