Ctrl K
ComunidadGuías…

config.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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://")