Update app.py

This commit is contained in:
dev 2026-10-30 01:02:31 +00:00
parent 11b6848099
commit 02ecabf0ea

407
app.py
View File

@ -24,8 +24,10 @@ from features import maybe_cleanup_orphaned_attachments
from routes import register_routes from routes import register_routes
APP_ROOT = Path(__file__).resolve().parent APP_ROOT = Path(__file__).resolve().parent
DB_PATH = os.environ.get("CHAT_DB_PATH", str(APP_ROOT / "chat.db")) 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"))) UPLOAD_DIR = Path(os.environ.get("CHAT_UPLOAD_DIR", str(APP_ROOT / "uploads")))
SECRET_KEY_PATH = os.environ.get( SECRET_KEY_PATH = os.environ.get(
"CHAT_SECRET_KEY_PATH", "CHAT_SECRET_KEY_PATH",
str(Path(DB_PATH).resolve().parent / "flask_secret.key"), str(Path(DB_PATH).resolve().parent / "flask_secret.key"),
@ -59,20 +61,18 @@ MAX_LOCATION_LENGTH = 120
MAX_WEBSITE_LENGTH = 300 MAX_WEBSITE_LENGTH = 300
MAX_CUSTOM_CSS_LENGTH = 20000 MAX_CUSTOM_CSS_LENGTH = 20000
MAX_PROFILE_BADGE_LENGTH = 48 MAX_PROFILE_BADGE_LENGTH = 48
MAX_STATUS_MESSAGE_LENGTH = 160
MAX_OWNED_TEAMS_PER_USER = 3 MAX_OWNED_TEAMS_PER_USER = 3
MAX_MESSAGE_EDIT_WINDOW_SECONDS = 15 * 60 MAX_MESSAGE_EDIT_WINDOW_SECONDS = 15 * 60
MAX_SEARCH_QUERY_LENGTH = 120 MAX_SEARCH_QUERY_LENGTH = 120
MAX_SEARCH_RESULTS = 100 MAX_SEARCH_RESULTS = 100
READ_RECEIPT_BATCH_LIMIT = 200 READ_RECEIPT_BATCH_LIMIT = 200
MAX_PAGE_COMMENT_LENGTH = 3000 GROUP_CALL_PARTICIPANT_STALE_SECONDS = 45
MAX_PAGE_SHARE_TTL_SECONDS = int(os.environ.get("MAX_PAGE_SHARE_TTL_SECONDS", "604800"))
PRIVATE_ACCESS_KEY = os.environ.get("PRIVATE_ACCESS_KEY", "").strip() PRIVATE_ACCESS_KEY = os.environ.get("PRIVATE_ACCESS_KEY", "").strip()
CALLS_BASE = os.environ.get("CALLS_BASE", "https://calls.example.com").rstrip("/") 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_HELPER_SHARED_SECRET = os.environ.get("CALL_HELPER_SHARED_SECRET", "").strip()
CALL_TOKEN_TTL_SECONDS = int(os.environ.get("CALL_TOKEN_TTL_SECONDS", "60")) 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 = { ALLOWED_ATTACHMENT_EXTENSIONS = {
"txt", "txt",
@ -94,6 +94,7 @@ ALLOWED_ATTACHMENT_EXTENSIONS = {
"m4v", "m4v",
"ogv", "ogv",
} }
ALLOWED_ATTACHMENT_MIME_TYPES = { ALLOWED_ATTACHMENT_MIME_TYPES = {
"text/plain", "text/plain",
"application/pdf", "application/pdf",
@ -114,8 +115,10 @@ ALLOWED_ATTACHMENT_MIME_TYPES = {
"video/quicktime", "video/quicktime",
"video/ogg", "video/ogg",
} }
USERNAME_RE = re.compile(r"^[a-z0-9](?:[a-z0-9._-]{1,28}[a-z0-9])?$") 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])?$") ROOM_SLUG_RE = re.compile(r"^[a-z0-9](?:[a-z0-9-]{0,46}[a-z0-9])?$")
ROLES = {"user", "moderator", "admin"} ROLES = {"user", "moderator", "admin"}
STATUSES = {"active", "disabled", "restricted", "blacklisted"} STATUSES = {"active", "disabled", "restricted", "blacklisted"}
ROOM_VISIBILITIES = {"public", "private"} ROOM_VISIBILITIES = {"public", "private"}
@ -130,13 +133,6 @@ RATE_LIMITS = {
"reaction": (120, 60), "reaction": (120, 60),
"profile": (30, 60), "profile": (30, 60),
"settings": (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 = { PLACEHOLDER_SECRETS = {
@ -150,7 +146,6 @@ PLACEHOLDER_SECRETS = {
_runtime_state = { _runtime_state = {
"last_attachment_cleanup": 0.0, "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() configured = os.environ.get(env_value, "").strip()
if configured and configured.lower() not in PLACEHOLDER_SECRETS and len(configured) >= min_length: if configured and configured.lower() not in PLACEHOLDER_SECRETS and len(configured) >= min_length:
return configured return configured
path = Path(path_value) path = Path(path_value)
path.parent.mkdir(parents=True, exist_ok=True) path.parent.mkdir(parents=True, exist_ok=True)
if path.exists(): if path.exists():
existing = path.read_text(encoding="utf-8").strip() existing = path.read_text(encoding="utf-8").strip()
if len(existing) >= min_length: if len(existing) >= min_length:
return existing return existing
generated = secrets.token_urlsafe(64) generated = secrets.token_urlsafe(64)
try: try:
path.write_text(generated, encoding="utf-8") path.write_text(generated, encoding="utf-8")
@ -232,6 +230,7 @@ def get_db() -> sqlite3.Connection:
db_dir = os.path.dirname(DB_PATH) db_dir = os.path.dirname(DB_PATH)
if db_dir: if db_dir:
os.makedirs(db_dir, exist_ok=True) os.makedirs(db_dir, exist_ok=True)
conn = sqlite3.connect(DB_PATH, timeout=8, isolation_level=None, check_same_thread=False) conn = sqlite3.connect(DB_PATH, timeout=8, isolation_level=None, check_same_thread=False)
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
conn.execute("PRAGMA foreign_keys = ON") conn.execute("PRAGMA foreign_keys = ON")
@ -586,12 +585,7 @@ MESSAGE_SELECT = """
a.mime_type AS attachment_mime_type, a.mime_type AS attachment_mime_type,
a.size_bytes AS attachment_size_bytes, a.size_bytes AS attachment_size_bytes,
u.role AS user_role, u.role AS user_role,
us.name_color AS user_name_color, us.name_color AS user_name_color
EXISTS (
SELECT 1
FROM message_pins mp
WHERE mp.message_id = m.id
) AS is_pinned
FROM messages AS m FROM messages AS m
JOIN users AS u JOIN users AS u
ON u.id = m.user_id ON u.id = m.user_id
@ -608,7 +602,6 @@ def team_page_select() -> str:
p.id, p.id,
p.team_id, p.team_id,
p.channel_id, p.channel_id,
p.parent_page_id,
p.slug, p.slug,
p.title, p.title,
p.body, p.body,
@ -622,16 +615,12 @@ def team_page_select() -> str:
t.slug AS team_slug, t.slug AS team_slug,
t.name AS team_name, t.name AS team_name,
c.slug AS channel_slug, c.slug AS channel_slug,
c.name AS channel_name, c.name AS channel_name
parent.slug AS parent_page_slug,
parent.title AS parent_page_title
FROM pages AS p FROM pages AS p
JOIN teams AS t JOIN teams AS t
ON t.id = p.team_id ON t.id = p.team_id
LEFT JOIN channels AS c LEFT JOIN channels AS c
ON c.id = p.channel_id 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() now = time.time()
cutoff = now - window_seconds cutoff = now - window_seconds
subject_hash = current_client_hash() subject_hash = current_client_hash()
with get_db() as db: with get_db() as db:
db.execute("DELETE FROM rate_limit_events WHERE created_at < ?", (cutoff - 60,)) db.execute("DELETE FROM rate_limit_events WHERE created_at < ?", (cutoff - 60,))
rows = db.execute( rows = db.execute(
@ -792,9 +782,11 @@ def check_rate_limit(scope: str) -> Optional[int]:
""", """,
(scope, subject_hash, cutoff), (scope, subject_hash, cutoff),
).fetchall() ).fetchall()
if len(rows) >= limit: if len(rows) >= limit:
oldest = rows[0]["created_at"] oldest = rows[0]["created_at"]
return max(1, int(window_seconds - (now - oldest)) + 1) return max(1, int(window_seconds - (now - oldest)) + 1)
db.execute( db.execute(
""" """
INSERT INTO rate_limit_events ( INSERT INTO rate_limit_events (
@ -806,6 +798,7 @@ def check_rate_limit(scope: str) -> Optional[int]:
(scope, subject_hash, now), (scope, subject_hash, now),
) )
db.commit() db.commit()
return None return None
@ -851,7 +844,7 @@ def csrf_valid() -> bool:
return bool(supplied and expected and hmac.compare_digest(supplied, expected)) 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"] status = row["status"]
restricted_until = parse_iso(row["restricted_until"]) restricted_until = parse_iso(row["restricted_until"])
if status == "restricted" and restricted_until and restricted_until <= utc_now(): 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"], "role": row["role"],
"status": status, "status": status,
"restricted_until": row["restricted_until"], "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") user_id = session.get("user_id")
if not user_id: if not user_id:
return None return None
with get_db() as db: with get_db() as db:
row = db.execute( row = db.execute(
""" """
@ -884,19 +877,13 @@ def current_user() -> Optional[dict]:
""", """,
(user_id,), (user_id,),
).fetchone() ).fetchone()
settings_row = db.execute(
"""
SELECT presence_mode
FROM user_settings
WHERE user_id = ?
""",
(user_id,),
).fetchone()
if not row: if not row:
session.clear() session.clear()
return None return None
user = user_payload(row, settings_row) user = user_payload(row)
if user["status"] == "restricted": if user["status"] == "restricted":
expires = parse_iso(user["restricted_until"]) expires = parse_iso(user["restricted_until"])
if expires and expires <= utc_now(): if expires and expires <= utc_now():
@ -918,6 +905,7 @@ def current_user() -> Optional[dict]:
if user["status"] in {"disabled", "blacklisted"}: if user["status"] in {"disabled", "blacklisted"}:
session.clear() session.clear()
return None return None
return user return user
@ -932,7 +920,6 @@ def login_user(row: sqlite3.Row | dict) -> dict:
"role": row["role"], "role": row["role"],
"status": row["status"], "status": row["status"],
"restricted_until": row.get("restricted_until") if isinstance(row, dict) else row["restricted_until"], "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_blacklisted():
if request.path.startswith("/api/"): if request.path.startswith("/api/"):
return jsonify({"error": "Access to this service has been blocked.", "code": "blacklisted"}), 403 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 @app.context_processor
@ -1040,6 +1027,7 @@ def sqlite_error(error):
def init_db() -> None: def init_db() -> None:
UPLOAD_DIR.mkdir(parents=True, exist_ok=True) UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
with get_db() as db: with get_db() as db:
db.execute( db.execute(
""" """
@ -1058,6 +1046,7 @@ def init_db() -> None:
) )
""" """
) )
db.execute( db.execute(
""" """
CREATE TABLE IF NOT EXISTS rooms ( CREATE TABLE IF NOT EXISTS rooms (
@ -1073,6 +1062,7 @@ def init_db() -> None:
) )
""" """
) )
db.execute( db.execute(
""" """
CREATE TABLE IF NOT EXISTS room_members ( CREATE TABLE IF NOT EXISTS room_members (
@ -1086,6 +1076,7 @@ def init_db() -> None:
) )
""" """
) )
db.execute( db.execute(
""" """
CREATE TABLE IF NOT EXISTS attachments ( CREATE TABLE IF NOT EXISTS attachments (
@ -1100,6 +1091,7 @@ def init_db() -> None:
) )
""" """
) )
db.execute( db.execute(
""" """
CREATE TABLE IF NOT EXISTS teams ( CREATE TABLE IF NOT EXISTS teams (
@ -1114,6 +1106,7 @@ def init_db() -> None:
) )
""" """
) )
db.execute( db.execute(
""" """
CREATE TABLE IF NOT EXISTS team_members ( CREATE TABLE IF NOT EXISTS team_members (
@ -1127,6 +1120,7 @@ def init_db() -> None:
) )
""" """
) )
db.execute( db.execute(
""" """
CREATE TABLE IF NOT EXISTS team_invites ( CREATE TABLE IF NOT EXISTS team_invites (
@ -1145,6 +1139,7 @@ def init_db() -> None:
) )
""" """
) )
db.execute( db.execute(
""" """
CREATE TABLE IF NOT EXISTS channels ( 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( db.execute(
""" """
CREATE TABLE IF NOT EXISTS messages ( CREATE TABLE IF NOT EXISTS messages (
@ -1207,6 +1182,7 @@ def init_db() -> None:
) )
""" """
) )
db.execute( db.execute(
""" """
CREATE TABLE IF NOT EXISTS room_invites ( CREATE TABLE IF NOT EXISTS room_invites (
@ -1224,6 +1200,7 @@ def init_db() -> None:
) )
""" """
) )
db.execute( db.execute(
""" """
CREATE TABLE IF NOT EXISTS typing_events ( CREATE TABLE IF NOT EXISTS typing_events (
@ -1236,6 +1213,7 @@ def init_db() -> None:
) )
""" """
) )
db.execute( db.execute(
""" """
CREATE TABLE IF NOT EXISTS channel_typing_events ( CREATE TABLE IF NOT EXISTS channel_typing_events (
@ -1248,6 +1226,7 @@ def init_db() -> None:
) )
""" """
) )
db.execute( db.execute(
""" """
CREATE TABLE IF NOT EXISTS dm_typing_events ( CREATE TABLE IF NOT EXISTS dm_typing_events (
@ -1260,6 +1239,7 @@ def init_db() -> None:
) )
""" """
) )
db.execute( db.execute(
""" """
CREATE TABLE IF NOT EXISTS blacklist_entries ( CREATE TABLE IF NOT EXISTS blacklist_entries (
@ -1275,6 +1255,7 @@ def init_db() -> None:
) )
""" """
) )
db.execute( db.execute(
""" """
CREATE TABLE IF NOT EXISTS rate_limit_events ( CREATE TABLE IF NOT EXISTS rate_limit_events (
@ -1285,6 +1266,7 @@ def init_db() -> None:
) )
""" """
) )
db.execute( db.execute(
""" """
CREATE TABLE IF NOT EXISTS audit_log ( CREATE TABLE IF NOT EXISTS audit_log (
@ -1300,13 +1282,13 @@ def init_db() -> None:
) )
""" """
) )
db.execute( db.execute(
""" """
CREATE TABLE IF NOT EXISTS pages ( CREATE TABLE IF NOT EXISTS pages (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
team_id INTEGER NOT NULL, team_id INTEGER NOT NULL,
channel_id INTEGER, channel_id INTEGER,
parent_page_id INTEGER,
slug TEXT COLLATE NOCASE NOT NULL, slug TEXT COLLATE NOCASE NOT NULL,
title TEXT NOT NULL, title TEXT NOT NULL,
body TEXT NOT NULL DEFAULT '', body TEXT NOT NULL DEFAULT '',
@ -1320,63 +1302,12 @@ def init_db() -> None:
UNIQUE(team_id, slug), UNIQUE(team_id, slug),
FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE, FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE,
FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE SET NULL, 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 (created_by_user_id) REFERENCES users(id) ON DELETE RESTRICT,
FOREIGN KEY (updated_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( db.execute(
""" """
CREATE TABLE IF NOT EXISTS user_profiles ( CREATE TABLE IF NOT EXISTS user_profiles (
@ -1387,14 +1318,12 @@ def init_db() -> None:
bio_mode TEXT NOT NULL DEFAULT 'txt', bio_mode TEXT NOT NULL DEFAULT 'txt',
website TEXT, website TEXT,
location TEXT, location TEXT,
status_message TEXT,
banner_attachment_id INTEGER,
updated_at TEXT NOT NULL, updated_at TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
FOREIGN KEY (banner_attachment_id) REFERENCES attachments(id) ON DELETE SET NULL
) )
""" """
) )
db.execute( db.execute(
""" """
CREATE TABLE IF NOT EXISTS user_settings ( CREATE TABLE IF NOT EXISTS user_settings (
@ -1403,16 +1332,12 @@ def init_db() -> None:
name_color TEXT, name_color TEXT,
custom_css TEXT NOT NULL DEFAULT '', custom_css TEXT NOT NULL DEFAULT '',
theme_mode TEXT NOT NULL DEFAULT 'dark', 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, updated_at TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) )
""" """
) )
db.execute( db.execute(
""" """
CREATE TABLE IF NOT EXISTS message_reactions ( CREATE TABLE IF NOT EXISTS message_reactions (
@ -1426,6 +1351,7 @@ def init_db() -> None:
) )
""" """
) )
db.execute( db.execute(
""" """
CREATE TABLE IF NOT EXISTS message_edits ( CREATE TABLE IF NOT EXISTS message_edits (
@ -1442,6 +1368,7 @@ def init_db() -> None:
) )
""" """
) )
db.execute( db.execute(
""" """
CREATE TABLE IF NOT EXISTS message_receipts ( CREATE TABLE IF NOT EXISTS message_receipts (
@ -1454,6 +1381,7 @@ def init_db() -> None:
) )
""" """
) )
db.execute( db.execute(
""" """
CREATE TABLE IF NOT EXISTS message_attachments ( CREATE TABLE IF NOT EXISTS message_attachments (
@ -1466,6 +1394,7 @@ def init_db() -> None:
) )
""" """
) )
db.execute( db.execute(
""" """
CREATE TABLE IF NOT EXISTS presence_events ( 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( db.execute(
""" """
CREATE TABLE IF NOT EXISTS call_sessions ( CREATE TABLE IF NOT EXISTS call_sessions (
@ -1493,6 +1446,7 @@ def init_db() -> None:
) )
""" """
) )
db.execute( db.execute(
""" """
CREATE TABLE IF NOT EXISTS call_signals ( CREATE TABLE IF NOT EXISTS call_signals (
@ -1507,6 +1461,7 @@ def init_db() -> None:
) )
""" """
) )
db.execute( db.execute(
""" """
CREATE TABLE IF NOT EXISTS group_call_sessions ( CREATE TABLE IF NOT EXISTS group_call_sessions (
@ -1523,6 +1478,7 @@ def init_db() -> None:
) )
""" """
) )
db.execute( db.execute(
""" """
CREATE TABLE IF NOT EXISTS group_call_invites ( CREATE TABLE IF NOT EXISTS group_call_invites (
@ -1541,81 +1497,21 @@ def init_db() -> None:
) )
""" """
) )
db.execute( db.execute(
""" """
CREATE TABLE IF NOT EXISTS message_pins ( CREATE TABLE IF NOT EXISTS group_call_participants (
message_id INTEGER PRIMARY KEY, group_call_session_id INTEGER NOT NULL,
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,
user_id INTEGER NOT NULL, user_id INTEGER NOT NULL,
username TEXT NOT NULL, joined_at TEXT NOT NULL,
text TEXT NOT NULL, updated_at TEXT NOT NULL,
attachment_ids_json TEXT NOT NULL DEFAULT '[]', left_at TEXT,
deliver_at TEXT NOT NULL, PRIMARY KEY (group_call_session_id, user_id),
created_at TEXT NOT NULL, FOREIGN KEY (group_call_session_id) REFERENCES group_call_sessions(id) ON DELETE CASCADE,
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,
FOREIGN KEY (user_id) REFERENCES users(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"): if not column_exists(db, "messages", "room_id"):
db.execute("ALTER TABLE messages ADD COLUMN room_id INTEGER") 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") db.execute("ALTER TABLE messages ADD COLUMN direct_conversation_id INTEGER")
if not column_exists(db, "user_settings", "name_color"): if not column_exists(db, "user_settings", "name_color"):
db.execute("ALTER TABLE user_settings ADD COLUMN name_color TEXT") 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"): if not column_exists(db, "presence_events", "direct_conversation_id"):
db.execute("ALTER TABLE presence_events ADD COLUMN direct_conversation_id INTEGER") db.execute("ALTER TABLE presence_events ADD COLUMN direct_conversation_id INTEGER")
if not column_exists(db, "user_profiles", "badge_text"): if not column_exists(db, "user_profiles", "badge_text"):
db.execute("ALTER TABLE user_profiles ADD COLUMN badge_text 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_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_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_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_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_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_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_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_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_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_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_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_group_call_participants_session_active ON group_call_participants(group_call_session_id, left_at)")
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_group_call_participants_user_active ON group_call_participants(user_id, left_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_group_call_participants_session_updated ON group_call_participants(group_call_session_id, updated_at)")
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)")
general = db.execute("SELECT id FROM rooms WHERE slug = 'general'").fetchone() general = db.execute("SELECT id FROM rooms WHERE slug = 'general'").fetchone()
if not general: if not general:
@ -1722,6 +1596,7 @@ def init_db() -> None:
OR role = '' OR role = ''
""" """
) )
db.execute( db.execute(
""" """
UPDATE users UPDATE users
@ -1766,6 +1641,7 @@ def init_db() -> None:
WHERE is_active = 1 WHERE is_active = 1
""" """
).fetchall() ).fetchall()
for room in legacy_rooms: for room in legacy_rooms:
existing_channel = db.execute( existing_channel = db.execute(
"SELECT id FROM channels WHERE legacy_room_id = ?", "SELECT id FROM channels WHERE legacy_room_id = ?",
@ -1820,6 +1696,7 @@ def init_db() -> None:
AND status = 'active' AND status = 'active'
""" """
).fetchall() ).fetchall()
for admin_user in admin_users: for admin_user in admin_users:
db.execute( db.execute(
""" """
@ -1843,8 +1720,6 @@ def init_db() -> None:
bio_mode, bio_mode,
website, website,
location, location,
status_message,
banner_attachment_id,
updated_at updated_at
) )
SELECT SELECT
@ -1855,13 +1730,12 @@ def init_db() -> None:
'txt', 'txt',
'', '',
'', '',
'',
NULL,
? ?
FROM users FROM users
""", """,
(now_iso(),), (now_iso(),),
) )
db.execute( db.execute(
""" """
INSERT OR IGNORE INTO user_settings ( INSERT OR IGNORE INTO user_settings (
@ -1870,11 +1744,6 @@ def init_db() -> None:
name_color, name_color,
custom_css, custom_css,
theme_mode, theme_mode,
profile_background_color,
profile_banner_color,
profile_visibility,
last_active_visibility,
presence_mode,
updated_at updated_at
) )
SELECT SELECT
@ -1883,11 +1752,6 @@ def init_db() -> None:
NULL, NULL,
'', '',
'dark', 'dark',
NULL,
NULL,
'public',
'everyone',
'online',
? ?
FROM users FROM users
""", """,
@ -1897,6 +1761,7 @@ def init_db() -> None:
existing_message_attachment_counts = db.execute( existing_message_attachment_counts = db.execute(
"SELECT COUNT(*) AS count FROM message_attachments" "SELECT COUNT(*) AS count FROM message_attachments"
).fetchone()["count"] ).fetchone()["count"]
if existing_message_attachment_counts == 0: if existing_message_attachment_counts == 0:
rows = db.execute( rows = db.execute(
""" """
@ -1924,16 +1789,20 @@ def init_db() -> None:
def bootstrap_admin() -> None: def bootstrap_admin() -> None:
username = normalize_username(os.environ.get("ADMIN_USERNAME", "")) username = normalize_username(os.environ.get("ADMIN_USERNAME", ""))
password = os.environ.get("ADMIN_PASSWORD", "") password = os.environ.get("ADMIN_PASSWORD", "")
if not username or not password: if not username or not password:
return return
if validate_username(username) or validate_password(password, username): if validate_username(username) or validate_password(password, username):
print("WARNING: Admin bootstrap credentials failed validation.") print("WARNING: Admin bootstrap credentials failed validation.")
return return
with get_db() as db: with get_db() as db:
existing = db.execute( existing = db.execute(
"SELECT id FROM users WHERE username = ?", "SELECT id FROM users WHERE username = ?",
(username,), (username,),
).fetchone() ).fetchone()
if existing: if existing:
db.execute( db.execute(
""" """
@ -1964,6 +1833,7 @@ def bootstrap_admin() -> None:
(username, generate_password_hash(password), now_iso()), (username, generate_password_hash(password), now_iso()),
) )
user_id = cursor.lastrowid user_id = cursor.lastrowid
db.execute( db.execute(
""" """
INSERT OR IGNORE INTO user_profiles ( INSERT OR IGNORE INTO user_profiles (
@ -1974,13 +1844,12 @@ def bootstrap_admin() -> None:
bio_mode, bio_mode,
website, website,
location, location,
status_message,
banner_attachment_id,
updated_at updated_at
) VALUES (?, ?, '', '', 'txt', '', '', '', NULL, ?) ) VALUES (?, ?, '', '', 'txt', '', '', ?)
""", """,
(user_id, username, now_iso()), (user_id, username, now_iso()),
) )
db.execute( db.execute(
""" """
INSERT OR IGNORE INTO user_settings ( INSERT OR IGNORE INTO user_settings (
@ -1989,16 +1858,12 @@ def bootstrap_admin() -> None:
name_color, name_color,
custom_css, custom_css,
theme_mode, theme_mode,
profile_background_color,
profile_banner_color,
profile_visibility,
last_active_visibility,
presence_mode,
updated_at updated_at
) VALUES (?, '#f7dce8', NULL, '', 'dark', NULL, NULL, 'public', 'everyone', 'online', ?) ) VALUES (?, '#f7dce8', NULL, '', 'dark', ?)
""", """,
(user_id, now_iso()), (user_id, now_iso()),
) )
db.commit() db.commit()
@ -2026,11 +1891,14 @@ def api_me():
@app.route("/api/register", methods=["POST"]) @app.route("/api/register", methods=["POST"])
def register(): def register():
limited = rate_limited("register") limited = rate_limited("register")
if limited: if limited:
return limited return limited
if not csrf_valid(): if not csrf_valid():
return jsonify({"error": "Security token expired.", "code": "csrf"}), 403 return jsonify({"error": "Security token expired.", "code": "csrf"}), 403
try: try:
data = safe_json() data = safe_json()
except ValueError as error: except ValueError as error:
@ -2042,8 +1910,10 @@ def register():
username = normalize_username(data.get("username")) username = normalize_username(data.get("username"))
password = data.get("password", "") password = data.get("password", "")
username_error = validate_username(username) username_error = validate_username(username)
password_error = validate_password(password, username) password_error = validate_password(password, username)
if username_error: if username_error:
return jsonify({"error": username_error}), 400 return jsonify({"error": username_error}), 400
if password_error: if password_error:
@ -2073,8 +1943,8 @@ def register():
"role": "user", "role": "user",
"status": "active", "status": "active",
"restricted_until": None, "restricted_until": None,
"presence_mode": "online",
} }
db_write( db_write(
""" """
INSERT OR IGNORE INTO user_profiles ( INSERT OR IGNORE INTO user_profiles (
@ -2085,13 +1955,12 @@ def register():
bio_mode, bio_mode,
website, website,
location, location,
status_message,
banner_attachment_id,
updated_at updated_at
) VALUES (?, ?, '', '', 'txt', '', '', '', NULL, ?) ) VALUES (?, ?, '', '', 'txt', '', '', ?)
""", """,
(user["id"], username, now_iso()), (user["id"], username, now_iso()),
) )
db_write( db_write(
""" """
INSERT OR IGNORE INTO user_settings ( INSERT OR IGNORE INTO user_settings (
@ -2100,19 +1969,15 @@ def register():
name_color, name_color,
custom_css, custom_css,
theme_mode, theme_mode,
profile_background_color,
profile_banner_color,
profile_visibility,
last_active_visibility,
presence_mode,
updated_at updated_at
) VALUES (?, '#f7dce8', NULL, '', 'dark', NULL, NULL, 'public', 'everyone', 'online', ?) ) VALUES (?, '#f7dce8', NULL, '', 'dark', ?)
""", """,
(user["id"], now_iso()), (user["id"], now_iso()),
) )
authenticated = login_user(user) authenticated = login_user(user)
audit("account_registered", user["id"], "user", str(user["id"])) audit("account_registered", user["id"], "user", str(user["id"]))
return jsonify( return jsonify(
{ {
"status": "registered", "status": "registered",
@ -2127,6 +1992,7 @@ def login():
limited = rate_limited("login") limited = rate_limited("login")
if limited: if limited:
return limited return limited
if not csrf_valid(): if not csrf_valid():
return jsonify({"error": "Security token expired.", "code": "csrf"}), 403 return jsonify({"error": "Security token expired.", "code": "csrf"}), 403
@ -2146,29 +2012,28 @@ def login():
row = db.execute( row = db.execute(
""" """
SELECT SELECT
u.id, id,
u.username, username,
u.password_hash, password_hash,
u.role, role,
u.status, status,
u.restricted_until, restricted_until
us.presence_mode FROM users
FROM users u WHERE username = ?
LEFT JOIN user_settings us
ON us.user_id = u.id
WHERE u.username = ?
""", """,
(username,), (username,),
).fetchone() ).fetchone()
if not row or not isinstance(password, str) or not check_password_hash(row["password_hash"], password): 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 return jsonify({"error": "Invalid username or password."}), 401
if row["status"] in {"disabled", "blacklisted"}: if row["status"] in {"disabled", "blacklisted"}:
return jsonify({"error": "Invalid username or password."}), 401 return jsonify({"error": "Invalid username or password."}), 401
db_write("UPDATE users SET last_login_at = ? WHERE id = ?", (now_iso(), row["id"])) 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"])) audit("account_logged_in", row["id"], "user", str(row["id"]))
return jsonify( return jsonify(
{ {
"status": "logged_in", "status": "logged_in",
@ -2187,12 +2052,10 @@ def logout():
def _maybe_cleanup_orphaned_attachments(): def _maybe_cleanup_orphaned_attachments():
from features import cleanup_orphaned_attachments
maybe_cleanup_orphaned_attachments( maybe_cleanup_orphaned_attachments(
_runtime_state, _runtime_state,
ATTACHMENT_CLEANUP_INTERVAL_SECONDS, ATTACHMENT_CLEANUP_INTERVAL_SECONDS,
lambda: cleanup_orphaned_attachments( lambda: __import__("features").cleanup_orphaned_attachments(
get_db, get_db,
UPLOAD_DIR, UPLOAD_DIR,
utc_now, 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( register_routes(
app, app,
{ {
@ -2275,9 +2124,6 @@ register_routes(
"html": html, "html": html,
"attachment_list_for_message": lambda message_id: __import__("features").attachment_list_for_message(get_db, message_id), "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_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": { "constants": {
"MAX_MESSAGE_LENGTH": MAX_MESSAGE_LENGTH, "MAX_MESSAGE_LENGTH": MAX_MESSAGE_LENGTH,
"MAX_HISTORY_MESSAGES": MAX_HISTORY_MESSAGES, "MAX_HISTORY_MESSAGES": MAX_HISTORY_MESSAGES,
@ -2302,12 +2148,11 @@ register_routes(
"MAX_WEBSITE_LENGTH": MAX_WEBSITE_LENGTH, "MAX_WEBSITE_LENGTH": MAX_WEBSITE_LENGTH,
"MAX_CUSTOM_CSS_LENGTH": MAX_CUSTOM_CSS_LENGTH, "MAX_CUSTOM_CSS_LENGTH": MAX_CUSTOM_CSS_LENGTH,
"MAX_PROFILE_BADGE_LENGTH": MAX_PROFILE_BADGE_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_OWNED_TEAMS_PER_USER": MAX_OWNED_TEAMS_PER_USER,
"MAX_PAGE_COMMENT_LENGTH": MAX_PAGE_COMMENT_LENGTH,
"ROOM_VISIBILITIES": ROOM_VISIBILITIES, "ROOM_VISIBILITIES": ROOM_VISIBILITIES,
"ROLES": ROLES, "ROLES": ROLES,
"STATUSES": STATUSES, "STATUSES": STATUSES,
"GROUP_CALL_PARTICIPANT_STALE_SECONDS": GROUP_CALL_PARTICIPANT_STALE_SECONDS,
}, },
}, },
) )