"""Dispatch ingest notifications per global policy (severity → channels).""" import logging from sqlalchemy.orm import Session from app.models import Event, Problem from app.services import email_notify, mobile_notify, telegram_notify, webhook_notify from app.services.notification_cooldown import should_notify_event, should_notify_problem from app.services.notification_policy import get_effective_notification_policy from app.services.event_type_visibility import event_type_notifications_enabled from app.services.host_health import HEARTBEAT_TYPE from app.services.notification_severity import severity_meets_minimum logger = logging.getLogger(__name__) LIFECYCLE_EVENT_TYPE = "agent.lifecycle" DAILY_REPORT_EVENT_TYPES = frozenset({"report.daily.ssh", "report.daily.rdp"}) AUTH_LOGIN_SUCCESS_TYPES = frozenset({"rdp.login.success", "ssh.login.success"}) RDG_CONNECTION_TYPES = frozenset({ "rdg.connection.success", "rdg.connection.disconnected", "rdg.connection.failed", }) def _event_telegram_via_agent(event: Event) -> bool: details = event.details if isinstance(event.details, dict) else {} via = str(details.get("telegram_via") or "").strip().lower() return via == "agent" def _skip_notifications_for_hidden_event(event: Event, db: Session | None) -> bool: if event_type_notifications_enabled(event.type, db): return False logger.info( "notify skipped hidden event type=%s event_id=%s", event.type, event.event_id, ) return True def _dispatch_event_channels(event: Event, *, db: Session | None, policy) -> None: if policy.use_telegram: telegram_notify.notify_event(event, db=db, apply_policy_gate=False) if policy.use_webhook: webhook_notify.notify_event(event, db=db, apply_policy_gate=False) if policy.use_email: email_notify.notify_event(event, db=db, apply_policy_gate=False) if policy.use_mobile: mobile_notify.notify_event(event, db=db, apply_policy_gate=False) def _dispatch_problem_channels(problem: Problem, event: Event | None, *, db: Session | None, policy) -> None: if policy.use_telegram: telegram_notify.notify_problem(problem, event, db=db, apply_policy_gate=False) if policy.use_webhook: webhook_notify.notify_problem(problem, event, db=db, apply_policy_gate=False) if policy.use_email: email_notify.notify_problem(problem, event, db=db, apply_policy_gate=False) if policy.use_mobile: mobile_notify.notify_problem(problem, event, db=db, apply_policy_gate=False) def notify_event(event: Event, *, db: Session | None = None) -> None: # Heartbeat — только для UI/статуса хоста, не для Telegram/email/push. if event.type == HEARTBEAT_TYPE: return if _skip_notifications_for_hidden_event(event, db): return policy = get_effective_notification_policy(db) if not severity_meets_minimum(event.severity, policy.min_severity): return if not should_notify_event(event, db): return _dispatch_event_channels(event, db=db, policy=policy) def notify_problem(problem: Problem, event: Event | None = None, *, db: Session | None = None) -> None: policy = get_effective_notification_policy(db) if not severity_meets_minimum(problem.severity, policy.min_severity): return if not should_notify_problem(problem, db): return _dispatch_problem_channels(problem, event, db=db, policy=policy) def _dispatch_lifecycle_channels(event: Event, *, db: Session | None, policy) -> None: skip_telegram = _event_telegram_via_agent(event) if policy.use_telegram and not skip_telegram: telegram_notify.notify_event(event, db=db, apply_policy_gate=False) if policy.use_webhook: webhook_notify.notify_event(event, db=db, apply_policy_gate=False) if policy.use_email: email_notify.notify_event(event, db=db, apply_policy_gate=False) if policy.use_mobile: mobile_notify.notify_event(event, db=db, apply_policy_gate=False) def notify_daily_report(event: Event, *, db: Session | None = None) -> None: """Оповещение по суточному отчёту (severity=info, вне порога policy). При UseSAC=dual агент шлёт TG сам (telegram_via=agent) — SAC не дублирует. """ if _skip_notifications_for_hidden_event(event, db): return policy = get_effective_notification_policy(db) if not should_notify_event(event, db): return _dispatch_lifecycle_channels(event, db=db, policy=policy) def notify_lifecycle(event: Event, *, db: Session | None = None) -> None: """Старт/стоп/reload агента — всегда в каналы SAC (кроме TG, если telegram_via=agent).""" if _skip_notifications_for_hidden_event(event, db): return policy = get_effective_notification_policy(db) if not should_notify_event(event, db): return _dispatch_lifecycle_channels(event, db=db, policy=policy) def notify_auth_login(event: Event, *, db: Session | None = None) -> None: """Успешный удалённый вход RDP/SSH — всегда в Telegram SAC (info вне min_severity). При UseSAC=dual агент шлёт TG сам (telegram_via=agent) — SAC не дублирует. """ if _skip_notifications_for_hidden_event(event, db): return policy = get_effective_notification_policy(db) if not should_notify_event(event, db): return _dispatch_lifecycle_channels(event, db=db, policy=policy) def notify_rdg_connection(event: Event, *, db: Session | None = None) -> None: """RD Gateway 302/303 — ingest всегда; Telegram SAC вне min_severity (как auth login).""" if _skip_notifications_for_hidden_event(event, db): return policy = get_effective_notification_policy(db) if not should_notify_event(event, db): return _dispatch_lifecycle_channels(event, db=db, policy=policy)