fix: Seaca push uses same lifecycle text as Telegram (0.9.11)
Reuse telegram templates for FCM title/body so agent start/restart reasons reach mobile without Seaca app changes. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -16,6 +16,7 @@ from app.models import Event, Problem
|
|||||||
from app.models.mobile_device import MobileDevice
|
from app.models.mobile_device import MobileDevice
|
||||||
from app.services.notification_severity import severity_meets_minimum
|
from app.services.notification_severity import severity_meets_minimum
|
||||||
from app.services.notification_policy import get_effective_notification_policy
|
from app.services.notification_policy import get_effective_notification_policy
|
||||||
|
from app.services.telegram_templates import format_event_mobile_push
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -170,8 +171,7 @@ def _send_fcm_data(
|
|||||||
|
|
||||||
|
|
||||||
def _event_payload(event: Event) -> tuple[str, str, dict[str, str]]:
|
def _event_payload(event: Event) -> tuple[str, str, dict[str, str]]:
|
||||||
title = f"[{event.severity}] {event.title}"
|
title, body = format_event_mobile_push(event)
|
||||||
body = (event.summary or "")[:200]
|
|
||||||
data = {
|
data = {
|
||||||
"kind": "event",
|
"kind": "event",
|
||||||
"id": str(event.id),
|
"id": str(event.id),
|
||||||
|
|||||||
@@ -506,6 +506,43 @@ def format_event_telegram_html(event: Event) -> str:
|
|||||||
return _append_event_source(_format_event_body_html(event), event)
|
return _append_event_source(_format_event_body_html(event), event)
|
||||||
|
|
||||||
|
|
||||||
|
_TELEGRAM_INLINE_TAGS = re.compile(r"</?(?:b|i|u|s|code|pre|tg-spoiler)(\s[^>]*)?>", re.I)
|
||||||
|
_TELEGRAM_ANCHOR_OPEN = re.compile(r"<a\b[^>]*>", re.I)
|
||||||
|
_TELEGRAM_ANCHOR_CLOSE = re.compile(r"</a>", re.I)
|
||||||
|
|
||||||
|
_MOBILE_PUSH_BODY_MAX = 3500
|
||||||
|
|
||||||
|
|
||||||
|
def telegram_html_to_plaintext(text: str) -> str:
|
||||||
|
"""Plain text for Seaca push from the same HTML templates as Telegram."""
|
||||||
|
out = sanitize_telegram_html(text)
|
||||||
|
out = _TELEGRAM_INLINE_TAGS.sub("", out)
|
||||||
|
out = _TELEGRAM_ANCHOR_OPEN.sub("", out)
|
||||||
|
out = _TELEGRAM_ANCHOR_CLOSE.sub("", out)
|
||||||
|
out = html.unescape(out)
|
||||||
|
out = re.sub(r"\n{3,}", "\n\n", out)
|
||||||
|
return out.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def format_event_mobile_push(event: Event) -> tuple[str, str]:
|
||||||
|
"""Title + body for FCM data payload (mirrors Telegram wording)."""
|
||||||
|
plain = telegram_html_to_plaintext(format_event_telegram_html(event))
|
||||||
|
lines = [line.strip() for line in plain.split("\n") if line.strip()]
|
||||||
|
if lines:
|
||||||
|
title = lines[0]
|
||||||
|
body = "\n".join(lines[1:]).strip()
|
||||||
|
if not body:
|
||||||
|
body = (event.summary or "").strip()
|
||||||
|
else:
|
||||||
|
title = f"[{event.severity}] {event.title}"
|
||||||
|
body = (event.summary or "").strip()
|
||||||
|
if len(title) > 120:
|
||||||
|
title = title[:117] + "…"
|
||||||
|
if len(body) > _MOBILE_PUSH_BODY_MAX:
|
||||||
|
body = body[: _MOBILE_PUSH_BODY_MAX - 1] + "…"
|
||||||
|
return title, body
|
||||||
|
|
||||||
|
|
||||||
def _format_rdg_html(event: Event) -> str:
|
def _format_rdg_html(event: Event) -> str:
|
||||||
details = _details_dict(event)
|
details = _details_dict(event)
|
||||||
if event.type.endswith(".success"):
|
if event.type.endswith(".success"):
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"""Единый источник версии SAC (API, health, логи, OpenAPI)."""
|
"""Единый источник версии SAC (API, health, логи, OpenAPI)."""
|
||||||
|
|
||||||
APP_NAME = "Security Alert Center"
|
APP_NAME = "Security Alert Center"
|
||||||
APP_VERSION = "0.9.10"
|
APP_VERSION = "0.9.11"
|
||||||
APP_VERSION_LABEL = f"{APP_NAME} v.{APP_VERSION}"
|
APP_VERSION_LABEL = f"{APP_NAME} v.{APP_VERSION}"
|
||||||
|
|||||||
@@ -4,6 +4,6 @@ from app.version import APP_NAME, APP_VERSION, APP_VERSION_LABEL
|
|||||||
|
|
||||||
|
|
||||||
def test_version_constants():
|
def test_version_constants():
|
||||||
assert APP_VERSION == "0.9.10"
|
assert APP_VERSION == "0.9.11"
|
||||||
assert APP_NAME == "Security Alert Center"
|
assert APP_NAME == "Security Alert Center"
|
||||||
assert APP_VERSION_LABEL == "Security Alert Center v.0.9.10"
|
assert APP_VERSION_LABEL == "Security Alert Center v.0.9.11"
|
||||||
|
|||||||
@@ -1,10 +1,33 @@
|
|||||||
"""FCM push must not break event ingest."""
|
"""FCM push must not break event ingest."""
|
||||||
|
|
||||||
|
from datetime import datetime, timezone
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from app.services.mobile_notify import MobileNotConfiguredError, _send_fcm_data
|
from app.models import Event, Host
|
||||||
|
from app.services.mobile_notify import MobileNotConfiguredError, _event_payload, _send_fcm_data
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_payload_lifecycle_uses_telegram_wording():
|
||||||
|
host = Host(hostname="WIN01", display_name="K6A-DC3", os_family="windows")
|
||||||
|
event = Event(
|
||||||
|
event_id="00000000-0000-4000-8000-000000000511",
|
||||||
|
host_id=1,
|
||||||
|
host=host,
|
||||||
|
occurred_at=datetime(2026, 5, 29, 9, 0, tzinfo=timezone.utc),
|
||||||
|
category="agent",
|
||||||
|
type="agent.lifecycle",
|
||||||
|
severity="info",
|
||||||
|
title="started",
|
||||||
|
summary="short",
|
||||||
|
details={"lifecycle": "started", "trigger": "settings_reload", "telegram_via": "sac"},
|
||||||
|
payload={"source": {"product": "ssh-monitor", "product_version": "1.4.0-SAC"}},
|
||||||
|
)
|
||||||
|
title, body, data = _event_payload(event)
|
||||||
|
assert title == "✅ Агент запущен"
|
||||||
|
assert "graceful restart" in body
|
||||||
|
assert data["type"] == "agent.lifecycle"
|
||||||
|
|
||||||
|
|
||||||
def test_send_fcm_data_swallows_token_errors_by_default():
|
def test_send_fcm_data_swallows_token_errors_by_default():
|
||||||
|
|||||||
@@ -3,7 +3,13 @@
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
from app.models import Event, Host
|
from app.models import Event, Host
|
||||||
from app.services.telegram_templates import format_event_telegram_html, format_rdp_login_html, html_escape
|
from app.services.telegram_templates import (
|
||||||
|
format_event_mobile_push,
|
||||||
|
format_event_telegram_html,
|
||||||
|
format_rdp_login_html,
|
||||||
|
html_escape,
|
||||||
|
telegram_html_to_plaintext,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_html_escape_special_chars():
|
def test_html_escape_special_chars():
|
||||||
@@ -145,6 +151,61 @@ def test_event_telegram_includes_sac_source_for_sac_daily_report():
|
|||||||
assert "📡 Оповещение: SAC (Security Alert Center)" in text
|
assert "📡 Оповещение: SAC (Security Alert Center)" in text
|
||||||
|
|
||||||
|
|
||||||
|
def test_telegram_html_to_plaintext_strips_tags():
|
||||||
|
assert telegram_html_to_plaintext("<b>✅ Агент запущен</b>\n🏢 Хост: WIN01") == (
|
||||||
|
"✅ Агент запущен\n🏢 Хост: WIN01"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_format_event_mobile_push_lifecycle_includes_trigger():
|
||||||
|
host = Host(hostname="WIN01", display_name="K6A-DC3", os_family="windows")
|
||||||
|
event = Event(
|
||||||
|
event_id="00000000-0000-4000-8000-000000000509",
|
||||||
|
host_id=1,
|
||||||
|
host=host,
|
||||||
|
occurred_at=datetime(2026, 5, 29, 9, 0, tzinfo=timezone.utc),
|
||||||
|
category="agent",
|
||||||
|
type="agent.lifecycle",
|
||||||
|
severity="info",
|
||||||
|
title="started",
|
||||||
|
summary="Мониторинг запущен",
|
||||||
|
details={"lifecycle": "started", "trigger": "deploy_recycle", "telegram_via": "sac"},
|
||||||
|
payload={"source": {"product": "rdp-login-monitor", "product_version": "1.2.30-SAC"}},
|
||||||
|
)
|
||||||
|
title, body = format_event_mobile_push(event)
|
||||||
|
assert title == "✅ Агент запущен"
|
||||||
|
assert "K6A-DC3" in body
|
||||||
|
assert "1.2.30-SAC" in body
|
||||||
|
assert "обновление скрипта" in body
|
||||||
|
assert "📡 Оповещение: SAC" in body
|
||||||
|
|
||||||
|
|
||||||
|
def test_format_event_mobile_push_uses_notification_body():
|
||||||
|
host = Host(hostname="WIN01", display_name="K6A-DC3", os_family="windows")
|
||||||
|
body_text = "✅ Мониторинг логинов ЗАПУЩЕН\n🖥️ Сервер: K6A-DC3 (10.0.0.1)"
|
||||||
|
event = Event(
|
||||||
|
event_id="00000000-0000-4000-8000-000000000510",
|
||||||
|
host_id=1,
|
||||||
|
host=host,
|
||||||
|
occurred_at=datetime(2026, 5, 29, 9, 0, tzinfo=timezone.utc),
|
||||||
|
category="agent",
|
||||||
|
type="agent.lifecycle",
|
||||||
|
severity="info",
|
||||||
|
title="started",
|
||||||
|
summary="Мониторинг запущен",
|
||||||
|
details={
|
||||||
|
"lifecycle": "started",
|
||||||
|
"trigger": "boot",
|
||||||
|
"notification_body": body_text,
|
||||||
|
"telegram_via": "sac",
|
||||||
|
},
|
||||||
|
payload={"source": {"product": "rdp-login-monitor", "product_version": "1.2.31-SAC"}},
|
||||||
|
)
|
||||||
|
title, body = format_event_mobile_push(event)
|
||||||
|
assert title == "✅ Мониторинг логинов ЗАПУЩЕН"
|
||||||
|
assert "K6A-DC3 (10.0.0.1)" in body
|
||||||
|
|
||||||
|
|
||||||
def test_lifecycle_started_template():
|
def test_lifecycle_started_template():
|
||||||
host = Host(hostname="WIN01", display_name="K6A-DC3", os_family="windows")
|
host = Host(hostname="WIN01", display_name="K6A-DC3", os_family="windows")
|
||||||
event = Event(
|
event = Event(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/** Fallback до загрузки /health; при релизе держите в sync с backend/app/version.py */
|
/** Fallback до загрузки /health; при релизе держите в sync с backend/app/version.py */
|
||||||
export const APP_NAME = "Security Alert Center";
|
export const APP_NAME = "Security Alert Center";
|
||||||
export const APP_VERSION = "0.9.10";
|
export const APP_VERSION = "0.9.11";
|
||||||
export const APP_VERSION_LABEL = `${APP_NAME} v.${APP_VERSION}`;
|
export const APP_VERSION_LABEL = `${APP_NAME} v.${APP_VERSION}`;
|
||||||
|
|||||||
Reference in New Issue
Block a user