From 2670c8cb468b2b492920e9db55a5fae3da4d4ec3 Mon Sep 17 00:00:00 2001 From: PTah Date: Fri, 29 May 2026 16:35:04 +1000 Subject: [PATCH] fix: sanitize daily report HTML for Telegram parse_mode Strip div/br tags unsupported by Telegram API; reports still store UI HTML in details Co-authored-by: Cursor --- backend/app/services/daily_report.py | 3 ++- backend/app/services/telegram_templates.py | 28 +++++++++++++++++----- backend/tests/test_daily_report.py | 9 +++++-- backend/tests/test_telegram_templates.py | 6 +++++ 4 files changed, 37 insertions(+), 9 deletions(-) diff --git a/backend/app/services/daily_report.py b/backend/app/services/daily_report.py index 7f7bd09..31a651f 100644 --- a/backend/app/services/daily_report.py +++ b/backend/app/services/daily_report.py @@ -198,7 +198,8 @@ def _build_report_body_rdp(host: Host, stats: dict[str, Any], when_local: dateti def _details_from_body(body: str, stats: dict[str, Any]) -> dict[str, Any]: escaped = html.escape(body) - report_html = '
' + escaped.replace("\n", "
\n") + "
" + # UI (Vue) допускает div; для единообразия — обёртка без br (Telegram читает через sanitize) + report_html = f'
{escaped.replace(chr(10), "
")}
' return { "stats": stats, "report_body": body, diff --git a/backend/app/services/telegram_templates.py b/backend/app/services/telegram_templates.py index abf841d..0907eeb 100644 --- a/backend/app/services/telegram_templates.py +++ b/backend/app/services/telegram_templates.py @@ -3,6 +3,7 @@ from __future__ import annotations import html +import re from datetime import datetime from typing import Any @@ -20,6 +21,25 @@ LOGON_TYPE_NAMES: dict[int, str] = { } +# Telegram parse_mode=HTML: только теги из Bot API (без div/br/p и т.д.) +_TELEGRAM_BR = re.compile(r"", re.I) +_TELEGRAM_DROP_TAGS = re.compile( + r"]*>", + re.I, +) +_TELEGRAM_SPAN_OPEN = re.compile(r"]*\btg-spoiler\b)[^>]*>", re.I) +_TELEGRAM_SPAN_CLOSE = re.compile(r"", re.I) + + +def sanitize_telegram_html(text: str) -> str: + """Приводит HTML отчёта (UI/агент) к тегам, допустимым в Telegram.""" + out = _TELEGRAM_BR.sub("\n", text) + out = _TELEGRAM_DROP_TAGS.sub("", out) + out = _TELEGRAM_SPAN_OPEN.sub("", out) + out = _TELEGRAM_SPAN_CLOSE.sub("", out) + return out.strip() + + def html_escape(value: Any) -> str: if value is None: return "" @@ -163,15 +183,11 @@ def format_daily_report_html(event: Event) -> str: details = _details_dict(event) report_html = details.get("report_html") if isinstance(report_html, str) and report_html.strip(): - # Уже HTML от агента или SAC - return report_html.strip() + return sanitize_telegram_html(report_html.strip()) body = _detail(details, "report_body", default=event.summary) if body != "-": escaped = html.escape(body) - return ( - f"📊 {html_escape(event.title)}\n" - f'
{escaped.replace(chr(10), "
")}
' - ) + return f"📊 {html_escape(event.title)}\n{escaped}" return format_generic_event_html(event) diff --git a/backend/tests/test_daily_report.py b/backend/tests/test_daily_report.py index 0921ecd..f300fe8 100644 --- a/backend/tests/test_daily_report.py +++ b/backend/tests/test_daily_report.py @@ -16,7 +16,7 @@ from app.services.daily_report import ( run_daily_reports, ) from app.services.notification_policy import NotificationPolicyConfig -from app.services.telegram_templates import format_event_telegram_html +from app.services.telegram_templates import format_event_telegram_html, sanitize_telegram_html def _ssh_host(db) -> Host: @@ -209,4 +209,9 @@ def test_daily_report_telegram_html_uses_report_html(db_session): details={"report_html": "📊 OK
line"}, payload={}, ) - assert format_event_telegram_html(event) == "📊 OK
line" + assert format_event_telegram_html(event) == "📊 OK\nline" + + +def test_sanitize_telegram_html_strips_div_and_br(): + raw = '
a
b
' + assert sanitize_telegram_html(raw) == "a\nb" diff --git a/backend/tests/test_telegram_templates.py b/backend/tests/test_telegram_templates.py index e4120f9..904e34a 100644 --- a/backend/tests/test_telegram_templates.py +++ b/backend/tests/test_telegram_templates.py @@ -10,6 +10,12 @@ def test_html_escape_special_chars(): assert html_escape("a&") == "a<b>&" +def test_sanitize_telegram_html_keeps_bold(): + from app.services.telegram_templates import sanitize_telegram_html + + assert sanitize_telegram_html("title
line") == "title\nline" + + def test_rdp_failed_template_includes_user_ip_logon(): host = Host(hostname="K6A-DC3", display_name="UNMS Kalina", os_family="windows") event = Event(