v1.3.34.1 -- Telegram notifications: HTML default¶
A focused notifications-channel patch on top of v1.3.34. Switches the default Telegram parse_mode from MarkdownV2 to HTML so event types like config_change (with their underscores) stop tripping the Telegram parser. Closes the ninth strike in the upstream- behaviour pattern.
Why¶
Through v1.3.34 the default Telegram template emitted *{{ .Type }}* (bold) without piping .Type through escapeMD. For event types containing underscores -- config_change, threat_ip_banned, cert_renewal_failed, etc. -- Telegram's MarkdownV2 parser reads the unescaped _ as "begin italic" and fails the request:
{"ok":false,"error_code":400,"description":"can't parse entities:
Character '_' is reserved and must be escaped with the preceding
'\\'"}
Operator dogfood revealed every config_change / threat_ip_banned / cert_* delivery silently logging as failed in the notification_deliveries table. The Telegram channel was effectively silent for the 12 most common event types.
The MarkdownV2 escape set has 18 reserved chars (_*[]()~\>#+-=|{}.!); HTML has 3 (<,>,&`). Switching the default to HTML makes the parser an order of magnitude more forgiving for arbitrary operator data ending up in event fields, with no expressivity loss for the default template's bold-Type + code-HostDomain shape.
What ships¶
Two Go-source changes¶
backend/internal/notifications/templates.go:
- New
escapeHTMLtemplate function. Wrapshtml.EscapeStringfrom the Go stdlib (covers<,>,&, plus quotes for defence-in-depth). Acceptsanyso callers can pipe string-typed aliases likeEventTypethrough it withoutprintfcoercion. escapeMDwidened to also acceptany(wasstring-only) for the same reason -- regression-safe since Sprintf("%v", s) on astringreturns it unchanged.- Default Telegram template rewritten as HTML:
{{ .Severity | severityEmoji }} <b>{{ .Type | escapeHTML }}</b>
{{ if .HostDomain }}host: <code>{{ .HostDomain | escapeHTML }}</code>{{ end }}
{{ .Message | escapeHTML }}
backend/internal/notifications/senders/telegram.go:
- Empty-
parse_modefallback flipped fromMarkdownV2toHTML. Channels withparse_modeexplicitly set keep their setting (no forced migration).
Unit tests¶
backend/internal/notifications/templates_test.go (new):
TestTelegramDefaultTemplateRendersValidHTML-- default output contains<b>config_change</b>and<code>host_with_under.example.com</code>, no MarkdownV2 backslash-escapes, and the Message's</>/&survive as</>/&.TestEscapeHTMLOnDynamicFields--<script>alert(1)</script>in HostDomain is rendered as<script>...</script>, no raw markup leaks.TestEscapeMDStillWorks-- regression: a custom MarkdownV2 template using{{ ... | escapeMD }}keeps producing the v1.3.21-era\_,\(,\),\>escapes for operators with pinned MarkdownV2 channels.
backend/internal/notifications/senders/telegram_test.go (new):
TestTelegramSenderDefaultsToHTMLParseMode-- mock Bot API server captures the form body; assertsparse_mode=HTMLwhen the channel config omits the field.TestTelegramSenderHonoursExplicitMarkdownV2-- the same mock server assertsparse_mode=MarkdownV2is honoured when set in channel config; no forced migration of pre-v1.3.34.1 channels.TestTelegramSenderSurfacesAPIError-- 400 with adescription: "can't parse entities..."body produces a wrapped Go error containing both the status code and the Telegram description, so the worker'snotification_deliveriesrow carries a useful error_message.
All five new tests + the three existing rate-limit tests pass: go test ./internal/notifications/... is green.
Documentation¶
docs/features/notifications.md -- new "parse_mode: HTML (default) vs MarkdownV2" subsection under the telegram channel config docs. Lists the trade-offs (3 reserved chars vs 18), quotes the default HTML template, links to Telegram Bot API HTML Style, documents the no-forced-migration policy for existing channels that pinned parse_mode: "MarkdownV2".
Why a four-component version (and no version-string bump)¶
This release ships behavioural code change but the operator chose to keep argosVersion and frontend/package.json at 1.3.33 for the same reason v1.3.34 did: a coherent "v1.3.33-binary-line" identifier through the doc-refresh + notification-fix patch range. The next behavioural release that warrants a version-string bump (a feature, schema migration, or non-trivial behavior change) will resume the three-component sequence at v1.3.35.
The four-component v1.3.34.1 tag exists for git history / GitHub Releases (so the docs portal lists it next to v1.3.34 under release notes), and the panel does require a rebuild to pick up the Go source changes -- it's not a tag-without-rebuild release like v1.3.27.1 / v1.3.34 were.
scripts/check-no-personal-data.sh clean.
Mid-impl gotcha (caught + fixed pre-tag)¶
Go template type strictness. Initial escapeHTML implementation was func(s string) string. Tests immediately failed:
template: tmpl:1:46: executing "tmpl" at <escapeHTML>:
wrong type for value; expected string;
got notifications.EventType
text/template does not auto-convert string-typed aliases (like type EventType string) into string for FuncMap call sites. The pre-v1.3.34.1 default template never piped .Type through any function -- it just emitted *{{ .Type }}* raw, which is exactly the bug. Fix: escapeHTML and escapeMD now both accept any and stringify via fmt.Sprintf("%v", v). No behaviour change for existing string callers.
Smoke gate¶
The smoke for this release is operator-mediated against a real Telegram channel:
- After
make sync-prod+ binary rebuild +make deploy-prod, open the panel's notifications settings. - Create a fresh Telegram channel pointing at an operator- owned bot + chat (do NOT include
parse_modein the channel config -- let the v1.3.34.1 default kick in). - Click "Send test" on the channel.
- Verify Telegram receives the test message with the event type rendered as bold (e.g. threat_ip_banned), the host in monospace, and no 400/failed delivery in the
notification_deliveriesaudit table. - (Optional) Repeat with a custom template that contains characters like
<script>in the message body; verify Telegram displays them as literal text, not as parsed HTML.
A unit-tested mock-server smoke (the three sender tests above) already verifies the request-path shape; the operator-mediated smoke is the EFFECT gate.
Files changed¶
backend/internal/notifications/templates.go(escapeHTML + default template + escapeMD signature widening)backend/internal/notifications/senders/telegram.go(parse_mode default flipped to HTML)backend/internal/notifications/templates_test.go(new)backend/internal/notifications/senders/telegram_test.go(new)docs/features/notifications.md(parse_mode subsection)docs/release-notes/v1.3.34.1.md(this file)CHANGELOG.md,mkdocs.yml
NOT changed: backend/cmd/argos/main.go argosVersion stays at 1.3.33; frontend/package.json version stays at 1.3.33; no migrations; no frontend behavior; no smokes under scripts/smoke/.
Upgrade¶
cd ~/argos-edge
git pull
make sync-prod # picks up Go source change + docs
make deploy-prod # rebuilds the panel binary; required
# because templates.go + telegram.go
# ship new Go code
After deploy-prod finishes, the operator-mediated smoke gate above replaces a scripted smoke for this release.
For operators with an existing Telegram channel that has been silently failing on config_change / threat_ip_banned deliveries since v1.3.21 -- those events will start being delivered correctly within seconds of the new binary serving traffic. No channel reconfiguration is required for channels without a pinned parse_mode. Channels with parse_mode: "MarkdownV2" set explicitly are honoured as-is.
Ninth-strike entry in the upstream-behaviour pattern¶
The eight-strike pattern documented in memory/project_four_strike_upstream_pattern.md becomes nine with this release. The new strike's lesson:
When a third-party parser offers multiple syntaxes, prefer the variant with the smallest reserved-char set. HTML's three reserved chars are an order of magnitude more forgiving than MarkdownV2's eighteen for arbitrary operator data ending up in event fields.
This applies prospectively to future Slack / Discord / other chat-sender additions: start from plain or HTML, never from the richest markup available.