import os import sqlite3 import time from typing import Optional from werkzeug.security import generate_password_hash from config import ( DB_PATH, UPLOAD_DIR, ROLES, STATUSES, now_iso, ) def get_db() -> sqlite3.Connection: db_dir = os.path.dirname(DB_PATH) if db_dir: os.makedirs(db_dir, exist_ok=True) conn = sqlite3.connect(DB_PATH, timeout=8, isolation_level=None, check_same_thread=False) conn.row_factory = sqlite3.Row conn.execute("PRAGMA foreign_keys = ON") conn.execute("PRAGMA journal_mode = WAL") conn.execute("PRAGMA busy_timeout = 8000") conn.execute("PRAGMA synchronous = NORMAL") return conn def db_write(query: str, params: tuple = ()) -> sqlite3.Cursor: last_error = None for attempt in range(5): try: with get_db() as db: db.execute("BEGIN IMMEDIATE") cursor = db.execute(query, params) db.commit() return cursor except sqlite3.OperationalError as error: last_error = error if "locked" not in str(error).lower() and "busy" not in str(error).lower(): raise if attempt == 4: raise time.sleep(0.05 * (2 ** attempt)) raise last_error def column_exists(db: sqlite3.Connection, table: str, column: str) -> bool: rows = db.execute(f"PRAGMA table_info({table})").fetchall() return any(row["name"] == column for row in rows) def init_db() -> None: UPLOAD_DIR.mkdir(parents=True, exist_ok=True) with get_db() as db: db.execute( """ CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT COLLATE NOCASE UNIQUE NOT NULL, password_hash TEXT NOT NULL, is_admin INTEGER NOT NULL DEFAULT 0, is_active INTEGER NOT NULL DEFAULT 1, role TEXT NOT NULL DEFAULT 'user', status TEXT NOT NULL DEFAULT 'active', restricted_until TEXT, restriction_reason TEXT, created_at TEXT NOT NULL, last_login_at TEXT ) """ ) db.execute( """ CREATE TABLE IF NOT EXISTS rooms ( id INTEGER PRIMARY KEY AUTOINCREMENT, slug TEXT COLLATE NOCASE UNIQUE NOT NULL, name TEXT NOT NULL, description TEXT, visibility TEXT NOT NULL DEFAULT 'public', created_by_user_id INTEGER, created_at TEXT NOT NULL, is_active INTEGER NOT NULL DEFAULT 1, FOREIGN KEY (created_by_user_id) REFERENCES users(id) ) """ ) db.execute( """ CREATE TABLE IF NOT EXISTS room_members ( room_id INTEGER NOT NULL, user_id INTEGER NOT NULL, membership_role TEXT NOT NULL DEFAULT 'member', added_at TEXT NOT NULL, PRIMARY KEY (room_id, user_id), FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) """ ) db.execute( """ CREATE TABLE IF NOT EXISTS attachments ( id INTEGER PRIMARY KEY AUTOINCREMENT, uploader_user_id INTEGER NOT NULL, stored_name TEXT UNIQUE NOT NULL, original_name TEXT NOT NULL, mime_type TEXT NOT NULL, size_bytes INTEGER NOT NULL, created_at TEXT NOT NULL, FOREIGN KEY (uploader_user_id) REFERENCES users(id) ON DELETE CASCADE ) """ ) db.execute( """ CREATE TABLE IF NOT EXISTS teams ( id INTEGER PRIMARY KEY AUTOINCREMENT, slug TEXT COLLATE NOCASE UNIQUE NOT NULL, name TEXT NOT NULL, description TEXT, created_by_user_id INTEGER, created_at TEXT NOT NULL, is_active INTEGER NOT NULL DEFAULT 1, FOREIGN KEY (created_by_user_id) REFERENCES users(id) ON DELETE SET NULL ) """ ) db.execute( """ CREATE TABLE IF NOT EXISTS team_members ( team_id INTEGER NOT NULL, user_id INTEGER NOT NULL, membership_role TEXT NOT NULL DEFAULT 'member', added_at TEXT NOT NULL, PRIMARY KEY (team_id, user_id), FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) """ ) db.execute( """ CREATE TABLE IF NOT EXISTS team_invites ( id INTEGER PRIMARY KEY AUTOINCREMENT, team_id INTEGER NOT NULL, token_hash TEXT UNIQUE NOT NULL, created_by_user_id INTEGER NOT NULL, membership_role TEXT NOT NULL DEFAULT 'member', created_at TEXT NOT NULL, expires_at TEXT NOT NULL, used_at TEXT, used_by_user_id INTEGER, FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE, FOREIGN KEY (created_by_user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (used_by_user_id) REFERENCES users(id) ON DELETE SET NULL ) """ ) db.execute( """ CREATE TABLE IF NOT EXISTS channels ( id INTEGER PRIMARY KEY AUTOINCREMENT, team_id INTEGER NOT NULL, legacy_room_id INTEGER UNIQUE, slug TEXT COLLATE NOCASE NOT NULL, name TEXT NOT NULL, description TEXT, created_by_user_id INTEGER, created_at TEXT NOT NULL, is_active INTEGER NOT NULL DEFAULT 1, UNIQUE(team_id, slug), FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE, FOREIGN KEY (legacy_room_id) REFERENCES rooms(id) ON DELETE SET NULL, FOREIGN KEY (created_by_user_id) REFERENCES users(id) ON DELETE SET NULL ) """ ) db.execute( """ CREATE TABLE IF NOT EXISTS direct_conversations ( id INTEGER PRIMARY KEY AUTOINCREMENT, member_pair_key TEXT UNIQUE NOT NULL, created_at TEXT NOT NULL ) """ ) db.execute( """ CREATE TABLE IF NOT EXISTS direct_conversation_members ( direct_conversation_id INTEGER NOT NULL, user_id INTEGER NOT NULL, added_at TEXT NOT NULL, PRIMARY KEY (direct_conversation_id, user_id), FOREIGN KEY (direct_conversation_id) REFERENCES direct_conversations(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) """ ) db.execute( """ CREATE TABLE IF NOT EXISTS messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, room_id INTEGER, channel_id INTEGER, direct_conversation_id INTEGER, user_id INTEGER NOT NULL, username TEXT NOT NULL, text TEXT NOT NULL, attachment_id INTEGER, parent_message_id INTEGER, created_at TEXT NOT NULL, FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE, FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE, FOREIGN KEY (direct_conversation_id) REFERENCES direct_conversations(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (attachment_id) REFERENCES attachments(id) ON DELETE SET NULL, FOREIGN KEY (parent_message_id) REFERENCES messages(id) ON DELETE CASCADE ) """ ) db.execute( """ CREATE TABLE IF NOT EXISTS room_invites ( id INTEGER PRIMARY KEY AUTOINCREMENT, room_id INTEGER NOT NULL, token_hash TEXT UNIQUE NOT NULL, created_by_user_id INTEGER NOT NULL, created_at TEXT NOT NULL, expires_at TEXT NOT NULL, used_at TEXT, used_by_user_id INTEGER, FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE, FOREIGN KEY (created_by_user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (used_by_user_id) REFERENCES users(id) ON DELETE SET NULL ) """ ) db.execute( """ CREATE TABLE IF NOT EXISTS typing_events ( room_id INTEGER NOT NULL, user_id INTEGER NOT NULL, updated_at REAL NOT NULL, PRIMARY KEY (room_id, user_id), FOREIGN KEY (room_id) REFERENCES rooms(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) """ ) db.execute( """ CREATE TABLE IF NOT EXISTS channel_typing_events ( channel_id INTEGER NOT NULL, user_id INTEGER NOT NULL, updated_at REAL NOT NULL, PRIMARY KEY (channel_id, user_id), FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) """ ) db.execute( """ CREATE TABLE IF NOT EXISTS dm_typing_events ( direct_conversation_id INTEGER NOT NULL, user_id INTEGER NOT NULL, updated_at REAL NOT NULL, PRIMARY KEY (direct_conversation_id, user_id), FOREIGN KEY (direct_conversation_id) REFERENCES direct_conversations(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) """ ) db.execute( """ CREATE TABLE IF NOT EXISTS blacklist_entries ( id INTEGER PRIMARY KEY AUTOINCREMENT, kind TEXT NOT NULL, value_hash TEXT NOT NULL, reason TEXT, created_at TEXT NOT NULL, expires_at TEXT, created_by_user_id INTEGER, UNIQUE(kind, value_hash), FOREIGN KEY (created_by_user_id) REFERENCES users(id) ) """ ) db.execute( """ CREATE TABLE IF NOT EXISTS rate_limit_events ( id INTEGER PRIMARY KEY AUTOINCREMENT, scope TEXT NOT NULL, subject_hash TEXT NOT NULL, created_at REAL NOT NULL ) """ ) db.execute( """ CREATE TABLE IF NOT EXISTS audit_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, actor_user_id INTEGER, action TEXT NOT NULL, target_type TEXT, target_id TEXT, details TEXT, client_hash TEXT, created_at TEXT NOT NULL, FOREIGN KEY (actor_user_id) REFERENCES users(id) ON DELETE SET NULL ) """ ) db.execute( """ CREATE TABLE IF NOT EXISTS pages ( id INTEGER PRIMARY KEY AUTOINCREMENT, team_id INTEGER NOT NULL, channel_id INTEGER, slug TEXT COLLATE NOCASE NOT NULL, title TEXT NOT NULL, body TEXT NOT NULL DEFAULT '', visibility TEXT NOT NULL DEFAULT 'team', page_type TEXT NOT NULL DEFAULT 'document', created_by_user_id INTEGER NOT NULL, updated_by_user_id INTEGER NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, is_active INTEGER NOT NULL DEFAULT 1, UNIQUE(team_id, slug), FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE, FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE SET NULL, FOREIGN KEY (created_by_user_id) REFERENCES users(id) ON DELETE RESTRICT, FOREIGN KEY (updated_by_user_id) REFERENCES users(id) ON DELETE RESTRICT ) """ ) db.execute( """ CREATE TABLE IF NOT EXISTS user_profiles ( user_id INTEGER PRIMARY KEY, display_name TEXT, badge_text TEXT, bio TEXT NOT NULL DEFAULT '', bio_mode TEXT NOT NULL DEFAULT 'txt', website TEXT, location TEXT, status_message TEXT NOT NULL DEFAULT '', banner_attachment_id INTEGER, updated_at TEXT NOT NULL, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (banner_attachment_id) REFERENCES attachments(id) ON DELETE SET NULL ) """ ) db.execute( """ CREATE TABLE IF NOT EXISTS user_settings ( user_id INTEGER PRIMARY KEY, accent_color TEXT, name_color TEXT, custom_css TEXT NOT NULL DEFAULT '', theme_mode TEXT NOT NULL DEFAULT 'dark', profile_background_color TEXT, profile_banner_color TEXT, profile_visibility TEXT NOT NULL DEFAULT 'public', last_active_visibility TEXT NOT NULL DEFAULT 'everyone', presence_mode TEXT NOT NULL DEFAULT 'online', dm_layout_mode TEXT NOT NULL DEFAULT 'cards', show_presence_badge INTEGER NOT NULL DEFAULT 1, updated_at TEXT NOT NULL, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) """ ) db.execute( """ CREATE TABLE IF NOT EXISTS message_reactions ( message_id INTEGER NOT NULL, user_id INTEGER NOT NULL, emoji TEXT NOT NULL, created_at 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) ON DELETE CASCADE ) """ ) db.execute( """ CREATE TABLE IF NOT EXISTS message_edits ( id INTEGER PRIMARY KEY AUTOINCREMENT, message_id INTEGER NOT NULL, edited_by_user_id INTEGER NOT NULL, previous_text TEXT NOT NULL, new_text TEXT NOT NULL, previous_attachments_json TEXT NOT NULL DEFAULT '[]', new_attachments_json TEXT NOT NULL DEFAULT '[]', created_at TEXT NOT NULL, FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE, FOREIGN KEY (edited_by_user_id) REFERENCES users(id) ON DELETE CASCADE ) """ ) db.execute( """ CREATE TABLE IF NOT EXISTS message_receipts ( message_id INTEGER NOT NULL, user_id INTEGER NOT NULL, seen_at TEXT NOT NULL, PRIMARY KEY (message_id, user_id), FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) """ ) db.execute( """ CREATE TABLE IF NOT EXISTS message_attachments ( message_id INTEGER NOT NULL, attachment_id INTEGER NOT NULL, position INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (message_id, attachment_id), FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE, FOREIGN KEY (attachment_id) REFERENCES attachments(id) ON DELETE CASCADE ) """ ) db.execute( """ CREATE TABLE IF NOT EXISTS presence_events ( user_id INTEGER PRIMARY KEY, updated_at REAL NOT NULL, view_name TEXT, room_id INTEGER, channel_id INTEGER, direct_conversation_id INTEGER, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) """ ) db.execute( """ CREATE TABLE IF NOT EXISTS call_sessions ( id INTEGER PRIMARY KEY AUTOINCREMENT, direct_conversation_id INTEGER NOT NULL, started_by_user_id INTEGER NOT NULL, status TEXT NOT NULL DEFAULT 'active', created_at TEXT NOT NULL, ended_at TEXT, FOREIGN KEY (direct_conversation_id) REFERENCES direct_conversations(id) ON DELETE CASCADE, FOREIGN KEY (started_by_user_id) REFERENCES users(id) ON DELETE CASCADE ) """ ) db.execute( """ CREATE TABLE IF NOT EXISTS call_signals ( id INTEGER PRIMARY KEY AUTOINCREMENT, call_session_id INTEGER NOT NULL, from_user_id INTEGER NOT NULL, signal_type TEXT NOT NULL, payload_json TEXT NOT NULL DEFAULT '{}', created_at TEXT NOT NULL, FOREIGN KEY (call_session_id) REFERENCES call_sessions(id) ON DELETE CASCADE, FOREIGN KEY (from_user_id) REFERENCES users(id) ON DELETE CASCADE ) """ ) db.execute( """ CREATE TABLE IF NOT EXISTS group_call_sessions ( id INTEGER PRIMARY KEY AUTOINCREMENT, scope_kind TEXT NOT NULL, scope_id INTEGER NOT NULL, scope_slug TEXT NOT NULL, helper_call_id TEXT NOT NULL UNIQUE, created_by_user_id INTEGER NOT NULL, status TEXT NOT NULL DEFAULT 'active', created_at TEXT NOT NULL, ended_at TEXT, FOREIGN KEY (created_by_user_id) REFERENCES users(id) ON DELETE CASCADE ) """ ) db.execute( """ CREATE TABLE IF NOT EXISTS group_call_invites ( id INTEGER PRIMARY KEY AUTOINCREMENT, group_call_session_id INTEGER NOT NULL, inviter_user_id INTEGER NOT NULL, invitee_user_id INTEGER NOT NULL, helper_token TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending', created_at TEXT NOT NULL, expires_at TEXT NOT NULL, responded_at TEXT, FOREIGN KEY (group_call_session_id) REFERENCES group_call_sessions(id) ON DELETE CASCADE, FOREIGN KEY (inviter_user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (invitee_user_id) REFERENCES users(id) ON DELETE CASCADE ) """ ) db.execute( """ CREATE TABLE IF NOT EXISTS group_call_participants ( group_call_session_id INTEGER NOT NULL, user_id INTEGER NOT NULL, joined_at TEXT NOT NULL, updated_at TEXT NOT NULL, left_at TEXT, PRIMARY KEY (group_call_session_id, user_id), FOREIGN KEY (group_call_session_id) REFERENCES group_call_sessions(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) """ ) if not column_exists(db, "messages", "room_id"): db.execute("ALTER TABLE messages ADD COLUMN room_id INTEGER") if not column_exists(db, "messages", "attachment_id"): db.execute("ALTER TABLE messages ADD COLUMN attachment_id INTEGER") if not column_exists(db, "messages", "parent_message_id"): db.execute("ALTER TABLE messages ADD COLUMN parent_message_id INTEGER") if not column_exists(db, "messages", "channel_id"): db.execute("ALTER TABLE messages ADD COLUMN channel_id INTEGER") if not column_exists(db, "messages", "direct_conversation_id"): db.execute("ALTER TABLE messages ADD COLUMN direct_conversation_id INTEGER") if not column_exists(db, "user_profiles", "badge_text"): db.execute("ALTER TABLE user_profiles ADD COLUMN badge_text TEXT") if not column_exists(db, "user_profiles", "status_message"): db.execute("ALTER TABLE user_profiles ADD COLUMN status_message TEXT NOT NULL DEFAULT ''") if not column_exists(db, "user_profiles", "banner_attachment_id"): db.execute("ALTER TABLE user_profiles ADD COLUMN banner_attachment_id INTEGER") if not column_exists(db, "user_settings", "name_color"): db.execute("ALTER TABLE user_settings ADD COLUMN name_color TEXT") if not column_exists(db, "user_settings", "profile_background_color"): db.execute("ALTER TABLE user_settings ADD COLUMN profile_background_color TEXT") if not column_exists(db, "user_settings", "profile_banner_color"): db.execute("ALTER TABLE user_settings ADD COLUMN profile_banner_color TEXT") if not column_exists(db, "user_settings", "profile_visibility"): db.execute("ALTER TABLE user_settings ADD COLUMN profile_visibility TEXT NOT NULL DEFAULT 'public'") if not column_exists(db, "user_settings", "last_active_visibility"): db.execute("ALTER TABLE user_settings ADD COLUMN last_active_visibility TEXT NOT NULL DEFAULT 'everyone'") if not column_exists(db, "user_settings", "presence_mode"): db.execute("ALTER TABLE user_settings ADD COLUMN presence_mode TEXT NOT NULL DEFAULT 'online'") if not column_exists(db, "user_settings", "dm_layout_mode"): db.execute("ALTER TABLE user_settings ADD COLUMN dm_layout_mode TEXT NOT NULL DEFAULT 'cards'") if not column_exists(db, "user_settings", "show_presence_badge"): db.execute("ALTER TABLE user_settings ADD COLUMN show_presence_badge INTEGER NOT NULL DEFAULT 1") if not column_exists(db, "presence_events", "direct_conversation_id"): db.execute("ALTER TABLE presence_events ADD COLUMN direct_conversation_id INTEGER") db.execute("CREATE INDEX IF NOT EXISTS idx_messages_room_id_id ON messages(room_id, id)") db.execute("CREATE INDEX IF NOT EXISTS idx_messages_channel_id_id ON messages(channel_id, id)") db.execute("CREATE INDEX IF NOT EXISTS idx_messages_dm_id_id ON messages(direct_conversation_id, id)") db.execute("CREATE INDEX IF NOT EXISTS idx_messages_parent_message_id_id ON messages(parent_message_id, id)") db.execute("CREATE INDEX IF NOT EXISTS idx_messages_attachment_id ON messages(attachment_id)") db.execute("CREATE INDEX IF NOT EXISTS idx_room_members_user ON room_members(user_id)") db.execute("CREATE INDEX IF NOT EXISTS idx_team_members_user_id ON team_members(user_id)") db.execute("CREATE INDEX IF NOT EXISTS idx_channels_team_id_slug ON channels(team_id, slug)") db.execute("CREATE INDEX IF NOT EXISTS idx_channels_legacy_room_id ON channels(legacy_room_id)") db.execute("CREATE INDEX IF NOT EXISTS idx_pages_team_updated_at ON pages(team_id, updated_at DESC)") db.execute("CREATE INDEX IF NOT EXISTS idx_channel_typing_events_updated_at ON channel_typing_events(channel_id, updated_at)") db.execute("CREATE INDEX IF NOT EXISTS idx_dm_typing_events_updated_at ON dm_typing_events(direct_conversation_id, updated_at)") db.execute("CREATE INDEX IF NOT EXISTS idx_invites_token_hash ON room_invites(token_hash)") db.execute("CREATE INDEX IF NOT EXISTS idx_team_invites_token_hash ON team_invites(token_hash)") db.execute("CREATE INDEX IF NOT EXISTS idx_typing_room_updated ON typing_events(room_id, updated_at)") db.execute("CREATE INDEX IF NOT EXISTS idx_blacklist_lookup ON blacklist_entries(kind, value_hash)") db.execute("CREATE INDEX IF NOT EXISTS idx_rate_limit_lookup ON rate_limit_events(scope, subject_hash, created_at)") db.execute("CREATE INDEX IF NOT EXISTS idx_message_reactions_message ON message_reactions(message_id, emoji)") db.execute("CREATE INDEX IF NOT EXISTS idx_message_receipts_user_seen ON message_receipts(user_id, seen_at)") db.execute("CREATE INDEX IF NOT EXISTS idx_message_attachments_message_position ON message_attachments(message_id, position)") db.execute("CREATE INDEX IF NOT EXISTS idx_presence_events_updated_at ON presence_events(updated_at)") db.execute("CREATE INDEX IF NOT EXISTS idx_presence_events_dm_id ON presence_events(direct_conversation_id, updated_at)") db.execute("CREATE INDEX IF NOT EXISTS idx_direct_conversation_members_user ON direct_conversation_members(user_id, direct_conversation_id)") db.execute("CREATE INDEX IF NOT EXISTS idx_call_signals_lookup ON call_signals(call_session_id, id)") db.execute("CREATE INDEX IF NOT EXISTS idx_user_settings_name_color ON user_settings(user_id, name_color)") db.execute("CREATE INDEX IF NOT EXISTS idx_group_call_sessions_scope ON group_call_sessions(scope_kind, scope_id, status)") db.execute("CREATE INDEX IF NOT EXISTS idx_group_call_invites_invitee_status ON group_call_invites(invitee_user_id, status, expires_at)") db.execute("CREATE INDEX IF NOT EXISTS idx_group_call_participants_session_active ON group_call_participants(group_call_session_id, left_at)") db.execute("CREATE INDEX IF NOT EXISTS idx_group_call_participants_user_active ON group_call_participants(user_id, left_at)") db.execute("CREATE INDEX IF NOT EXISTS idx_group_call_participants_session_updated ON group_call_participants(group_call_session_id, updated_at)") general = db.execute("SELECT id FROM rooms WHERE slug = 'general'").fetchone() if not general: cursor = db.execute( """ INSERT INTO rooms ( slug, name, description, visibility, created_at, is_active ) VALUES (?, ?, ?, ?, ?, 1) """, ("general", "general", "Default public chat room", "public", now_iso()), ) general_id = cursor.lastrowid else: general_id = general["id"] db.execute( "UPDATE messages SET room_id = ? WHERE room_id IS NULL AND direct_conversation_id IS NULL", (general_id,), ) db.execute( """ UPDATE users SET role = CASE WHEN is_admin = 1 THEN 'admin' ELSE 'user' END WHERE role IS NULL OR role = '' """ ) db.execute( """ UPDATE users SET status = CASE WHEN is_active = 1 THEN 'active' ELSE 'disabled' END WHERE status IS NULL OR status = '' """ ) general_team = db.execute("SELECT id FROM teams WHERE slug = 'general'").fetchone() if general_team: general_team_id = general_team["id"] else: cursor = db.execute( """ INSERT INTO teams ( slug, name, description, created_by_user_id, created_at, is_active ) VALUES (?, ?, ?, NULL, ?, 1) """, ("general", "General", "Default team for existing chat rooms.", now_iso()), ) general_team_id = cursor.lastrowid legacy_rooms = db.execute( """ SELECT id, slug, name, description, created_by_user_id, created_at FROM rooms WHERE is_active = 1 """ ).fetchall() for room in legacy_rooms: existing_channel = db.execute( "SELECT id FROM channels WHERE legacy_room_id = ?", (room["id"],), ).fetchone() if existing_channel: continue db.execute( """ INSERT INTO channels ( team_id, legacy_room_id, slug, name, description, created_by_user_id, created_at, is_active ) VALUES (?, ?, ?, ?, ?, ?, ?, 1) """, ( general_team_id, room["id"], room["slug"], room["name"], room["description"], room["created_by_user_id"], room["created_at"], ), ) db.execute( """ UPDATE messages SET channel_id = ( SELECT c.id FROM channels AS c WHERE c.legacy_room_id = messages.room_id LIMIT 1 ) WHERE channel_id IS NULL AND room_id IS NOT NULL AND direct_conversation_id IS NULL """ ) admin_users = db.execute( """ SELECT id FROM users WHERE role = 'admin' AND status = 'active' """ ).fetchall() for admin_user in admin_users: db.execute( """ INSERT OR IGNORE INTO team_members ( team_id, user_id, membership_role, added_at ) VALUES (?, ?, 'admin', ?) """, (general_team_id, admin_user["id"], now_iso()), ) db.execute( """ INSERT OR IGNORE INTO user_profiles ( user_id, display_name, badge_text, bio, bio_mode, website, location, status_message, banner_attachment_id, updated_at ) SELECT id, username, '', '', 'txt', '', '', '', NULL, ? FROM users """, (now_iso(),), ) db.execute( """ INSERT OR IGNORE INTO user_settings ( user_id, accent_color, name_color, custom_css, theme_mode, profile_background_color, profile_banner_color, profile_visibility, last_active_visibility, presence_mode, dm_layout_mode, show_presence_badge, updated_at ) SELECT id, '#f7dce8', NULL, '', 'dark', NULL, NULL, 'public', 'everyone', 'online', 'cards', 1, ? FROM users """, (now_iso(),), ) existing_message_attachment_counts = db.execute( "SELECT COUNT(*) AS count FROM message_attachments" ).fetchone()["count"] if existing_message_attachment_counts == 0: rows = db.execute( """ SELECT id, attachment_id FROM messages WHERE attachment_id IS NOT NULL ORDER BY id ASC """ ).fetchall() for row in rows: db.execute( """ INSERT OR IGNORE INTO message_attachments ( message_id, attachment_id, position ) VALUES (?, ?, 0) """, (row["id"], row["attachment_id"]), ) db.commit() def bootstrap_admin() -> None: username = os.environ.get("ADMIN_USERNAME", "").strip().lower() password = os.environ.get("ADMIN_PASSWORD", "") if not username or not password: return with get_db() as db: existing = db.execute( "SELECT id FROM users WHERE username = ?", (username,), ).fetchone() if existing: db.execute( """ UPDATE users SET role = 'admin', status = 'active', is_admin = 1, is_active = 1 WHERE id = ? """, (existing["id"],), ) user_id = existing["id"] else: cursor = db.execute( """ INSERT INTO users ( username, password_hash, role, status, is_admin, is_active, created_at ) VALUES (?, ?, 'admin', 'active', 1, 1, ?) """, (username, generate_password_hash(password), now_iso()), ) user_id = cursor.lastrowid db.execute( """ INSERT OR IGNORE INTO user_profiles ( user_id, display_name, badge_text, bio, bio_mode, website, location, status_message, banner_attachment_id, updated_at ) VALUES (?, ?, '', '', 'txt', '', '', '', NULL, ?) """, (user_id, username, now_iso()), ) db.execute( """ INSERT OR IGNORE INTO user_settings ( user_id, accent_color, name_color, custom_css, theme_mode, profile_background_color, profile_banner_color, profile_visibility, last_active_visibility, presence_mode, dm_layout_mode, show_presence_badge, updated_at ) VALUES (?, '#f7dce8', NULL, '', 'dark', NULL, NULL, 'public', 'everyone', 'online', 'cards', 1, ?) """, (user_id, now_iso()), ) db.commit()