"""Effective notification channel config (DB overrides env).""" from __future__ import annotations from dataclasses import dataclass from sqlalchemy import select from sqlalchemy.orm import Session from app.config import get_settings from app.models.notification_channel import NotificationChannel VALID_SEVERITIES = frozenset({"info", "warning", "high", "critical"}) CHANNEL_TELEGRAM = "telegram" CHANNEL_WEBHOOK = "webhook" @dataclass(frozen=True) class TelegramConfig: enabled: bool bot_token: str chat_id: str min_severity: str source: str # env | db @property def configured(self) -> bool: return bool(self.bot_token.strip() and self.chat_id.strip()) @property def active(self) -> bool: return self.enabled and self.configured @dataclass(frozen=True) class WebhookConfig: enabled: bool url: str secret_header: str secret: str min_severity: str source: str # env | db @property def configured(self) -> bool: return bool(self.url.strip()) @property def active(self) -> bool: return self.enabled and self.configured def _telegram_from_env() -> TelegramConfig: settings = get_settings() return TelegramConfig( enabled=bool(settings.telegram_enabled), bot_token=settings.telegram_bot_token.strip(), chat_id=settings.telegram_chat_id.strip(), min_severity=settings.telegram_min_severity.strip() or "high", source="env", ) def get_effective_telegram_config(db: Session | None = None) -> TelegramConfig: if db is None: from app.database import SessionLocal session = SessionLocal() try: return get_effective_telegram_config(session) finally: session.close() row = db.scalar(select(NotificationChannel).where(NotificationChannel.channel == CHANNEL_TELEGRAM)) env_cfg = _telegram_from_env() if row is None: return env_cfg token = (row.bot_token or "").strip() or env_cfg.bot_token chat_id = (row.chat_id or "").strip() or env_cfg.chat_id min_sev = (row.min_severity or "").strip() or env_cfg.min_severity if min_sev not in VALID_SEVERITIES: min_sev = env_cfg.min_severity return TelegramConfig( enabled=bool(row.enabled), bot_token=token, chat_id=chat_id, min_severity=min_sev, source="db", ) def upsert_telegram_channel( db: Session, *, enabled: bool | None = None, bot_token: str | None = None, chat_id: str | None = None, min_severity: str | None = None, ) -> TelegramConfig: row = db.scalar(select(NotificationChannel).where(NotificationChannel.channel == CHANNEL_TELEGRAM)) env_cfg = _telegram_from_env() if row is None: row = NotificationChannel( channel=CHANNEL_TELEGRAM, enabled=env_cfg.enabled, bot_token=env_cfg.bot_token or None, chat_id=env_cfg.chat_id or None, min_severity=env_cfg.min_severity, ) db.add(row) if enabled is not None: row.enabled = enabled if min_severity is not None: if min_severity not in VALID_SEVERITIES: raise ValueError(f"invalid min_severity: {min_severity}") row.min_severity = min_severity if bot_token is not None and bot_token.strip(): row.bot_token = bot_token.strip() if chat_id is not None and chat_id.strip(): row.chat_id = chat_id.strip() db.commit() db.refresh(row) return get_effective_telegram_config(db) def _webhook_from_env() -> WebhookConfig: settings = get_settings() return WebhookConfig( enabled=bool(settings.webhook_enabled), url=settings.webhook_url.strip(), secret_header=settings.webhook_secret_header.strip(), secret=settings.webhook_secret.strip(), min_severity=settings.webhook_min_severity.strip() or "high", source="env", ) def get_effective_webhook_config(db: Session | None = None) -> WebhookConfig: if db is None: from app.database import SessionLocal session = SessionLocal() try: return get_effective_webhook_config(session) finally: session.close() row = db.scalar(select(NotificationChannel).where(NotificationChannel.channel == CHANNEL_WEBHOOK)) env_cfg = _webhook_from_env() if row is None: return env_cfg url = (row.webhook_url or "").strip() or env_cfg.url secret_header = (row.webhook_secret_header or "").strip() or env_cfg.secret_header secret = (row.webhook_secret or "").strip() or env_cfg.secret min_sev = (row.min_severity or "").strip() or env_cfg.min_severity if min_sev not in VALID_SEVERITIES: min_sev = env_cfg.min_severity return WebhookConfig( enabled=bool(row.enabled), url=url, secret_header=secret_header, secret=secret, min_severity=min_sev, source="db", ) def upsert_webhook_channel( db: Session, *, enabled: bool | None = None, url: str | None = None, secret_header: str | None = None, secret: str | None = None, min_severity: str | None = None, ) -> WebhookConfig: row = db.scalar(select(NotificationChannel).where(NotificationChannel.channel == CHANNEL_WEBHOOK)) env_cfg = _webhook_from_env() if row is None: row = NotificationChannel( channel=CHANNEL_WEBHOOK, enabled=env_cfg.enabled, min_severity=env_cfg.min_severity, webhook_url=env_cfg.url or None, webhook_secret_header=env_cfg.secret_header or None, webhook_secret=env_cfg.secret or None, ) db.add(row) if enabled is not None: row.enabled = enabled if min_severity is not None: if min_severity not in VALID_SEVERITIES: raise ValueError(f"invalid min_severity: {min_severity}") row.min_severity = min_severity if url is not None and url.strip(): row.webhook_url = url.strip() if secret_header is not None: row.webhook_secret_header = secret_header.strip() or None if secret is not None and secret.strip(): row.webhook_secret = secret.strip() db.commit() db.refresh(row) return get_effective_webhook_config(db)