197 lines
5.4 KiB
Go
197 lines
5.4 KiB
Go
|
package barfdb
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"crypto/rand"
|
||
|
"database/sql"
|
||
|
"database/sql/driver"
|
||
|
"encoding/json"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"strings"
|
||
|
)
|
||
|
|
||
|
type Dataset struct {
|
||
|
CurrentPhase int `json:"currentPhase"`
|
||
|
CurrentPhaseDialog int `json:"currentPhaseDialog"`
|
||
|
AudioEnabled bool `json:"audioEnabled"`
|
||
|
Name string `json:"name"`
|
||
|
DiscordUsername string `json:"discordUsername"`
|
||
|
ReceiveEmail bool `json:"receiveEmail"`
|
||
|
Email string `json:"email"`
|
||
|
DateSat31August string `json:"dateSat31August"`
|
||
|
DateSat7September string `json:"dateSat7September"`
|
||
|
ActivityEscapeRoom string `json:"activityEscapeRoom"`
|
||
|
ActivityPub string `json:"activityPub"`
|
||
|
ActivityMeanGirlsTheMusical string `json:"activityMeanGirlsTheMusical"`
|
||
|
ActivityKaraoke string `json:"activityKaraoke"`
|
||
|
AccommodationRequired bool `json:"accommodationRequired"`
|
||
|
TravelCosts bool `json:"travelCosts"`
|
||
|
Misc string `json:"misc"`
|
||
|
}
|
||
|
|
||
|
func (ds *Dataset) Scan(value any) error {
|
||
|
return json.Unmarshal([]byte(value.(string)), ds)
|
||
|
}
|
||
|
|
||
|
func (ds *Dataset) Value() (driver.Value, error) {
|
||
|
b, err := json.Marshal(ds)
|
||
|
return string(b), err
|
||
|
}
|
||
|
|
||
|
func (ds *Dataset) HumanString() (string, error) {
|
||
|
bs, err := json.MarshalIndent(ds, "", " ")
|
||
|
if err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
return string(bs), nil
|
||
|
}
|
||
|
|
||
|
type DB struct {
|
||
|
db *sql.DB
|
||
|
}
|
||
|
|
||
|
func New(sdb *sql.DB) *DB {
|
||
|
return &DB{sdb}
|
||
|
}
|
||
|
|
||
|
func (d *DB) Close() error {
|
||
|
return d.db.Close()
|
||
|
}
|
||
|
|
||
|
var ErrNoSuchKey = errors.New("barfdb: no such key")
|
||
|
|
||
|
const (
|
||
|
keyBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||
|
keyLength = 12
|
||
|
)
|
||
|
|
||
|
func (d *DB) Initialize(ctx context.Context) error {
|
||
|
_, err := d.db.ExecContext(ctx, "CREATE TABLE responses (key STRING PRIMARY KEY NOT NULL, responses JSONB NOT NULL)")
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
type Entry struct {
|
||
|
FullKey string
|
||
|
Dataset Dataset
|
||
|
}
|
||
|
|
||
|
func (e Entry) BaseKey() string {
|
||
|
if strings.Contains(e.FullKey, "/") {
|
||
|
return e.FullKey[:strings.Index(e.FullKey, "/")]
|
||
|
}
|
||
|
return e.FullKey
|
||
|
}
|
||
|
|
||
|
func (e Entry) Completed() bool {
|
||
|
mustHaveValue := []string{
|
||
|
e.Dataset.DiscordUsername,
|
||
|
e.Dataset.Name,
|
||
|
e.Dataset.ActivityEscapeRoom,
|
||
|
e.Dataset.ActivityKaraoke,
|
||
|
e.Dataset.ActivityMeanGirlsTheMusical,
|
||
|
e.Dataset.ActivityPub,
|
||
|
e.Dataset.DateSat31August,
|
||
|
e.Dataset.DateSat7September,
|
||
|
}
|
||
|
for _, v := range mustHaveValue {
|
||
|
if v == "" {
|
||
|
return false
|
||
|
}
|
||
|
}
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
func (d *DB) List(ctx context.Context) ([]Entry, error) {
|
||
|
rows, err := d.db.QueryContext(ctx, "SELECT key, responses FROM responses ORDER BY key")
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("querying database: %w", err)
|
||
|
}
|
||
|
var entries []Entry
|
||
|
for rows.Next() {
|
||
|
var (
|
||
|
fullKey string
|
||
|
dataset Dataset
|
||
|
)
|
||
|
if err := rows.Scan(&fullKey, &dataset); err != nil {
|
||
|
return nil, fmt.Errorf("scanning row from database: %w", err)
|
||
|
}
|
||
|
|
||
|
entry := Entry{
|
||
|
FullKey: fullKey,
|
||
|
Dataset: dataset,
|
||
|
}
|
||
|
entries = append(entries, entry)
|
||
|
}
|
||
|
return entries, nil
|
||
|
}
|
||
|
|
||
|
func (d *DB) GetOrCreateKey(ctx context.Context, baseKey string, initialDataset *Dataset) (string, error) {
|
||
|
var existingKey string
|
||
|
err := d.db.QueryRowContext(ctx, `SELECT key FROM responses WHERE key LIKE ? || '/%'`, baseKey).Scan(&existingKey)
|
||
|
if err == nil {
|
||
|
return existingKey, nil
|
||
|
} else if errors.Is(err, sql.ErrNoRows) {
|
||
|
// This is fine, we'll create it.
|
||
|
} else {
|
||
|
return "", fmt.Errorf("querying database for existing keys for base %q: %w", baseKey, err)
|
||
|
}
|
||
|
|
||
|
randomKeyBytes := make([]byte, keyLength)
|
||
|
if _, err := rand.Read(randomKeyBytes); err != nil {
|
||
|
return "", fmt.Errorf("generating random key: %w", err)
|
||
|
}
|
||
|
|
||
|
randomKeyStringBytes := make([]byte, keyLength)
|
||
|
for n, b := range randomKeyBytes {
|
||
|
randomKeyStringBytes[n] = keyBytes[int(b)%len(keyBytes)]
|
||
|
}
|
||
|
randomKey := string(randomKeyStringBytes)
|
||
|
fullKey := baseKey + "/" + randomKey
|
||
|
|
||
|
if _, err := d.db.ExecContext(ctx, `INSERT INTO responses (key, responses) VALUES (?, ?)`, fullKey, initialDataset); err != nil {
|
||
|
return "", fmt.Errorf("inserting into responses table: %w", err)
|
||
|
}
|
||
|
return fullKey, nil
|
||
|
}
|
||
|
|
||
|
func (d *DB) GetResponseForShortKey(ctx context.Context, shortKey string) (*Dataset, error) {
|
||
|
var response Dataset
|
||
|
err := d.db.QueryRowContext(ctx, `SELECT responses FROM responses WHERE key = ? OR key LIKE ? || '/%'`, shortKey, shortKey).Scan(&response)
|
||
|
if errors.Is(err, sql.ErrNoRows) {
|
||
|
return nil, ErrNoSuchKey
|
||
|
} else if err != nil {
|
||
|
return nil, fmt.Errorf("fetching %v from database: %w", shortKey, err)
|
||
|
}
|
||
|
return &response, nil
|
||
|
}
|
||
|
|
||
|
func (d *DB) GetResponseForKey(ctx context.Context, key string) (*Dataset, error) {
|
||
|
var response Dataset
|
||
|
err := d.db.QueryRowContext(ctx, `SELECT responses FROM responses WHERE key = ?`, key).Scan(&response)
|
||
|
if errors.Is(err, sql.ErrNoRows) {
|
||
|
return nil, ErrNoSuchKey
|
||
|
} else if err != nil {
|
||
|
return nil, fmt.Errorf("fetching %v from database: %w", key, err)
|
||
|
}
|
||
|
return &response, nil
|
||
|
}
|
||
|
|
||
|
func (d *DB) SaveResponseForKey(ctx context.Context, key string, dataset *Dataset) error {
|
||
|
res, err := d.db.ExecContext(ctx, `UPDATE responses SET responses = ? WHERE key = ?`, dataset, key)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("updating database for %v: %w", key, err)
|
||
|
}
|
||
|
rowCount, err := res.RowsAffected()
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("determining affected row count for %v: %w", key, err)
|
||
|
} else if rowCount == 0 {
|
||
|
return ErrNoSuchKey
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func NewDB(db *sql.DB) *DB {
|
||
|
return &DB{db: db}
|
||
|
}
|