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 <cursoragent@cursor.com>
This commit is contained in:
@@ -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]:
|
def _details_from_body(body: str, stats: dict[str, Any]) -> dict[str, Any]:
|
||||||
escaped = html.escape(body)
|
escaped = html.escape(body)
|
||||||
report_html = '<div class="agent-report">' + escaped.replace("\n", "<br>\n") + "</div>"
|
# UI (Vue) допускает div; для единообразия — обёртка без br (Telegram читает через sanitize)
|
||||||
|
report_html = f'<div class="agent-report">{escaped.replace(chr(10), "<br>")}</div>'
|
||||||
return {
|
return {
|
||||||
"stats": stats,
|
"stats": stats,
|
||||||
"report_body": body,
|
"report_body": body,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import html
|
import html
|
||||||
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any
|
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"<br\s*/?>", re.I)
|
||||||
|
_TELEGRAM_DROP_TAGS = re.compile(
|
||||||
|
r"</?(?:div|p|ul|ol|li|hr|h[1-6]|table|tr|td|th|thead|tbody|body|html|head)\b[^>]*>",
|
||||||
|
re.I,
|
||||||
|
)
|
||||||
|
_TELEGRAM_SPAN_OPEN = re.compile(r"<span\b(?![^>]*\btg-spoiler\b)[^>]*>", re.I)
|
||||||
|
_TELEGRAM_SPAN_CLOSE = re.compile(r"</span>", 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:
|
def html_escape(value: Any) -> str:
|
||||||
if value is None:
|
if value is None:
|
||||||
return ""
|
return ""
|
||||||
@@ -163,15 +183,11 @@ def format_daily_report_html(event: Event) -> str:
|
|||||||
details = _details_dict(event)
|
details = _details_dict(event)
|
||||||
report_html = details.get("report_html")
|
report_html = details.get("report_html")
|
||||||
if isinstance(report_html, str) and report_html.strip():
|
if isinstance(report_html, str) and report_html.strip():
|
||||||
# Уже HTML от агента или SAC
|
return sanitize_telegram_html(report_html.strip())
|
||||||
return report_html.strip()
|
|
||||||
body = _detail(details, "report_body", default=event.summary)
|
body = _detail(details, "report_body", default=event.summary)
|
||||||
if body != "-":
|
if body != "-":
|
||||||
escaped = html.escape(body)
|
escaped = html.escape(body)
|
||||||
return (
|
return f"<b>📊 {html_escape(event.title)}</b>\n{escaped}"
|
||||||
f"<b>📊 {html_escape(event.title)}</b>\n"
|
|
||||||
f'<div class="agent-report">{escaped.replace(chr(10), "<br>")}</div>'
|
|
||||||
)
|
|
||||||
return format_generic_event_html(event)
|
return format_generic_event_html(event)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from app.services.daily_report import (
|
|||||||
run_daily_reports,
|
run_daily_reports,
|
||||||
)
|
)
|
||||||
from app.services.notification_policy import NotificationPolicyConfig
|
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:
|
def _ssh_host(db) -> Host:
|
||||||
@@ -209,4 +209,9 @@ def test_daily_report_telegram_html_uses_report_html(db_session):
|
|||||||
details={"report_html": "<b>📊 OK</b><br>line"},
|
details={"report_html": "<b>📊 OK</b><br>line"},
|
||||||
payload={},
|
payload={},
|
||||||
)
|
)
|
||||||
assert format_event_telegram_html(event) == "<b>📊 OK</b><br>line"
|
assert format_event_telegram_html(event) == "<b>📊 OK</b>\nline"
|
||||||
|
|
||||||
|
|
||||||
|
def test_sanitize_telegram_html_strips_div_and_br():
|
||||||
|
raw = '<div class="agent-report">a<br>b</div>'
|
||||||
|
assert sanitize_telegram_html(raw) == "a\nb"
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ def test_html_escape_special_chars():
|
|||||||
assert html_escape("a<b>&") == "a<b>&"
|
assert html_escape("a<b>&") == "a<b>&"
|
||||||
|
|
||||||
|
|
||||||
|
def test_sanitize_telegram_html_keeps_bold():
|
||||||
|
from app.services.telegram_templates import sanitize_telegram_html
|
||||||
|
|
||||||
|
assert sanitize_telegram_html("<b>title</b><br>line") == "<b>title</b>\nline"
|
||||||
|
|
||||||
|
|
||||||
def test_rdp_failed_template_includes_user_ip_logon():
|
def test_rdp_failed_template_includes_user_ip_logon():
|
||||||
host = Host(hostname="K6A-DC3", display_name="UNMS Kalina", os_family="windows")
|
host = Host(hostname="K6A-DC3", display_name="UNMS Kalina", os_family="windows")
|
||||||
event = Event(
|
event = Event(
|
||||||
|
|||||||
Reference in New Issue
Block a user