Update app.py
This commit is contained in:
parent
11b6848099
commit
02ecabf0ea
407
app.py
407
app.py
@ -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,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user