- Playwright держит открытыми веб-версии Outlook и Teams (постоянный профиль, Xvfb) - watcher.js перехватывает уведомления клиента и изменения карточек в реальном времени - pipeline: дедуп, baseline, VIP/MUTE, оценка важности через Haiku (OpenRouter) - aiogram-бот: уведомления, дайджест, вопросы по журналу, /login с 2FA-кодом, ручное управление браузером - Docker/compose, systemd-юнит, e2e-тест на поддельных страницах Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0194buWJjpDjU52JPX7PjsMs
105 lines
3.1 KiB
Python
105 lines
3.1 KiB
Python
"""Настройки из переменных окружения (.env)."""
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
from dataclasses import dataclass
|
||
from pathlib import Path
|
||
|
||
from dotenv import load_dotenv
|
||
|
||
load_dotenv()
|
||
|
||
ROOT = Path(__file__).resolve().parent.parent
|
||
|
||
|
||
def _list(name: str) -> list[str]:
|
||
return [x.strip() for x in os.getenv(name, "").split(",") if x.strip()]
|
||
|
||
|
||
def _bool(name: str, default: bool) -> bool:
|
||
v = os.getenv(name)
|
||
if v is None or v == "":
|
||
return default
|
||
return v.strip().lower() in ("1", "true", "yes", "on")
|
||
|
||
|
||
def _quiet(v: str | None) -> tuple[int, int] | None:
|
||
# "23-8" -> (23, 8)
|
||
if not v:
|
||
return None
|
||
try:
|
||
a, b = v.split("-")
|
||
return int(a), int(b)
|
||
except ValueError:
|
||
return None
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class Settings:
|
||
tg_token: str
|
||
owner_id: int
|
||
openrouter_key: str
|
||
model: str
|
||
uni_email: str
|
||
uni_password: str | None
|
||
my_name: str
|
||
mail_url: str
|
||
teams_url: str
|
||
data_dir: Path
|
||
headless: bool
|
||
chromium_path: str | None
|
||
notify_min: int
|
||
quiet_hours: tuple[int, int] | None
|
||
digest_time: str | None
|
||
tz: str
|
||
reload_hours: float
|
||
scan_interval: int
|
||
batch_window: float
|
||
vip: list[str]
|
||
mute: list[str]
|
||
rules_path: Path
|
||
selectors_path: Path
|
||
|
||
@property
|
||
def profile_dir(self) -> Path:
|
||
return self.data_dir / "browser-profile"
|
||
|
||
@property
|
||
def db_path(self) -> Path:
|
||
return self.data_dir / "uni.db"
|
||
|
||
|
||
def load() -> Settings:
|
||
missing = [n for n in ("TG_TOKEN", "OWNER_ID", "OPENROUTER_API_KEY", "UNI_EMAIL") if not os.getenv(n)]
|
||
if missing:
|
||
raise SystemExit(f"Не заданы переменные окружения: {', '.join(missing)} (см. .env.example)")
|
||
|
||
data_dir = Path(os.getenv("DATA_DIR", ROOT / "data"))
|
||
data_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
return Settings(
|
||
tg_token=os.environ["TG_TOKEN"],
|
||
owner_id=int(os.environ["OWNER_ID"]),
|
||
openrouter_key=os.environ["OPENROUTER_API_KEY"],
|
||
model=os.getenv("LLM_MODEL", "~anthropic/claude-haiku-latest"),
|
||
uni_email=os.environ["UNI_EMAIL"],
|
||
uni_password=os.getenv("UNI_PASSWORD") or None,
|
||
my_name=os.getenv("MY_NAME", ""),
|
||
mail_url=os.getenv("MAIL_URL", "https://outlook.office.com/mail/"),
|
||
teams_url=os.getenv("TEAMS_URL", "https://teams.microsoft.com/v2/"),
|
||
data_dir=data_dir,
|
||
headless=_bool("HEADLESS", False),
|
||
chromium_path=os.getenv("CHROMIUM_PATH") or None,
|
||
notify_min=int(os.getenv("NOTIFY_MIN_LEVEL", "2")),
|
||
quiet_hours=_quiet(os.getenv("QUIET_HOURS")),
|
||
digest_time=os.getenv("DIGEST_TIME") or None,
|
||
tz=os.getenv("TZ", "Europe/Warsaw"),
|
||
reload_hours=float(os.getenv("RELOAD_HOURS", "6")),
|
||
scan_interval=int(os.getenv("SCAN_INTERVAL", "120")),
|
||
batch_window=float(os.getenv("BATCH_WINDOW", "4")),
|
||
vip=_list("VIP"),
|
||
mute=_list("MUTE"),
|
||
rules_path=Path(os.getenv("RULES_PATH", ROOT / "config" / "rules.md")),
|
||
selectors_path=Path(os.getenv("SELECTORS_PATH", ROOT / "config" / "selectors.json")),
|
||
)
|