232 lines
6.5 KiB
Python
232 lines
6.5 KiB
Python
import os
|
|
import secrets
|
|
from datetime import timedelta, timezone, datetime
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
APP_ROOT = Path(__file__).resolve().parent
|
|
|
|
DB_PATH = os.environ.get("CHAT_DB_PATH", str(APP_ROOT / "chat.db"))
|
|
UPLOAD_DIR = Path(os.environ.get("CHAT_UPLOAD_DIR", str(APP_ROOT / "uploads")))
|
|
|
|
SECRET_KEY_PATH = os.environ.get(
|
|
"CHAT_SECRET_KEY_PATH",
|
|
str(Path(DB_PATH).resolve().parent / "flask_secret.key"),
|
|
)
|
|
CLIENT_HASH_KEY_PATH = os.environ.get(
|
|
"CHAT_CLIENT_HASH_KEY_PATH",
|
|
str(Path(DB_PATH).resolve().parent / "client_hash.key"),
|
|
)
|
|
|
|
MAX_ATTACHMENT_BYTES = 100 * 1024 * 1024
|
|
MAX_REQUEST_BYTES = max(110 * 1024 * 1024, MAX_ATTACHMENT_BYTES + 10 * 1024 * 1024)
|
|
|
|
MAX_MESSAGE_LENGTH = 1000
|
|
MAX_USERNAME_LENGTH = 30
|
|
MIN_USERNAME_LENGTH = 3
|
|
MIN_PASSWORD_LENGTH = 7
|
|
MAX_PASSWORD_LENGTH = 256
|
|
MAX_HISTORY_MESSAGES = 100
|
|
MAX_INCREMENTAL_MESSAGES = 100
|
|
MAX_FEED_MESSAGES = 50
|
|
MAX_THREAD_REPLIES = 100
|
|
MAX_ATTACHMENT_NAME_LENGTH = 150
|
|
ATTACHMENT_ORPHAN_TTL_HOURS = 24
|
|
ATTACHMENT_CLEANUP_INTERVAL_SECONDS = 60 * 60
|
|
INVITE_TTL_HOURS = 72
|
|
TEAM_INVITE_TTL_HOURS = 72
|
|
TYPING_TTL_SECONDS = 7
|
|
PRESENCE_TTL_SECONDS = 40
|
|
MAX_PROFILE_BIO_LENGTH = 5000
|
|
MAX_DISPLAY_NAME_LENGTH = 80
|
|
MAX_LOCATION_LENGTH = 120
|
|
MAX_WEBSITE_LENGTH = 300
|
|
MAX_CUSTOM_CSS_LENGTH = 20000
|
|
MAX_PROFILE_BADGE_LENGTH = 48
|
|
MAX_OWNED_TEAMS_PER_USER = 3
|
|
MAX_MESSAGE_EDIT_WINDOW_SECONDS = 15 * 60
|
|
MAX_SEARCH_QUERY_LENGTH = 120
|
|
MAX_SEARCH_RESULTS = 100
|
|
READ_RECEIPT_BATCH_LIMIT = 200
|
|
GROUP_CALL_PARTICIPANT_STALE_SECONDS = 45
|
|
|
|
PRIVATE_ACCESS_KEY = os.environ.get("PRIVATE_ACCESS_KEY", "").strip()
|
|
|
|
CALLS_BASE = os.environ.get("CALLS_BASE", "https://calls.example.com").rstrip("/")
|
|
CALL_HELPER_SHARED_SECRET = os.environ.get("CALL_HELPER_SHARED_SECRET", "").strip()
|
|
CALL_TOKEN_TTL_SECONDS = int(os.environ.get("CALL_TOKEN_TTL_SECONDS", "60"))
|
|
|
|
ALLOWED_ATTACHMENT_EXTENSIONS = {
|
|
"txt",
|
|
"pdf",
|
|
"png",
|
|
"jpg",
|
|
"jpeg",
|
|
"gif",
|
|
"webp",
|
|
"mp3",
|
|
"wav",
|
|
"ogg",
|
|
"m4a",
|
|
"aac",
|
|
"flac",
|
|
"mp4",
|
|
"webm",
|
|
"mov",
|
|
"m4v",
|
|
"ogv",
|
|
}
|
|
|
|
ALLOWED_ATTACHMENT_MIME_TYPES = {
|
|
"text/plain",
|
|
"application/pdf",
|
|
"image/png",
|
|
"image/jpeg",
|
|
"image/gif",
|
|
"image/webp",
|
|
"audio/mpeg",
|
|
"audio/mp3",
|
|
"audio/wav",
|
|
"audio/x-wav",
|
|
"audio/ogg",
|
|
"audio/mp4",
|
|
"audio/aac",
|
|
"audio/flac",
|
|
"video/mp4",
|
|
"video/webm",
|
|
"video/quicktime",
|
|
"video/ogg",
|
|
}
|
|
|
|
ROLES = {"user", "moderator", "admin"}
|
|
STATUSES = {"active", "disabled", "restricted", "blacklisted"}
|
|
ROOM_VISIBILITIES = {"public", "private"}
|
|
|
|
RATE_LIMITS = {
|
|
"login": (8, 10 * 60),
|
|
"register": (5, 60 * 60),
|
|
"send": (30, 60),
|
|
"admin": (60, 60),
|
|
"attachment": (20, 60),
|
|
"typing": (60, 60),
|
|
"reaction": (120, 60),
|
|
"profile": (30, 60),
|
|
"settings": (30, 60),
|
|
}
|
|
|
|
PLACEHOLDER_SECRETS = {
|
|
"",
|
|
"change-this-secret-key",
|
|
"replace-this-with-a-long-random-secret",
|
|
"replace-me",
|
|
"secret",
|
|
"changeme",
|
|
}
|
|
|
|
RUNTIME_STATE = {
|
|
"last_attachment_cleanup": 0.0,
|
|
}
|
|
|
|
|
|
def utc_now() -> datetime:
|
|
return datetime.now(timezone.utc)
|
|
|
|
|
|
def now_iso() -> str:
|
|
return utc_now().isoformat()
|
|
|
|
|
|
def parse_iso(value: Optional[str]) -> Optional[datetime]:
|
|
if not value:
|
|
return None
|
|
try:
|
|
parsed = datetime.fromisoformat(value)
|
|
except ValueError:
|
|
return None
|
|
if not parsed.tzinfo:
|
|
return parsed.replace(tzinfo=timezone.utc)
|
|
return parsed
|
|
|
|
|
|
def load_or_create_key(path_value: str, env_value: str, min_length: int = 32) -> str:
|
|
configured = os.environ.get(env_value, "").strip()
|
|
if configured and configured.lower() not in PLACEHOLDER_SECRETS and len(configured) >= min_length:
|
|
return configured
|
|
|
|
path = Path(path_value)
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
if path.exists():
|
|
existing = path.read_text(encoding="utf-8").strip()
|
|
if len(existing) >= min_length:
|
|
return existing
|
|
|
|
generated = secrets.token_urlsafe(64)
|
|
try:
|
|
path.write_text(generated, encoding="utf-8")
|
|
os.chmod(path, 0o600)
|
|
except OSError:
|
|
pass
|
|
return generated
|
|
|
|
|
|
SECRET_KEY = load_or_create_key(SECRET_KEY_PATH, "FLASK_SECRET_KEY")
|
|
CLIENT_HASH_KEY = load_or_create_key(CLIENT_HASH_KEY_PATH, "CHAT_CLIENT_HASH_KEY")
|
|
|
|
TRUSTED_PROXY_HOPS = int(os.environ.get("TRUSTED_PROXY_HOPS", "0"))
|
|
SESSION_COOKIE_SECURE = (os.environ.get("SESSION_COOKIE_SECURE", "1") == "1")
|
|
PERMANENT_SESSION_LIFETIME = timedelta(days=14)
|
|
|
|
|
|
def get_asset_version() -> str:
|
|
try:
|
|
js_dir = APP_ROOT / "static" / "js"
|
|
values = [int((APP_ROOT / "static" / "style.css").stat().st_mtime)]
|
|
values.extend(int(path.stat().st_mtime) for path in js_dir.glob("*.js"))
|
|
return str(max(values))
|
|
except OSError:
|
|
return "1"
|
|
|
|
|
|
def flask_config() -> dict:
|
|
return {
|
|
"MAX_CONTENT_LENGTH": MAX_REQUEST_BYTES,
|
|
"SESSION_COOKIE_NAME": "chat_session",
|
|
"SESSION_COOKIE_HTTPONLY": True,
|
|
"SESSION_COOKIE_SAMESITE": "Lax",
|
|
"SESSION_COOKIE_SECURE": SESSION_COOKIE_SECURE,
|
|
"PERMANENT_SESSION_LIFETIME": PERMANENT_SESSION_LIFETIME,
|
|
}
|
|
|
|
|
|
CONSTANTS = {
|
|
"MAX_MESSAGE_LENGTH": MAX_MESSAGE_LENGTH,
|
|
"MAX_HISTORY_MESSAGES": MAX_HISTORY_MESSAGES,
|
|
"MAX_INCREMENTAL_MESSAGES": MAX_INCREMENTAL_MESSAGES,
|
|
"MAX_FEED_MESSAGES": MAX_FEED_MESSAGES,
|
|
"MAX_THREAD_REPLIES": MAX_THREAD_REPLIES,
|
|
"MAX_ATTACHMENT_BYTES": MAX_ATTACHMENT_BYTES,
|
|
"MAX_ATTACHMENT_NAME_LENGTH": MAX_ATTACHMENT_NAME_LENGTH,
|
|
"ALLOWED_ATTACHMENT_EXTENSIONS": ALLOWED_ATTACHMENT_EXTENSIONS,
|
|
"ALLOWED_ATTACHMENT_MIME_TYPES": ALLOWED_ATTACHMENT_MIME_TYPES,
|
|
"INVITE_TTL_HOURS": INVITE_TTL_HOURS,
|
|
"TEAM_INVITE_TTL_HOURS": TEAM_INVITE_TTL_HOURS,
|
|
"TYPING_TTL_SECONDS": TYPING_TTL_SECONDS,
|
|
"PRESENCE_TTL_SECONDS": PRESENCE_TTL_SECONDS,
|
|
"MAX_SEARCH_QUERY_LENGTH": MAX_SEARCH_QUERY_LENGTH,
|
|
"MAX_SEARCH_RESULTS": MAX_SEARCH_RESULTS,
|
|
"READ_RECEIPT_BATCH_LIMIT": READ_RECEIPT_BATCH_LIMIT,
|
|
"MAX_MESSAGE_EDIT_WINDOW_SECONDS": MAX_MESSAGE_EDIT_WINDOW_SECONDS,
|
|
"MAX_DISPLAY_NAME_LENGTH": MAX_DISPLAY_NAME_LENGTH,
|
|
"MAX_PROFILE_BIO_LENGTH": MAX_PROFILE_BIO_LENGTH,
|
|
"MAX_LOCATION_LENGTH": MAX_LOCATION_LENGTH,
|
|
"MAX_WEBSITE_LENGTH": MAX_WEBSITE_LENGTH,
|
|
"MAX_CUSTOM_CSS_LENGTH": MAX_CUSTOM_CSS_LENGTH,
|
|
"MAX_PROFILE_BADGE_LENGTH": MAX_PROFILE_BADGE_LENGTH,
|
|
"MAX_OWNED_TEAMS_PER_USER": MAX_OWNED_TEAMS_PER_USER,
|
|
"ROOM_VISIBILITIES": ROOM_VISIBILITIES,
|
|
"ROLES": ROLES,
|
|
"STATUSES": STATUSES,
|
|
"GROUP_CALL_PARTICIPANT_STALE_SECONDS": GROUP_CALL_PARTICIPANT_STALE_SECONDS,
|
|
}
|