package database import ( "database/sql" "os" "path/filepath" "time" _ "github.com/mattn/go-sqlite3" ) type DB struct { *sql.DB } func Open(dataDir string) (*DB, error) { if err := os.MkdirAll(dataDir, 0755); err != nil { return nil, err } dbPath := filepath.Join(dataDir, "jam.db") db, err := sql.Open("sqlite3", dbPath+"?_journal_mode=WAL&_busy_timeout=5000&_synchronous=NORMAL") if err != nil { return nil, err } db.SetMaxOpenConns(1) db.SetMaxIdleConns(1) db.SetConnMaxLifetime(time.Hour) d := &DB{db} if err := d.migrate(); err != nil { return nil, err } return d, nil } func (db *DB) migrate() error { schema := ` CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, username TEXT NOT NULL UNIQUE, avatar TEXT DEFAULT '', bio TEXT DEFAULT '', public_key BLOB, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS guilds ( id TEXT PRIMARY KEY, name TEXT NOT NULL, owner_id TEXT NOT NULL, icon TEXT DEFAULT '', description TEXT DEFAULT '', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (owner_id) REFERENCES users(id) ); CREATE TABLE IF NOT EXISTS guild_members ( guild_id TEXT NOT NULL, user_id TEXT NOT NULL, nickname TEXT DEFAULT '', avatar TEXT DEFAULT '', joined_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (guild_id, user_id), FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS categories ( id TEXT PRIMARY KEY, guild_id TEXT NOT NULL, name TEXT NOT NULL, position INTEGER DEFAULT 0, FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS channels ( id TEXT PRIMARY KEY, guild_id TEXT NOT NULL, category_id TEXT DEFAULT '', name TEXT NOT NULL, type TEXT NOT NULL DEFAULT 'text', topic TEXT DEFAULT '', position INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS roles ( id TEXT PRIMARY KEY, guild_id TEXT NOT NULL, name TEXT NOT NULL, color INTEGER DEFAULT 0, position INTEGER DEFAULT 0, permissions TEXT DEFAULT '[]', is_default INTEGER DEFAULT 0, FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS role_members ( role_id TEXT NOT NULL, user_id TEXT NOT NULL, PRIMARY KEY (role_id, user_id), FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS messages ( id TEXT PRIMARY KEY, channel_id TEXT NOT NULL, author_id TEXT NOT NULL, content BLOB, encrypted INTEGER DEFAULT 0, nonce BLOB, message_type TEXT DEFAULT 'text', reply_to TEXT, pinned INTEGER DEFAULT 0, self_destruct DATETIME, edited_at DATETIME, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE, FOREIGN KEY (author_id) REFERENCES users(id) ); CREATE INDEX IF NOT EXISTS idx_messages_channel ON messages(channel_id, created_at); CREATE TABLE IF NOT EXISTS attachments ( id TEXT PRIMARY KEY, message_id TEXT NOT NULL, filename TEXT NOT NULL, file_type TEXT DEFAULT '', size INTEGER DEFAULT 0, compressed INTEGER DEFAULT 0, original_size INTEGER DEFAULT 0, url TEXT NOT NULL, hash TEXT DEFAULT '', FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS reactions ( message_id TEXT NOT NULL, user_id TEXT NOT NULL, emoji TEXT NOT NULL, PRIMARY KEY (message_id, user_id, emoji), FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(id) ); CREATE TABLE IF NOT EXISTS stickers ( id TEXT PRIMARY KEY, guild_id TEXT NOT NULL, name TEXT NOT NULL, data BLOB, format TEXT DEFAULT 'png', FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS voice_states ( user_id TEXT NOT NULL, channel_id TEXT NOT NULL, guild_id TEXT NOT NULL, muted INTEGER DEFAULT 0, deafened INTEGER DEFAULT 0, PRIMARY KEY (user_id, channel_id) ); CREATE TABLE IF NOT EXISTS streams ( id TEXT PRIMARY KEY, channel_id TEXT NOT NULL, user_id TEXT NOT NULL, title TEXT DEFAULT '', started_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS federation_peers ( domain TEXT PRIMARY KEY, name TEXT NOT NULL, public_key BLOB, last_seen DATETIME, is_active INTEGER DEFAULT 1 ); CREATE TABLE IF NOT EXISTS server_config ( key TEXT PRIMARY KEY, value TEXT NOT NULL ); ` _, err := db.Exec(schema) return err }