Skip to content

Commit bb7c7eb

Browse files
author
Bub
committed
feat(telegram): add sendRichMessage support via telegram_rich.py
Add telegram_rich.py wrapping the Bot API 10.1 (June 2026) sendRichMessage endpoint, supporting both rich-html-style and rich-markdown-style payloads. Use --html-file / --markdown-file to pass large payloads from disk to avoid shell-escaping pitfalls, plus --edit MESSAGE_ID for in-place edits. Update SKILL.md to mark rich messages as the preferred path for anything beyond a single short paragraph, document the new script, and call out the tag set supported by rich-html-style.
1 parent e213bee commit bb7c7eb

2 files changed

Lines changed: 273 additions & 2 deletions

File tree

src/bub_skills/telegram/SKILL.md

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ description: |
66
(2) Reply to a specific Telegram message with reply_to_message_id,
77
(3) Edit an existing Telegram message, or (4) Push proactive Telegram notifications
88
when working outside an active Telegram session.
9+
Prefer rich formatted messages (headings, lists, tables, blockquotes, math, collapsible details, captioned media) over plain markdown whenever the content is more than one short paragraph.
910
metadata:
1011
channel: telegram
1112
---
@@ -25,13 +26,45 @@ Collect these before execution:
2526
- message content (required for send/edit)
2627
- `reply_to_message_id` (required for threaded reply behavior)
2728

29+
## Rich Messages (Bot API 10.1+, June 2026) — **PREFERRED**
30+
31+
For any message that benefits from structured formatting — headings, bullet/numbered lists, tables, blockquotes, collapsible `<details>`, captioned media, math blocks, code blocks with language hints, or mixed media — use the dedicated `telegram_rich.py` script with `rich-html-style` markup. This is the **first-choice** path; fall back to plain markdown (`telegram_send.py`) only for short single-paragraph replies.
32+
33+
The Rich Message API uses `sendRichMessage` and `editMessageText` with a `rich_message` payload. Exactly one of `html` or `markdown` must be supplied. Supported HTML tags include `<b>`, `<i>`, `<u>`, `<s>`, `<tg-spoiler>`, `<code>`, `<pre>`, `<mark>`, `<sub>`, `<sup>`, `<h1>`-`<h6>`, `<p>`, `<ul>`/`<ol>`/`<li>`, `<blockquote>`, `<aside>`, `<details>`/`<summary>`, `<table>`, `<figure>`/`<figcaption>`, `<img>`/`<video>`/`<audio>`, `<tg-collage>`, `<tg-slideshow>`, `<tg-map>`, `<tg-math>`, `<tg-math-block>`, `<tg-reference>`, `<tg-emoji>`, `<tg-time>`. Full reference: https://core.telegram.org/bots/api#rich-html-style
34+
35+
```bash
36+
# Send rich message (HTML)
37+
uv run ./scripts/telegram_rich.py \
38+
--chat-id <CHAT_ID> \
39+
--html "<h2>Title</h2><p>Body with <b>bold</b> and <code>code</code>.</p>"
40+
41+
# Send rich message (HTML from heredoc via file)
42+
uv run ./scripts/telegram_rich.py \
43+
--chat-id <CHAT_ID> \
44+
--html-file /tmp/payload.html \
45+
--reply-to <MESSAGE_ID>
46+
47+
# Send rich message (Markdown instead of HTML)
48+
uv run ./scripts/telegram_rich.py \
49+
--chat-id <CHAT_ID> \
50+
--markdown "## Title\n\n- item one\n- item two"
51+
52+
# Edit an existing message to become a rich message
53+
uv run ./scripts/telegram_rich.py \
54+
--chat-id <CHAT_ID> \
55+
--edit <MESSAGE_ID> \
56+
--html "<p>Updated content.</p>"
57+
```
58+
59+
For multi-line rich HTML, prefer `--html-file` over passing the payload on the command line to avoid shell escaping pitfalls. The script reads the file as UTF-8 and strips a single trailing newline.
60+
2861
## Execution Policy
2962

3063
1. If handling a direct user message in Telegram and `message_id` is known, send a reply message (`--reply-to`).
3164
2. If source metadata says sender is a bot (`sender_is_bot=true`), do not use reply mode, but send a normal message and prefix content with `@<sender_username>` (or the provided source username). If the user doesn't have a username, use `--source-user-id` to mention via `tg://user?id=` link.
3265
3. For long-running tasks, optionally send one progress message, then edit that same message for final status.
3366
4. For multi-line text, pass the content via heredoc command substitution instead of embedding raw line breaks in quoted strings.
34-
5. Avoid emitting HTML tags in message content; use Markdown for formatting instead.
67+
5. **Prefer `telegram_rich.py` (rich-html-style) over `telegram_send.py` for anything beyond a single short paragraph.** See the "Rich Messages" section above.
3568

3669
## Active Response Policy
3770

@@ -117,6 +150,20 @@ For other actions that not covered by these scripts, use `curl` to call Telegram
117150

118151
## Script Interface Reference
119152

153+
### `telegram_rich.py` (preferred for structured messages)
154+
155+
- `--chat-id`, `-c`: required
156+
- `--html`: rich message content as HTML
157+
- `--html-file`: read rich message HTML from a file (preferred for multi-line payloads)
158+
- `--markdown`: rich message content as Markdown
159+
- `--markdown-file`: read rich message Markdown from a file
160+
- `--rtl`: render right-to-left
161+
- `--skip-entity-detection`: skip URL/email/mention auto-detection
162+
- `--reply-to`, `-r`: optional message ID to reply to
163+
- `--disable-notification`: send silently
164+
- `--edit MESSAGE_ID`: edit an existing message instead of sending
165+
- `--token`, `-t`: optional (normally not needed)
166+
120167
### `telegram_send.py`
121168

122169
- `--chat-id`, `-c`: required, supports comma-separated ids
@@ -133,4 +180,4 @@ For other actions that not covered by these scripts, use `curl` to call Telegram
133180
- `--chat-id`, `-c`: required
134181
- `--message-id`, `-m`: required
135182
- `--text`, `-t`: required
136-
- `--token`: optional (normally not needed)
183+
- `--token`: optional (normally not needed)
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
#!/usr/bin/env uv run
2+
# /// script
3+
# requires-python = ">=3.10"
4+
# dependencies = [
5+
# "requests>=2.31.0",
6+
# ]
7+
# ///
8+
9+
"""
10+
Telegram Bot Rich Message Sender
11+
12+
Sends rich formatted messages via the Bot API 10.1 (June 2026) `sendRichMessage`
13+
endpoint using the `rich-html-style` markup.
14+
15+
Use this script as the **preferred** path whenever the message benefits from
16+
structured formatting (headings, lists, tables, blockquotes, collapsible
17+
details, math, captioned figures, etc.). Fall back to `telegram_send.py`
18+
(plain markdown) only for short single-paragraph replies.
19+
"""
20+
21+
import argparse
22+
import json
23+
import os
24+
import sys
25+
26+
import requests
27+
28+
29+
def send_rich_message(
30+
bot_token: str,
31+
chat_id: str,
32+
html: str | None = None,
33+
markdown: str | None = None,
34+
is_rtl: bool = False,
35+
skip_entity_detection: bool = False,
36+
reply_to_message_id: int | None = None,
37+
disable_notification: bool | None = None,
38+
) -> dict:
39+
"""
40+
Call `sendRichMessage` on the Bot API.
41+
42+
Exactly one of `html` or `markdown` must be supplied.
43+
44+
Returns:
45+
API response as dict
46+
"""
47+
if (html is None) == (markdown is None):
48+
raise ValueError("Exactly one of --html or --markdown must be supplied")
49+
50+
url = f"https://api.telegram.org/bot{bot_token}/sendRichMessage"
51+
52+
rich_message: dict = {}
53+
if html is not None:
54+
rich_message["html"] = html
55+
if markdown is not None:
56+
rich_message["markdown"] = markdown
57+
if is_rtl:
58+
rich_message["is_rtl"] = True
59+
if skip_entity_detection:
60+
rich_message["skip_entity_detection"] = True
61+
62+
payload: dict = {
63+
"chat_id": chat_id,
64+
"rich_message": json.dumps(rich_message, ensure_ascii=False),
65+
}
66+
if reply_to_message_id is not None:
67+
payload["reply_to_message_id"] = reply_to_message_id
68+
if disable_notification is not None:
69+
payload["disable_notification"] = disable_notification
70+
71+
response = requests.post(url, json=payload, timeout=30)
72+
if response.status_code == 400 and reply_to_message_id is not None:
73+
# Some channels don't accept threaded replies; retry without it.
74+
payload.pop("reply_to_message_id", None)
75+
response = requests.post(url, json=payload, timeout=30)
76+
response.raise_for_status()
77+
return response.json()
78+
79+
80+
def edit_rich_message(
81+
bot_token: str,
82+
chat_id: str,
83+
message_id: int,
84+
html: str | None = None,
85+
markdown: str | None = None,
86+
is_rtl: bool = False,
87+
) -> dict:
88+
"""Edit an existing message in-place to a rich message."""
89+
if (html is None) == (markdown is None):
90+
raise ValueError("Exactly one of --html or --markdown must be supplied")
91+
92+
url = f"https://api.telegram.org/bot{bot_token}/editMessageText"
93+
94+
rich_message: dict = {}
95+
if html is not None:
96+
rich_message["html"] = html
97+
if markdown is not None:
98+
rich_message["markdown"] = markdown
99+
if is_rtl:
100+
rich_message["is_rtl"] = True
101+
102+
payload: dict = {
103+
"chat_id": chat_id,
104+
"message_id": message_id,
105+
"rich_message": json.dumps(rich_message, ensure_ascii=False),
106+
}
107+
response = requests.post(url, json=payload, timeout=30)
108+
response.raise_for_status()
109+
return response.json()
110+
111+
112+
def read_payload_from_file(path: str) -> str:
113+
with open(path, "r", encoding="utf-8") as f:
114+
return f.read().rstrip("\n")
115+
116+
117+
def main():
118+
parser = argparse.ArgumentParser(
119+
description="Send rich formatted messages via Bot API 10.1 sendRichMessage (rich-html-style)",
120+
)
121+
parser.add_argument("--chat-id", "-c", required=True, help="Target chat ID")
122+
parser.add_argument(
123+
"--html",
124+
help="Rich message content as HTML. Mutually exclusive with --markdown.",
125+
)
126+
parser.add_argument(
127+
"--markdown",
128+
help="Rich message content as Markdown. Mutually exclusive with --html.",
129+
)
130+
parser.add_argument(
131+
"--html-file",
132+
help="Read rich message HTML from a file (useful for heredoc payloads).",
133+
)
134+
parser.add_argument(
135+
"--markdown-file",
136+
help="Read rich message Markdown from a file (useful for heredoc payloads).",
137+
)
138+
parser.add_argument(
139+
"--rtl",
140+
action="store_true",
141+
help="Render the message right-to-left.",
142+
)
143+
parser.add_argument(
144+
"--skip-entity-detection",
145+
action="store_true",
146+
help="Skip auto-detection of URLs/emails/mentions in the text.",
147+
)
148+
parser.add_argument(
149+
"--reply-to",
150+
"-r",
151+
type=int,
152+
help="Message ID to reply to (creates threaded conversation).",
153+
)
154+
parser.add_argument(
155+
"--disable-notification",
156+
action="store_true",
157+
help="Send silently.",
158+
)
159+
parser.add_argument(
160+
"--edit",
161+
type=int,
162+
metavar="MESSAGE_ID",
163+
help="Edit an existing message instead of sending a new one.",
164+
)
165+
parser.add_argument(
166+
"--token",
167+
"-t",
168+
help="Bot token (defaults to $BUB_TELEGRAM_TOKEN env var).",
169+
)
170+
171+
args = parser.parse_args()
172+
173+
bot_token = args.token or os.environ.get("BUB_TELEGRAM_TOKEN")
174+
if not bot_token:
175+
print("❌ Error: Bot token required. Set BUB_TELEGRAM_TOKEN env var or use --token")
176+
sys.exit(1)
177+
178+
html = args.html
179+
markdown = args.markdown
180+
if args.html_file:
181+
html = read_payload_from_file(args.html_file)
182+
if args.markdown_file:
183+
markdown = read_payload_from_file(args.markdown_file)
184+
185+
if (html is None) == (markdown is None):
186+
print("❌ Error: supply exactly one of --html, --html-file, --markdown, --markdown-file")
187+
sys.exit(1)
188+
189+
try:
190+
if args.edit is not None:
191+
result = edit_rich_message(
192+
bot_token=bot_token,
193+
chat_id=args.chat_id,
194+
message_id=args.edit,
195+
html=html,
196+
markdown=markdown,
197+
is_rtl=args.rtl,
198+
)
199+
print(f"✅ Rich message edited (message_id = {args.edit})")
200+
else:
201+
result = send_rich_message(
202+
bot_token=bot_token,
203+
chat_id=args.chat_id,
204+
html=html,
205+
markdown=markdown,
206+
is_rtl=args.rtl,
207+
skip_entity_detection=args.skip_entity_detection,
208+
reply_to_message_id=args.reply_to,
209+
disable_notification=args.disable_notification or None,
210+
)
211+
mid = result.get("result", {}).get("message_id")
212+
print(f"✅ Rich message sent successfully to {args.chat_id} (message_id = {mid})")
213+
except requests.HTTPError as e:
214+
print(f"❌ HTTP Error: {e}")
215+
body = e.response.text if e.response is not None else ""
216+
print(f" Response: {body[:1000]}")
217+
sys.exit(1)
218+
except Exception as e:
219+
print(f"❌ Error: {e}")
220+
sys.exit(1)
221+
222+
223+
if __name__ == "__main__":
224+
main()

0 commit comments

Comments
 (0)