config.py
Rama main · réplica de lectura del repositorio original.
import os
from ipaddress import ip_network
from pathlib import Path
from urllib.parse import urlparse
from dotenv import load_dotenv
env_path = os.getenv("DOTENV_PATH", os.path.join(os.path.dirname(__file__), ".env"))
if os.getenv("LOAD_DOTENV", "true").lower() not in ("0", "false", "no"):
load_dotenv(dotenv_path=env_path)
BASE_PATH: Path = Path(__file__).resolve().parent
DB_PATH: Path = BASE_PATH / "database"
UPLOAD_FOLDER: Path = DB_PATH / "uploads"
CHATS_FOLDER: Path = DB_PATH / "chats"
CREATE_IMAGE_FOLDER: Path = DB_PATH / "generated_images"
PYTHON_RUNNER_QUEUE: Path = Path(os.getenv("PYTHON_RUNNER_QUEUE", "/var/lib/remind-python-runner"))
LOGS_FOLDER: Path = BASE_PATH / "logs"
for folder in [UPLOAD_FOLDER, CHATS_FOLDER, CREATE_IMAGE_FOLDER, LOGS_FOLDER]:
folder.mkdir(parents=True, exist_ok=True)
try:
MAX_CONTENT_LENGTH: int = int(os.getenv("MAX_CONTENT_LENGTH", 10 * 1024 * 1024))
except ValueError:
MAX_CONTENT_LENGTH: int = 10 * 1024 * 1024
try:
CHAT_MAX_VARIANTS_PER_TURN: int = max(
2, min(200, int(os.getenv("CHAT_MAX_VARIANTS_PER_TURN", "50")))
)
except ValueError:
CHAT_MAX_VARIANTS_PER_TURN: int = 50
ALLOWED_IMAGE_EXTENSIONS = {"png", "jpg", "jpeg", "webp", "gif"}
DEFAULT_LANGUAGE: str = "ru"
_flask_debug = os.getenv("FLASK_DEBUG", "0")
_is_debug = _flask_debug == "1" or _flask_debug.lower() == "true"
IS_PRODUCTION = not _is_debug and os.getenv("FLASK_ENV") != "development"
DEBUG_MODE = _is_debug
TESTING = False
def _env_bool(name: str, default: bool = False) -> bool:
raw = os.getenv(name)
if raw is None:
return default
return raw.lower() in ("1", "true", "yes", "on")
PYTHON_RUNNER_ENABLED = _env_bool("PYTHON_RUNNER_ENABLED", default=False)
def _sqlite_directory_usable(folder: Path) -> bool:
try:
folder.mkdir(parents=True, exist_ok=True)
probe = folder / f".sqlite_probe_{os.getpid()}.tmp"
probe.write_text("ok", encoding="utf-8")
probe.unlink()
return True
except Exception:
return False
def _normalize_host_entry(value: str) -> str:
raw = (value or "").strip()
if not raw:
return ""
if raw.startswith("."):
return raw.lower()
candidate = raw
if "://" in raw:
parsed = urlparse(raw)
candidate = parsed.hostname or ""
else:
parsed = urlparse(f"//{raw}")
candidate = parsed.hostname or raw
candidate = candidate.split("/")[0].split("?")[0].split("#")[0].strip()
if not candidate:
return ""
return candidate.lower()
ALLOWED_HOSTS = []
for host in ("127.0.0.1", "localhost"):
normalized = _normalize_host_entry(host)
if normalized:
ALLOWED_HOSTS.append(normalized)
if os.getenv("ALLOWED_HOSTS"):
for raw_host in os.getenv("ALLOWED_HOSTS").split(","):
normalized = _normalize_host_entry(raw_host)
if normalized and normalized not in ALLOWED_HOSTS:
ALLOWED_HOSTS.append(normalized)
CORS_ORIGINS = []
_DEV_CORS_ORIGINS = [
"http://localhost:3000",
"http://localhost:8000",
"http://127.0.0.1:3000",
"http://127.0.0.1:8000",
"http://localhost:5000",
"http://localhost:5173",
"http://127.0.0.1:5173",
]
if not IS_PRODUCTION:
CORS_ORIGINS.extend(_DEV_CORS_ORIGINS)
if os.getenv("CORS_ORIGINS"):
CORS_ORIGINS.extend([o.strip() for o in os.getenv("CORS_ORIGINS").split(",") if o.strip()])
CORS_ORIGINS = [o for o in CORS_ORIGINS if o != "*"]
CORS_ALLOW_LOCAL_ORIGINS = _env_bool("CORS_ALLOW_LOCAL_ORIGINS", default=not IS_PRODUCTION)
if not CORS_ALLOW_LOCAL_ORIGINS:
CORS_ORIGINS = [
origin
for origin in CORS_ORIGINS
if _normalize_host_entry(origin) not in {"localhost", "127.0.0.1", "0.0.0.0", "::1"}
]
CORS_ALLOW_HEADERS = [
"Content-Type",
"Authorization",
"X-Requested-With",
"X-CSRF-Token",
"X-Request-Id",
"Accept",
"Accept-Language",
]
CORS_EXPOSE_HEADERS = [
"Content-Type",
"X-CSRF-Token",
"X-Request-Id",
"X-Total-Count",
"X-Page-Number",
"X-Chat-Request-Id",
"X-Chat-Session-Token",
]
CORS_METHODS = ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"]
CORS_MAX_AGE = 3600
CORS_ALLOW_CREDENTIALS = True
CORS_SEND_WILDCARD = False
CORS_ALWAYS_SEND = True
_DEFAULT_OPERATIONAL_ALLOWED_NETWORKS = (
"127.0.0.1/32," "::1/128," "10.0.0.0/8," "172.16.0.0/12," "192.168.0.0/16," "fc00::/7"
)
_raw_operational_networks = os.getenv(
"OPERATIONAL_ENDPOINT_ALLOWED_NETWORKS",
_DEFAULT_OPERATIONAL_ALLOWED_NETWORKS,
)
OPERATIONAL_ENDPOINT_ALLOWED_NETWORKS = []
for raw_network in _raw_operational_networks.split(","):
candidate = raw_network.strip()
if not candidate:
continue
try:
OPERATIONAL_ENDPOINT_ALLOWED_NETWORKS.append(ip_network(candidate, strict=False))
except ValueError:
continue
PUBLIC_METRICS_ENABLED = _env_bool("PUBLIC_METRICS_ENABLED", default=False)
PUBLIC_OPENAPI_ENABLED = _env_bool("PUBLIC_OPENAPI_ENABLED", default=False)
SERVER_THREADS: int = 8
SERVER_CONNECTION_LIMIT: int = 200
SERVER_CHANNEL_TIMEOUT: int = 300
SECRET_KEY = os.getenv("SECRET_KEY")
_database_url = os.getenv("DATABASE_URL")
if _database_url:
_db_url = _database_url.strip()
if _db_url.startswith("sqlite:///"):
_sqlite_target = _db_url[len("sqlite:///") :]
if _sqlite_target and _sqlite_target != ":memory:":
if not Path(_sqlite_target).is_absolute():
_sqlite_path = (BASE_PATH / _sqlite_target).resolve()
target_parent = _sqlite_path.parent
else:
_sqlite_path = Path(_sqlite_target)
target_parent = _sqlite_path.parent
if not _sqlite_directory_usable(target_parent):
fallback_sqlite_path = (BASE_PATH / "users.db").resolve()
_sqlite_path = fallback_sqlite_path
_sqlite_path.parent.mkdir(parents=True, exist_ok=True)
SQLALCHEMY_DATABASE_URI = f"sqlite:///{_sqlite_path.as_posix()}"
else:
SQLALCHEMY_DATABASE_URI = _db_url
else:
SQLALCHEMY_DATABASE_URI = _db_url
else:
SQLALCHEMY_DATABASE_URI = f"sqlite:///{(DB_PATH / 'users.db').as_posix()}"
SQLALCHEMY_TRACK_MODIFICATIONS = False
REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379/0")
CELERY_BROKER_URL = os.getenv("CELERY_BROKER_URL", "redis://localhost:6379/1")
CELERY_RESULT_BACKEND = os.getenv("CELERY_RESULT_BACKEND", "redis://localhost:6379/1")
GOOGLE_CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID")
GOOGLE_CLIENT_SECRET = os.getenv("GOOGLE_CLIENT_SECRET")
APPLE_APP_BUNDLE_ID = (
os.getenv("APPLE_APP_BUNDLE_ID", "synvexai.remind").strip() or "synvexai.remind"
)
APPLE_SERVICE_ID = (os.getenv("APPLE_SERVICE_ID") or "").strip()
APPLE_WEB_REDIRECT_URI = (os.getenv("APPLE_WEB_REDIRECT_URI") or "").strip()
APPLE_CLIENT_SECRET = (os.getenv("APPLE_CLIENT_SECRET") or "").strip()
APPLE_TEAM_ID = (os.getenv("APPLE_TEAM_ID") or "").strip()
APPLE_KEY_ID = (os.getenv("APPLE_KEY_ID") or "").strip()
APPLE_PRIVATE_KEY = (os.getenv("APPLE_PRIVATE_KEY") or "").strip()
APPLE_PRIVATE_KEY_PATH = (os.getenv("APPLE_PRIVATE_KEY_PATH") or "").strip()
TELEGRAM_CLIENT_ID = (os.getenv("TELEGRAM_CLIENT_ID") or "").strip()
TELEGRAM_BOT_TOKEN = (os.getenv("TELEGRAM_BOT_TOKEN") or "").strip()
TELEGRAM_BOT_USERNAME = (os.getenv("TELEGRAM_BOT_USERNAME") or "").strip().lstrip("@")
TELEGRAM_BOT_API_BASE = (
os.getenv("TELEGRAM_BOT_API_BASE", "https://api.telegram.org").strip().rstrip("/")
or "https://api.telegram.org"
)
try:
TELEGRAM_BOT_POLL_TIMEOUT_SECONDS = min(
50, max(5, int(os.getenv("TELEGRAM_BOT_POLL_TIMEOUT_SECONDS", "30")))
)
except ValueError:
TELEGRAM_BOT_POLL_TIMEOUT_SECONDS = 30
try:
TELEGRAM_BOT_REQUEST_TIMEOUT_SECONDS = min(
120, max(5, int(os.getenv("TELEGRAM_BOT_REQUEST_TIMEOUT_SECONDS", "45")))
)
except ValueError:
TELEGRAM_BOT_REQUEST_TIMEOUT_SECONDS = 45
IOS_OAUTH_REDIRECT_URI = (
os.getenv("IOS_OAUTH_REDIRECT_URI", "remind://auth/google").strip() or "remind://auth/google"
)
IOS_OAUTH_CALLBACK_SCHEME = urlparse(IOS_OAUTH_REDIRECT_URI).scheme or "remind"
BACKEND_URL = os.getenv("BACKEND_URL", None)
if BACKEND_URL:
backend_host = _normalize_host_entry(BACKEND_URL)
if backend_host and backend_host not in ALLOWED_HOSTS:
ALLOWED_HOSTS.append(backend_host)
if not APPLE_WEB_REDIRECT_URI and APPLE_SERVICE_ID and BACKEND_URL:
_apple_backend_url = BACKEND_URL.strip().rstrip("/")
if urlparse(_apple_backend_url).scheme == "https":
APPLE_WEB_REDIRECT_URI = f"{_apple_backend_url}/login/apple/callback"
SESSION_COOKIE_DOMAIN = (os.getenv("SESSION_COOKIE_DOMAIN") or "").strip() or None
ALLOW_CROSS_SUBDOMAIN_SESSION_COOKIE = _env_bool(
"ALLOW_CROSS_SUBDOMAIN_SESSION_COOKIE", default=False
)
if SESSION_COOKIE_DOMAIN and not ALLOW_CROSS_SUBDOMAIN_SESSION_COOKIE:
SESSION_COOKIE_DOMAIN = None
SESSION_COOKIE_NAME = (
os.getenv("SESSION_COOKIE_NAME") or "remind_session"
).strip() or "remind_session"
TURNSTILE_SITE_KEY = os.getenv("TURNSTILE_SITE_KEY", "")
TURNSTILE_SECRET_KEY = os.getenv("TURNSTILE_SECRET_KEY", "")
TURNSTILE_VERIFY_URL = "https://challenges.cloudflare.com/turnstile/v0/siteverify"
LOCALHOST_MODE = os.getenv("LOCALHOST_MODE", "False").lower() in ("1", "true", "yes")
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
GEMINI_MODEL_NAME = os.getenv("GEMINI_MODEL_NAME")
try:
GEMINI_STREAM_TIMEOUT_MS = max(
30_000,
min(180_000, int(os.getenv("GEMINI_STREAM_TIMEOUT_MS", "120000"))),
)
except ValueError:
GEMINI_STREAM_TIMEOUT_MS = 120_000
ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY")
try:
ANTHROPIC_STREAM_TIMEOUT_MS = max(
30_000,
min(600_000, int(os.getenv("ANTHROPIC_STREAM_TIMEOUT_MS", "300000"))),
)
except ValueError:
ANTHROPIC_STREAM_TIMEOUT_MS = 300_000
AI_PROVIDER_API_KEY = os.getenv("AI_PROVIDER_API_KEY") or GEMINI_API_KEY
AI_PROVIDER_MODEL_NAME = os.getenv("AI_PROVIDER_MODEL_NAME") or GEMINI_MODEL_NAME
GITHUB_APP_ID = os.getenv("GITHUB_APP_ID", "").strip()
GITHUB_APP_CLIENT_ID = os.getenv("GITHUB_APP_CLIENT_ID", "").strip()
GITHUB_APP_CLIENT_SECRET = os.getenv("GITHUB_APP_CLIENT_SECRET", "").strip()
GITHUB_APP_SLUG = os.getenv("GITHUB_APP_SLUG", "").strip()
GITHUB_APP_PRIVATE_KEY = os.getenv("GITHUB_APP_PRIVATE_KEY", "").strip()
GITHUB_APP_PRIVATE_KEY_PATH = os.getenv("GITHUB_APP_PRIVATE_KEY_PATH", "").strip()
GITHUB_PUBLIC_BASE_URL = os.getenv("GITHUB_PUBLIC_BASE_URL", "").strip().rstrip("/")
GITHUB_BRANCH_PREFIX = os.getenv("GITHUB_BRANCH_PREFIX", "remind").strip() or "remind"
GITHUB_OAUTH_ENCRYPTION_KEY = os.getenv("GITHUB_OAUTH_ENCRYPTION_KEY", "").strip()
try:
GITHUB_OAUTH_STATE_TTL_SECONDS = max(
60, int(os.getenv("GITHUB_OAUTH_STATE_TTL_SECONDS", "600"))
)
except ValueError:
GITHUB_OAUTH_STATE_TTL_SECONDS = 600
try:
GITHUB_OAUTH_CREDENTIAL_TTL_SECONDS = max(
60, int(os.getenv("GITHUB_OAUTH_CREDENTIAL_TTL_SECONDS", "900"))
)
except ValueError:
GITHUB_OAUTH_CREDENTIAL_TTL_SECONDS = 900
try:
GITHUB_AGENT_MAX_FILE_CHARS = int(os.getenv("GITHUB_AGENT_MAX_FILE_CHARS", "80000"))
except ValueError:
GITHUB_AGENT_MAX_FILE_CHARS = 80000
try:
GITHUB_AGENT_MAX_PLAN_FILES = int(os.getenv("GITHUB_AGENT_MAX_PLAN_FILES", "8"))
except ValueError:
GITHUB_AGENT_MAX_PLAN_FILES = 8
if not SECRET_KEY:
if IS_PRODUCTION:
raise ValueError("SECRET_KEY must be set when production settings are active")
import secrets
SECRET_KEY = secrets.token_hex(32)
try:
PIL_AVAILABLE = True
except Exception:
PIL_AVAILABLE = False
USER_AGENT: str = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/124.0 Safari/537.36 ReMindBot/1.0"
)
ALLOWED_USER_AGENT_PATTERNS = [
r"Mozilla/5\.0.*Chrome/.*Safari",
r"Mozilla/5\.0.*Firefox/",
r"Mozilla/5\.0.*Safari/.*Version/",
r"Mozilla/5\.0.*Edg/",
r"Mozilla/5\.0.*OPR/",
r"AppleWebKit/.*Safari",
r"^com\.apple\.WebKit\.Networking/",
]
VALIDATE_USER_AGENT: bool = os.getenv("VALIDATE_USER_AGENT", "True").lower() in (
"1",
"true",
"yes",
)
BYPASS_USER_AGENT_VALIDATION_ROUTES = [
r"^/health",
r"^/metrics",
r"^/openapi\\.json$",
r"^/\.well-known/apple-app-site-association$",
r"^/status",
r"^/$",
r"^/index\.html$",
r"^/yandex_34c67fdadc366239\.html$",
]
ALLOW_GUEST_CHATS_SAVE: bool = os.getenv("ALLOW_GUEST_CHATS_SAVE", "False").lower() in (
"1",
"true",
"yes",
)
try:
TEMPORARY_CHAT_RETENTION_DAYS: int = int(os.getenv("TEMPORARY_CHAT_RETENTION_DAYS", "30"))
except ValueError:
TEMPORARY_CHAT_RETENTION_DAYS = 30
try:
SECURITY_LOG_RETENTION_DAYS: int = int(os.getenv("SECURITY_LOG_RETENTION_DAYS", "30"))
except ValueError:
SECURITY_LOG_RETENTION_DAYS = 30
WEB_SEARCH_ENABLED: bool = os.getenv("WEB_SEARCH_ENABLED", "True").lower() in (
"1",
"true",
"yes",
)
try:
WEB_SEARCH_FETCH_TIMEOUT_SECONDS: float = float(
os.getenv("WEB_SEARCH_FETCH_TIMEOUT_SECONDS", "12")
)
except ValueError:
WEB_SEARCH_FETCH_TIMEOUT_SECONDS = 12.0
try:
WEB_SEARCH_MAX_RESPONSE_BYTES: int = int(
os.getenv("WEB_SEARCH_MAX_RESPONSE_BYTES", str(512 * 1024))
)
except ValueError:
WEB_SEARCH_MAX_RESPONSE_BYTES = 512 * 1024
try:
WEB_SEARCH_PAGE_TEXT_CHARS: int = int(os.getenv("WEB_SEARCH_PAGE_TEXT_CHARS", "2200"))
except ValueError:
WEB_SEARCH_PAGE_TEXT_CHARS = 2200
ENABLE_STRICT_HTTPS = os.getenv(
"ENABLE_STRICT_HTTPS", "true" if IS_PRODUCTION else "false"
).lower() in ("1", "true", "yes")
RATELIMIT_ENABLED = True
RATELIMIT_STORAGE_URL = os.getenv("REDIS_URL", "memory://")