From 02ecabf0ea32cd483352442b12f239e29549454e Mon Sep 17 00:00:00 2001 From: dev Date: Fri, 30 Oct 2026 01:02:31 +0000 Subject: [PATCH] Update app.py --- app.py | 407 ++++++++++++++++++--------------------------------------- 1 file changed, 126 insertions(+), 281 deletions(-) diff --git a/app.py b/app.py index 2eb587e..1b17ee5 100644 --- a/app.py +++ b/app.py @@ -24,8 +24,10 @@ from features import maybe_cleanup_orphaned_attachments from routes import register_routes APP_ROOT = Path(__file__).resolve().parent + DB_PATH = os.environ.get("CHAT_DB_PATH", str(APP_ROOT / "chat.db")) UPLOAD_DIR = Path(os.environ.get("CHAT_UPLOAD_DIR", str(APP_ROOT / "uploads"))) + SECRET_KEY_PATH = os.environ.get( "CHAT_SECRET_KEY_PATH", str(Path(DB_PATH).resolve().parent / "flask_secret.key"), @@ -59,20 +61,18 @@ MAX_LOCATION_LENGTH = 120 MAX_WEBSITE_LENGTH = 300 MAX_CUSTOM_CSS_LENGTH = 20000 MAX_PROFILE_BADGE_LENGTH = 48 -MAX_STATUS_MESSAGE_LENGTH = 160 MAX_OWNED_TEAMS_PER_USER = 3 MAX_MESSAGE_EDIT_WINDOW_SECONDS = 15 * 60 MAX_SEARCH_QUERY_LENGTH = 120 MAX_SEARCH_RESULTS = 100 READ_RECEIPT_BATCH_LIMIT = 200 -MAX_PAGE_COMMENT_LENGTH = 3000 -MAX_PAGE_SHARE_TTL_SECONDS = int(os.environ.get("MAX_PAGE_SHARE_TTL_SECONDS", "604800")) +GROUP_CALL_PARTICIPANT_STALE_SECONDS = 45 + PRIVATE_ACCESS_KEY = os.environ.get("PRIVATE_ACCESS_KEY", "").strip() + CALLS_BASE = os.environ.get("CALLS_BASE", "https://calls.example.com").rstrip("/") CALL_HELPER_SHARED_SECRET = os.environ.get("CALL_HELPER_SHARED_SECRET", "").strip() CALL_TOKEN_TTL_SECONDS = int(os.environ.get("CALL_TOKEN_TTL_SECONDS", "60")) -SHARE_TOKEN_TTL_SECONDS = int(os.environ.get("SHARE_TOKEN_TTL_SECONDS", "86400")) -SCHEDULED_MESSAGE_POLL_SECONDS = int(os.environ.get("SCHEDULED_MESSAGE_POLL_SECONDS", "10")) ALLOWED_ATTACHMENT_EXTENSIONS = { "txt", @@ -94,6 +94,7 @@ ALLOWED_ATTACHMENT_EXTENSIONS = { "m4v", "ogv", } + ALLOWED_ATTACHMENT_MIME_TYPES = { "text/plain", "application/pdf", @@ -114,8 +115,10 @@ ALLOWED_ATTACHMENT_MIME_TYPES = { "video/quicktime", "video/ogg", } + USERNAME_RE = re.compile(r"^[a-z0-9](?:[a-z0-9._-]{1,28}[a-z0-9])?$") ROOM_SLUG_RE = re.compile(r"^[a-z0-9](?:[a-z0-9-]{0,46}[a-z0-9])?$") + ROLES = {"user", "moderator", "admin"} STATUSES = {"active", "disabled", "restricted", "blacklisted"} ROOM_VISIBILITIES = {"public", "private"} @@ -130,13 +133,6 @@ RATE_LIMITS = { "reaction": (120, 60), "profile": (30, 60), "settings": (30, 60), - "share": (60, 60), - "bookmark": (120, 60), - "pin": (120, 60), - "schedule": (60, 60), - "page_comment": (60, 60), - "page_share": (60, 60), - "page_restore": (30, 60), } PLACEHOLDER_SECRETS = { @@ -150,7 +146,6 @@ PLACEHOLDER_SECRETS = { _runtime_state = { "last_attachment_cleanup": 0.0, - "last_scheduled_dispatch": 0.0, } @@ -178,12 +173,15 @@ def load_or_create_key(path_value: str, env_value: str, min_length: int = 32) -> configured = os.environ.get(env_value, "").strip() if configured and configured.lower() not in PLACEHOLDER_SECRETS and len(configured) >= min_length: return configured + path = Path(path_value) path.parent.mkdir(parents=True, exist_ok=True) + if path.exists(): existing = path.read_text(encoding="utf-8").strip() if len(existing) >= min_length: return existing + generated = secrets.token_urlsafe(64) try: path.write_text(generated, encoding="utf-8") @@ -232,6 +230,7 @@ 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") @@ -586,12 +585,7 @@ MESSAGE_SELECT = """ a.mime_type AS attachment_mime_type, a.size_bytes AS attachment_size_bytes, u.role AS user_role, - us.name_color AS user_name_color, - EXISTS ( - SELECT 1 - FROM message_pins mp - WHERE mp.message_id = m.id - ) AS is_pinned + us.name_color AS user_name_color FROM messages AS m JOIN users AS u ON u.id = m.user_id @@ -608,7 +602,6 @@ def team_page_select() -> str: p.id, p.team_id, p.channel_id, - p.parent_page_id, p.slug, p.title, p.body, @@ -622,16 +615,12 @@ def team_page_select() -> str: t.slug AS team_slug, t.name AS team_name, c.slug AS channel_slug, - c.name AS channel_name, - parent.slug AS parent_page_slug, - parent.title AS parent_page_title + c.name AS channel_name FROM pages AS p JOIN teams AS t ON t.id = p.team_id LEFT JOIN channels AS c ON c.id = p.channel_id - LEFT JOIN pages AS parent - ON parent.id = p.parent_page_id """ @@ -779,6 +768,7 @@ def check_rate_limit(scope: str) -> Optional[int]: now = time.time() cutoff = now - window_seconds subject_hash = current_client_hash() + with get_db() as db: db.execute("DELETE FROM rate_limit_events WHERE created_at < ?", (cutoff - 60,)) rows = db.execute( @@ -792,9 +782,11 @@ def check_rate_limit(scope: str) -> Optional[int]: """, (scope, subject_hash, cutoff), ).fetchall() + if len(rows) >= limit: oldest = rows[0]["created_at"] return max(1, int(window_seconds - (now - oldest)) + 1) + db.execute( """ INSERT INTO rate_limit_events ( @@ -806,6 +798,7 @@ def check_rate_limit(scope: str) -> Optional[int]: (scope, subject_hash, now), ) db.commit() + return None @@ -851,7 +844,7 @@ def csrf_valid() -> bool: return bool(supplied and expected and hmac.compare_digest(supplied, expected)) -def user_payload(row: sqlite3.Row, settings_row: Optional[sqlite3.Row] = None) -> dict: +def user_payload(row: sqlite3.Row) -> dict: status = row["status"] restricted_until = parse_iso(row["restricted_until"]) if status == "restricted" and restricted_until and restricted_until <= utc_now(): @@ -862,7 +855,6 @@ def user_payload(row: sqlite3.Row, settings_row: Optional[sqlite3.Row] = None) - "role": row["role"], "status": status, "restricted_until": row["restricted_until"], - "presence_mode": settings_row["presence_mode"] if settings_row and "presence_mode" in settings_row.keys() else "online", } @@ -870,6 +862,7 @@ def current_user() -> Optional[dict]: user_id = session.get("user_id") if not user_id: return None + with get_db() as db: row = db.execute( """ @@ -884,19 +877,13 @@ def current_user() -> Optional[dict]: """, (user_id,), ).fetchone() - settings_row = db.execute( - """ - SELECT presence_mode - FROM user_settings - WHERE user_id = ? - """, - (user_id,), - ).fetchone() + if not row: session.clear() return None - user = user_payload(row, settings_row) + user = user_payload(row) + if user["status"] == "restricted": expires = parse_iso(user["restricted_until"]) if expires and expires <= utc_now(): @@ -918,6 +905,7 @@ def current_user() -> Optional[dict]: if user["status"] in {"disabled", "blacklisted"}: session.clear() return None + return user @@ -932,7 +920,6 @@ def login_user(row: sqlite3.Row | dict) -> dict: "role": row["role"], "status": row["status"], "restricted_until": row.get("restricted_until") if isinstance(row, dict) else row["restricted_until"], - "presence_mode": row.get("presence_mode", "online") if isinstance(row, dict) else "online", } @@ -992,7 +979,7 @@ def setup_request_security(): if request_blacklisted(): if request.path.startswith("/api/"): return jsonify({"error": "Access to this service has been blocked.", "code": "blacklisted"}), 403 - return render_template("index.html", asset_version=get_asset_version()), 403 + return render_template("blocked.html", asset_version=get_asset_version()), 403 @app.context_processor @@ -1040,6 +1027,7 @@ def sqlite_error(error): def init_db() -> None: UPLOAD_DIR.mkdir(parents=True, exist_ok=True) + with get_db() as db: db.execute( """ @@ -1058,6 +1046,7 @@ def init_db() -> None: ) """ ) + db.execute( """ CREATE TABLE IF NOT EXISTS rooms ( @@ -1073,6 +1062,7 @@ def init_db() -> None: ) """ ) + db.execute( """ CREATE TABLE IF NOT EXISTS room_members ( @@ -1086,6 +1076,7 @@ def init_db() -> None: ) """ ) + db.execute( """ CREATE TABLE IF NOT EXISTS attachments ( @@ -1100,6 +1091,7 @@ def init_db() -> None: ) """ ) + db.execute( """ CREATE TABLE IF NOT EXISTS teams ( @@ -1114,6 +1106,7 @@ def init_db() -> None: ) """ ) + db.execute( """ CREATE TABLE IF NOT EXISTS team_members ( @@ -1127,6 +1120,7 @@ def init_db() -> None: ) """ ) + db.execute( """ CREATE TABLE IF NOT EXISTS team_invites ( @@ -1145,6 +1139,7 @@ def init_db() -> None: ) """ ) + db.execute( """ CREATE TABLE IF NOT EXISTS channels ( @@ -1164,27 +1159,7 @@ def init_db() -> None: ) """ ) - 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 ( @@ -1207,6 +1182,7 @@ def init_db() -> None: ) """ ) + db.execute( """ CREATE TABLE IF NOT EXISTS room_invites ( @@ -1224,6 +1200,7 @@ def init_db() -> None: ) """ ) + db.execute( """ CREATE TABLE IF NOT EXISTS typing_events ( @@ -1236,6 +1213,7 @@ def init_db() -> None: ) """ ) + db.execute( """ CREATE TABLE IF NOT EXISTS channel_typing_events ( @@ -1248,6 +1226,7 @@ def init_db() -> None: ) """ ) + db.execute( """ CREATE TABLE IF NOT EXISTS dm_typing_events ( @@ -1260,6 +1239,7 @@ def init_db() -> None: ) """ ) + db.execute( """ CREATE TABLE IF NOT EXISTS blacklist_entries ( @@ -1275,6 +1255,7 @@ def init_db() -> None: ) """ ) + db.execute( """ CREATE TABLE IF NOT EXISTS rate_limit_events ( @@ -1285,6 +1266,7 @@ def init_db() -> None: ) """ ) + db.execute( """ CREATE TABLE IF NOT EXISTS audit_log ( @@ -1300,13 +1282,13 @@ def init_db() -> None: ) """ ) + db.execute( """ CREATE TABLE IF NOT EXISTS pages ( id INTEGER PRIMARY KEY AUTOINCREMENT, team_id INTEGER NOT NULL, channel_id INTEGER, - parent_page_id INTEGER, slug TEXT COLLATE NOCASE NOT NULL, title TEXT NOT NULL, body TEXT NOT NULL DEFAULT '', @@ -1320,63 +1302,12 @@ def init_db() -> None: 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 (parent_page_id) REFERENCES pages(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 page_comments ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - page_id INTEGER NOT NULL, - user_id INTEGER NOT NULL, - username TEXT NOT NULL, - body TEXT NOT NULL, - created_at TEXT NOT NULL, - updated_at TEXT, - deleted_at TEXT, - FOREIGN KEY (page_id) REFERENCES pages(id) ON DELETE CASCADE, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE - ) - """ - ) - db.execute( - """ - CREATE TABLE IF NOT EXISTS page_versions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - page_id INTEGER NOT NULL, - version_number INTEGER NOT NULL, - title TEXT NOT NULL, - body TEXT NOT NULL, - visibility TEXT NOT NULL, - page_type TEXT NOT NULL, - channel_id INTEGER, - parent_page_id INTEGER, - created_by_user_id INTEGER NOT NULL, - created_at TEXT NOT NULL, - UNIQUE(page_id, version_number), - FOREIGN KEY (page_id) REFERENCES pages(id) ON DELETE CASCADE, - FOREIGN KEY (created_by_user_id) REFERENCES users(id) ON DELETE RESTRICT - ) - """ - ) - db.execute( - """ - CREATE TABLE IF NOT EXISTS page_shares ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - page_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, - revoked_at TEXT, - FOREIGN KEY (page_id) REFERENCES pages(id) ON DELETE CASCADE, - FOREIGN KEY (created_by_user_id) REFERENCES users(id) ON DELETE CASCADE - ) - """ - ) + db.execute( """ CREATE TABLE IF NOT EXISTS user_profiles ( @@ -1387,14 +1318,12 @@ def init_db() -> None: bio_mode TEXT NOT NULL DEFAULT 'txt', website TEXT, location TEXT, - status_message TEXT, - 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 + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) """ ) + db.execute( """ CREATE TABLE IF NOT EXISTS user_settings ( @@ -1403,16 +1332,12 @@ def init_db() -> None: 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', updated_at TEXT NOT NULL, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) """ ) + db.execute( """ CREATE TABLE IF NOT EXISTS message_reactions ( @@ -1426,6 +1351,7 @@ def init_db() -> None: ) """ ) + db.execute( """ CREATE TABLE IF NOT EXISTS message_edits ( @@ -1442,6 +1368,7 @@ def init_db() -> None: ) """ ) + db.execute( """ CREATE TABLE IF NOT EXISTS message_receipts ( @@ -1454,6 +1381,7 @@ def init_db() -> None: ) """ ) + db.execute( """ CREATE TABLE IF NOT EXISTS message_attachments ( @@ -1466,6 +1394,7 @@ def init_db() -> None: ) """ ) + db.execute( """ CREATE TABLE IF NOT EXISTS presence_events ( @@ -1479,6 +1408,30 @@ def init_db() -> None: ) """ ) + + 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 call_sessions ( @@ -1493,6 +1446,7 @@ def init_db() -> None: ) """ ) + db.execute( """ CREATE TABLE IF NOT EXISTS call_signals ( @@ -1507,6 +1461,7 @@ def init_db() -> None: ) """ ) + db.execute( """ CREATE TABLE IF NOT EXISTS group_call_sessions ( @@ -1523,6 +1478,7 @@ def init_db() -> None: ) """ ) + db.execute( """ CREATE TABLE IF NOT EXISTS group_call_invites ( @@ -1541,81 +1497,21 @@ def init_db() -> None: ) """ ) + db.execute( """ - CREATE TABLE IF NOT EXISTS message_pins ( - message_id INTEGER PRIMARY KEY, - pinned_by_user_id INTEGER NOT NULL, - pinned_at TEXT NOT NULL, - FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE, - FOREIGN KEY (pinned_by_user_id) REFERENCES users(id) ON DELETE CASCADE - ) - """ - ) - db.execute( - """ - CREATE TABLE IF NOT EXISTS scheduled_messages ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - room_id INTEGER, - channel_id INTEGER, - direct_conversation_id INTEGER, + CREATE TABLE IF NOT EXISTS group_call_participants ( + group_call_session_id INTEGER NOT NULL, user_id INTEGER NOT NULL, - username TEXT NOT NULL, - text TEXT NOT NULL, - attachment_ids_json TEXT NOT NULL DEFAULT '[]', - deliver_at TEXT NOT NULL, - created_at TEXT NOT NULL, - sent_at TEXT, - cancelled_at TEXT, - 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, + 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 ) """ ) - db.execute( - """ - CREATE TABLE IF NOT EXISTS message_shares ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - message_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, - revoked_at TEXT, - FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE, - FOREIGN KEY (created_by_user_id) REFERENCES users(id) ON DELETE CASCADE - ) - """ - ) - db.execute( - """ - CREATE TABLE IF NOT EXISTS attachment_shares ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - attachment_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, - revoked_at TEXT, - FOREIGN KEY (attachment_id) REFERENCES attachments(id) ON DELETE CASCADE, - FOREIGN KEY (created_by_user_id) REFERENCES users(id) ON DELETE CASCADE - ) - """ - ) - db.execute( - """ - CREATE TABLE IF NOT EXISTS message_bookmarks ( - user_id INTEGER NOT NULL, - message_id INTEGER NOT NULL, - created_at TEXT NOT NULL, - PRIMARY KEY (user_id, message_id), - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, - FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE - ) - """ - ) if not column_exists(db, "messages", "room_id"): db.execute("ALTER TABLE messages ADD COLUMN room_id INTEGER") @@ -1629,26 +1525,10 @@ def init_db() -> None: db.execute("ALTER TABLE messages ADD COLUMN direct_conversation_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, "presence_events", "direct_conversation_id"): db.execute("ALTER TABLE presence_events 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") - 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, "pages", "parent_page_id"): - db.execute("ALTER TABLE pages ADD COLUMN parent_page_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)") @@ -1660,10 +1540,6 @@ def init_db() -> None: 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_pages_parent_page_id ON pages(parent_page_id)") - db.execute("CREATE INDEX IF NOT EXISTS idx_page_comments_page_created_at ON page_comments(page_id, created_at ASC)") - db.execute("CREATE INDEX IF NOT EXISTS idx_page_versions_page_version ON page_versions(page_id, version_number DESC)") - db.execute("CREATE INDEX IF NOT EXISTS idx_page_shares_token_hash ON page_shares(token_hash)") 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)") @@ -1681,11 +1557,9 @@ def init_db() -> None: 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_message_pins_pinned_at ON message_pins(pinned_at DESC)") - db.execute("CREATE INDEX IF NOT EXISTS idx_scheduled_messages_deliver_at ON scheduled_messages(deliver_at, sent_at, cancelled_at)") - db.execute("CREATE INDEX IF NOT EXISTS idx_message_shares_token_hash ON message_shares(token_hash)") - db.execute("CREATE INDEX IF NOT EXISTS idx_attachment_shares_token_hash ON attachment_shares(token_hash)") - db.execute("CREATE INDEX IF NOT EXISTS idx_message_bookmarks_user_created_at ON message_bookmarks(user_id, created_at DESC)") + 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: @@ -1722,6 +1596,7 @@ def init_db() -> None: OR role = '' """ ) + db.execute( """ UPDATE users @@ -1766,6 +1641,7 @@ def init_db() -> None: WHERE is_active = 1 """ ).fetchall() + for room in legacy_rooms: existing_channel = db.execute( "SELECT id FROM channels WHERE legacy_room_id = ?", @@ -1820,6 +1696,7 @@ def init_db() -> None: AND status = 'active' """ ).fetchall() + for admin_user in admin_users: db.execute( """ @@ -1843,8 +1720,6 @@ def init_db() -> None: bio_mode, website, location, - status_message, - banner_attachment_id, updated_at ) SELECT @@ -1855,13 +1730,12 @@ def init_db() -> None: 'txt', '', '', - '', - NULL, ? FROM users """, (now_iso(),), ) + db.execute( """ INSERT OR IGNORE INTO user_settings ( @@ -1870,11 +1744,6 @@ def init_db() -> None: name_color, custom_css, theme_mode, - profile_background_color, - profile_banner_color, - profile_visibility, - last_active_visibility, - presence_mode, updated_at ) SELECT @@ -1883,11 +1752,6 @@ def init_db() -> None: NULL, '', 'dark', - NULL, - NULL, - 'public', - 'everyone', - 'online', ? FROM users """, @@ -1897,6 +1761,7 @@ def init_db() -> None: existing_message_attachment_counts = db.execute( "SELECT COUNT(*) AS count FROM message_attachments" ).fetchone()["count"] + if existing_message_attachment_counts == 0: rows = db.execute( """ @@ -1924,16 +1789,20 @@ def init_db() -> None: def bootstrap_admin() -> None: username = normalize_username(os.environ.get("ADMIN_USERNAME", "")) password = os.environ.get("ADMIN_PASSWORD", "") + if not username or not password: return + if validate_username(username) or validate_password(password, username): print("WARNING: Admin bootstrap credentials failed validation.") return + with get_db() as db: existing = db.execute( "SELECT id FROM users WHERE username = ?", (username,), ).fetchone() + if existing: db.execute( """ @@ -1964,6 +1833,7 @@ def bootstrap_admin() -> None: (username, generate_password_hash(password), now_iso()), ) user_id = cursor.lastrowid + db.execute( """ INSERT OR IGNORE INTO user_profiles ( @@ -1974,13 +1844,12 @@ def bootstrap_admin() -> None: bio_mode, website, location, - status_message, - banner_attachment_id, updated_at - ) VALUES (?, ?, '', '', 'txt', '', '', '', NULL, ?) + ) VALUES (?, ?, '', '', 'txt', '', '', ?) """, (user_id, username, now_iso()), ) + db.execute( """ INSERT OR IGNORE INTO user_settings ( @@ -1989,16 +1858,12 @@ def bootstrap_admin() -> None: name_color, custom_css, theme_mode, - profile_background_color, - profile_banner_color, - profile_visibility, - last_active_visibility, - presence_mode, updated_at - ) VALUES (?, '#f7dce8', NULL, '', 'dark', NULL, NULL, 'public', 'everyone', 'online', ?) + ) VALUES (?, '#f7dce8', NULL, '', 'dark', ?) """, (user_id, now_iso()), ) + db.commit() @@ -2026,11 +1891,14 @@ def api_me(): @app.route("/api/register", methods=["POST"]) def register(): + limited = rate_limited("register") if limited: return limited + if not csrf_valid(): return jsonify({"error": "Security token expired.", "code": "csrf"}), 403 + try: data = safe_json() except ValueError as error: @@ -2042,8 +1910,10 @@ def register(): username = normalize_username(data.get("username")) password = data.get("password", "") + username_error = validate_username(username) password_error = validate_password(password, username) + if username_error: return jsonify({"error": username_error}), 400 if password_error: @@ -2073,8 +1943,8 @@ def register(): "role": "user", "status": "active", "restricted_until": None, - "presence_mode": "online", } + db_write( """ INSERT OR IGNORE INTO user_profiles ( @@ -2085,13 +1955,12 @@ def register(): bio_mode, website, location, - status_message, - banner_attachment_id, updated_at - ) VALUES (?, ?, '', '', 'txt', '', '', '', NULL, ?) + ) VALUES (?, ?, '', '', 'txt', '', '', ?) """, (user["id"], username, now_iso()), ) + db_write( """ INSERT OR IGNORE INTO user_settings ( @@ -2100,19 +1969,15 @@ def register(): name_color, custom_css, theme_mode, - profile_background_color, - profile_banner_color, - profile_visibility, - last_active_visibility, - presence_mode, updated_at - ) VALUES (?, '#f7dce8', NULL, '', 'dark', NULL, NULL, 'public', 'everyone', 'online', ?) + ) VALUES (?, '#f7dce8', NULL, '', 'dark', ?) """, (user["id"], now_iso()), ) authenticated = login_user(user) audit("account_registered", user["id"], "user", str(user["id"])) + return jsonify( { "status": "registered", @@ -2127,6 +1992,7 @@ def login(): limited = rate_limited("login") if limited: return limited + if not csrf_valid(): return jsonify({"error": "Security token expired.", "code": "csrf"}), 403 @@ -2146,29 +2012,28 @@ def login(): row = db.execute( """ SELECT - u.id, - u.username, - u.password_hash, - u.role, - u.status, - u.restricted_until, - us.presence_mode - FROM users u - LEFT JOIN user_settings us - ON us.user_id = u.id - WHERE u.username = ? + id, + username, + password_hash, + role, + status, + restricted_until + FROM users + WHERE username = ? """, (username,), ).fetchone() if not row or not isinstance(password, str) or not check_password_hash(row["password_hash"], password): return jsonify({"error": "Invalid username or password."}), 401 + if row["status"] in {"disabled", "blacklisted"}: return jsonify({"error": "Invalid username or password."}), 401 db_write("UPDATE users SET last_login_at = ? WHERE id = ?", (now_iso(), row["id"])) - authenticated = login_user(dict(row)) + authenticated = login_user(row) audit("account_logged_in", row["id"], "user", str(row["id"])) + return jsonify( { "status": "logged_in", @@ -2187,12 +2052,10 @@ def logout(): def _maybe_cleanup_orphaned_attachments(): - from features import cleanup_orphaned_attachments - maybe_cleanup_orphaned_attachments( _runtime_state, ATTACHMENT_CLEANUP_INTERVAL_SECONDS, - lambda: cleanup_orphaned_attachments( + lambda: __import__("features").cleanup_orphaned_attachments( get_db, UPLOAD_DIR, utc_now, @@ -2203,20 +2066,6 @@ def _maybe_cleanup_orphaned_attachments(): ) -def _maybe_dispatch_scheduled_messages(): - from features import dispatch_scheduled_messages - - now_ts = time.time() - last = _runtime_state.get("last_scheduled_dispatch", 0.0) - if now_ts - last < SCHEDULED_MESSAGE_POLL_SECONDS: - return - _runtime_state["last_scheduled_dispatch"] = now_ts - try: - dispatch_scheduled_messages(get_db, now_iso, app.logger) - except Exception: - app.logger.exception("Could not dispatch scheduled messages.") - - register_routes( app, { @@ -2275,9 +2124,6 @@ register_routes( "html": html, "attachment_list_for_message": lambda message_id: __import__("features").attachment_list_for_message(get_db, message_id), "maybe_cleanup_orphaned_attachments": _maybe_cleanup_orphaned_attachments, - "maybe_dispatch_scheduled_messages": _maybe_dispatch_scheduled_messages, - "SHARE_TOKEN_TTL_SECONDS": SHARE_TOKEN_TTL_SECONDS, - "MAX_PAGE_SHARE_TTL_SECONDS": MAX_PAGE_SHARE_TTL_SECONDS, "constants": { "MAX_MESSAGE_LENGTH": MAX_MESSAGE_LENGTH, "MAX_HISTORY_MESSAGES": MAX_HISTORY_MESSAGES, @@ -2302,12 +2148,11 @@ register_routes( "MAX_WEBSITE_LENGTH": MAX_WEBSITE_LENGTH, "MAX_CUSTOM_CSS_LENGTH": MAX_CUSTOM_CSS_LENGTH, "MAX_PROFILE_BADGE_LENGTH": MAX_PROFILE_BADGE_LENGTH, - "MAX_STATUS_MESSAGE_LENGTH": MAX_STATUS_MESSAGE_LENGTH, "MAX_OWNED_TEAMS_PER_USER": MAX_OWNED_TEAMS_PER_USER, - "MAX_PAGE_COMMENT_LENGTH": MAX_PAGE_COMMENT_LENGTH, "ROOM_VISIBILITIES": ROOM_VISIBILITIES, "ROLES": ROLES, "STATUSES": STATUSES, + "GROUP_CALL_PARTICIPANT_STALE_SECONDS": GROUP_CALL_PARTICIPANT_STALE_SECONDS, }, }, )