From 96d1d3dd10ec6a29bab02ba3a395e619819afd9f Mon Sep 17 00:00:00 2001 From: schrodinger01 Date: Fri, 22 May 2026 15:39:45 +0800 Subject: [PATCH] fix(alerter): escape all dynamic numeric fields in Telegram MarkdownV2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Telegram's MarkdownV2 parser treats `.` as a special character and rejects the message with `Bad Request: can't parse entities` if any unescaped `.` appears outside a code or pre block. The current formatter only escapes the static `Market:` title and the `$` in the USDC amount, while leaving three numeric runs unescaped: *Risk Score:* 0.82 (HIGH) ← `.` in the score literal *Trade:* BUY Yes @ $0.075 | $15,000.00 ↑ ↑ price USDC amount In production this means well-formed alerts silently fail to reach Telegram users — the dispatcher reports a 400 from the Bot API and the event is dropped. This change routes every dynamic value through `_escape_telegram_markdown` before interpolation: - `assessment.weighted_score` (risk score) - `trade.price` (price string) - `format_usdc(trade.notional_value)` (USDC amount, including its `.`) - `trade.side` and `trade.outcome` (defensive — upstream values may contain `-` or `.` in future schema changes) The test that previously asserted `"0.82" in result.telegram_markdown` is updated to require the escaped form `"0\.82"`, and a new test pins down that none of `0.82` / `0.075` / `15,000.00` appear in unescaped form. --- .../alerter/formatter.py | 20 +++++++++++-------- tests/alerter/test_formatter.py | 19 ++++++++++++++++-- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/src/polymarket_insider_tracker/alerter/formatter.py b/src/polymarket_insider_tracker/alerter/formatter.py index b38fe8b..1a16494 100644 --- a/src/polymarket_insider_tracker/alerter/formatter.py +++ b/src/polymarket_insider_tracker/alerter/formatter.py @@ -290,12 +290,14 @@ class AlertFormatter: wallet_line += f" \\(Age: {age_hours:.0f}h\\)" lines.append(wallet_line) - # Risk score - lines.append(f"*Risk Score:* {assessment.weighted_score:.2f} \\({risk_level}\\)") + # Risk score — every numeric literal here must be escaped because + # MarkdownV2 treats `.` as a special character and rejects unescaped + # ones with `Bad Request: can't parse entities`. + score_str = self._escape_telegram_markdown(f"{assessment.weighted_score:.2f}") + lines.append(f"*Risk Score:* {score_str} \\({risk_level}\\)") # Market market_title = trade.event_title or trade.market_slug or "Unknown Market" - # Escape special Telegram markdown characters market_title_escaped = self._escape_telegram_markdown(market_title) if "market" in links: lines.append(f"*Market:* [{market_title_escaped}]({links['market']})") @@ -303,14 +305,16 @@ class AlertFormatter: lines.append(f"*Market:* {market_title_escaped}") # Trade details - usdc_value = format_usdc(trade.notional_value).replace("$", "\\$") - lines.append( - f"*Trade:* {trade.side} {trade.outcome} @ \\${trade.price:.3f} \\| {usdc_value}" - ) + usdc_value = self._escape_telegram_markdown(format_usdc(trade.notional_value)) + price_str = self._escape_telegram_markdown(f"{trade.price:.3f}") + side_escaped = self._escape_telegram_markdown(trade.side) + outcome_escaped = self._escape_telegram_markdown(trade.outcome) + lines.append(f"*Trade:* {side_escaped} {outcome_escaped} @ \\${price_str} \\| {usdc_value}") # Signals if signals: - lines.append(f"*Signals:* {', '.join(signals)}") + signals_escaped = [self._escape_telegram_markdown(s) for s in signals] + lines.append(f"*Signals:* {', '.join(signals_escaped)}") # Links lines.append("") diff --git a/tests/alerter/test_formatter.py b/tests/alerter/test_formatter.py index eeef5b1..a0953ec 100644 --- a/tests/alerter/test_formatter.py +++ b/tests/alerter/test_formatter.py @@ -409,12 +409,27 @@ class TestTelegramMarkdown: assert "`0x1234...5678`" in result.telegram_markdown def test_telegram_includes_risk_score(self, high_risk_assessment: RiskAssessment) -> None: - """Test that Telegram message includes risk score.""" + """Test that Telegram message includes risk score, MarkdownV2-escaped.""" formatter = AlertFormatter() result = formatter.format(high_risk_assessment) - assert "0.82" in result.telegram_markdown + # MarkdownV2 requires `.` to be escaped, so 0.82 becomes 0\.82. + assert "0\\.82" in result.telegram_markdown assert "HIGH" in result.telegram_markdown + def test_telegram_escapes_all_decimals(self, high_risk_assessment: RiskAssessment) -> None: + """Telegram MarkdownV2 rejects unescaped `.` in dynamic numeric + fields (risk score, price, USDC amount). All must be present in + their escaped form.""" + formatter = AlertFormatter() + result = formatter.format(high_risk_assessment) + md = result.telegram_markdown + for unescaped in ("0.82", "0.075", "15,000.00"): + assert unescaped not in md, ( + f"unescaped {unescaped!r} would be rejected by Telegram MarkdownV2: {md!r}" + ) + for escaped in ("0\\.82", "0\\.075", "15,000\\.00"): + assert escaped in md, f"missing escaped {escaped!r} in {md!r}" + def test_telegram_includes_links(self, high_risk_assessment: RiskAssessment) -> None: """Test that Telegram message includes links.""" formatter = AlertFormatter()