Files
Pastel/cmd/migrate/main.go
prosolis e452e36a63 Fix deployment issues and add threads, numbered watchlist, search URL encoding
- Fix SQLite connection string (drop query param, use explicit PRAGMA)
- Register sqlite3-fk-wal driver for mautrix cryptohelper (pure Go, no CGO)
- Fetch DeviceID via /whoami when using bare access token
- Log cross-signing results at Warn/Info instead of Debug
- Post deals into Matrix threads: Game Deals, DLC Deals, Epic Free Games
- Add numbered watchlist; !extend and !unwatch accept list numbers
- URL-encode search queries for multi-word !search commands
- Add GetConfig/SetConfig helpers for persistent thread event IDs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 20:56:26 -07:00

175 lines
4.2 KiB
Go

// Command migrate converts a Pastel Python database to the Go format.
//
// The Python bot writes posted_at timestamps using SQLite's CURRENT_TIMESTAMP
// ("YYYY-MM-DD HH:MM:SS") and prunes with Python's isoformat()
// ("YYYY-MM-DDTHH:MM:SS+00:00"). The Go bot uses RFC 3339 ("YYYY-MM-DDTHH:MM:SSZ").
//
// This script normalizes all posted_at values to RFC 3339 so the Go bot can use
// its native format consistently. It also creates any new tables (watchlist) that
// the Python version didn't have.
//
// Usage:
//
// go run ./cmd/migrate [path-to-deals.db]
//
// The database is modified in place. A backup is created at <path>.bak first.
package main
import (
"database/sql"
"fmt"
"io"
"os"
"strings"
"time"
_ "modernc.org/sqlite"
)
// Formats the Python bot may have written.
var parseFormats = []string{
"2006-01-02 15:04:05", // SQLite CURRENT_TIMESTAMP
"2006-01-02T15:04:05+00:00", // Python datetime.isoformat()
"2006-01-02T15:04:05Z", // RFC 3339 (already correct)
"2006-01-02T15:04:05-07:00", // RFC 3339 with offset
}
func main() {
dbPath := "deals.db"
if len(os.Args) > 1 {
dbPath = os.Args[1]
}
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
fmt.Fprintf(os.Stderr, "Database not found: %s\n", dbPath)
os.Exit(1)
}
// Create backup
backupPath := dbPath + ".bak"
if err := copyFile(dbPath, backupPath); err != nil {
fmt.Fprintf(os.Stderr, "Failed to create backup: %v\n", err)
os.Exit(1)
}
fmt.Printf("Backup created: %s\n", backupPath)
db, err := sql.Open("sqlite", dbPath)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to open database: %v\n", err)
os.Exit(1)
}
defer db.Close()
// Ensure new tables exist (watchlist, etc.)
if _, err := db.Exec(`
CREATE TABLE IF NOT EXISTS watchlist (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
game_name TEXT NOT NULL,
game_name_normalized TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP NOT NULL,
expiry_warned INTEGER DEFAULT 0,
UNIQUE(user_id, game_name_normalized)
);
CREATE INDEX IF NOT EXISTS idx_watchlist_normalized ON watchlist(game_name_normalized);
CREATE INDEX IF NOT EXISTS idx_watchlist_expires ON watchlist(expires_at);
`); err != nil {
fmt.Fprintf(os.Stderr, "Failed to create new tables: %v\n", err)
os.Exit(1)
}
// Convert posted_at timestamps
rows, err := db.Query("SELECT id, posted_at FROM posted_deals")
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to query deals: %v\n", err)
os.Exit(1)
}
type update struct {
id string
converted string
}
var updates []update
for rows.Next() {
var id, raw string
if err := rows.Scan(&id, &raw); err != nil {
continue
}
converted, changed := normalizeTimestamp(raw)
if changed {
updates = append(updates, update{id, converted})
}
}
rows.Close()
if len(updates) == 0 {
fmt.Println("No timestamps need conversion. Database is already in Go format.")
return
}
tx, err := db.Begin()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to begin transaction: %v\n", err)
os.Exit(1)
}
stmt, err := tx.Prepare("UPDATE posted_deals SET posted_at = ? WHERE id = ?")
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to prepare statement: %v\n", err)
os.Exit(1)
}
for _, u := range updates {
if _, err := stmt.Exec(u.converted, u.id); err != nil {
tx.Rollback()
fmt.Fprintf(os.Stderr, "Failed to update %s: %v\n", u.id, err)
os.Exit(1)
}
}
stmt.Close()
if err := tx.Commit(); err != nil {
fmt.Fprintf(os.Stderr, "Failed to commit: %v\n", err)
os.Exit(1)
}
fmt.Printf("Converted %d timestamps to RFC 3339.\n", len(updates))
fmt.Println("Migration complete.")
}
func normalizeTimestamp(raw string) (string, bool) {
raw = strings.TrimSpace(raw)
for _, layout := range parseFormats {
if t, err := time.Parse(layout, raw); err == nil {
rfc := t.UTC().Format(time.RFC3339)
return rfc, rfc != raw
}
}
// Unparseable — leave as-is
return raw, false
}
func copyFile(src, dst string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return err
}
defer out.Close()
if _, err := io.Copy(out, in); err != nil {
return err
}
return out.Sync()
}