feat: favorite aliases, incoming event feed (callback mode), rich messages with Premium gating - #176
Conversation
…(Claude Code) Adds callback mode as an alternative to blocking wait_for_settled_message: a background task consumes settled incoming bursts and appends them as JSONL lines to incoming_feed.jsonl, so a client that can wake an agent on file output (Claude Code's persistent Monitor on tail -F) gets one wake-up per settled burst with no blocking tool call held open. - Tools: enable_incoming_feed / disable_incoming_feed / incoming_feed_status (returns feed path + ready watch_command, shell-quoted, tail -F). - Env autostart: TELEGRAM_EVENT_FEED=1 (one-shot — a user disable is never resurrected by the next incoming message); TELEGRAM_EVENT_FEED_FILE overrides the path. Feed file is created 0600. - Off by default: Codex and other clients keep the exact previous wait_for_new_message / wait_for_settled_message behavior. - Robustness (from adversarial review): feed file validated before the consumer task starts (no orphan task on bad paths); bursts are popped only after a successful write (write errors retry instead of dropping); monotonic clock for settle math; out-of-order handler interleaving keeps min/max message ids; names sanitized in feed lines and wait_for_new_message. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…th Premium gating
Telegram's June 2026 Rich Messages (server-parsed full Markdown/HTML:
tables, #headings, formulas, footnotes, collapsible sections) are exposed
through send_message / reply_to_message / edit_message via new parse_mode
values 'rich'/'rich_markdown'/'rich_html', using Telethon 1.44's
InputRichMessageMarkdown/HTML over raw SendMessage/EditMessage requests
(the high-level Telethon API does not carry rich_message yet).
Rich sending is a Telegram Premium feature and Premium is transient (it
can expire or be bought at any time), so the account's premium flag is
re-checked on EVERY rich call, never cached. Without Premium nothing is
sent: the tool returns a structured
{"sent": false, "reason": "telegram_premium_required"} result telling the
agent to reformat with classic 'md'/'html' and retry. A Premium lapse
between check and send (PREMIUM RPC error) returns the same refusal.
edit_message also gains the previously missing classic parse_mode.
Live-verified from a user account (not a bot): server accepts
InputRichMessageMarkdown and parses tables into PageBlockTable; MCP
send_message with parse_mode='rich' returns {"sent": true, "rich": true}.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
6ccbc7f to
9fc340b
Compare
| await cl.edit_message(entity, message_id, new_text) | ||
| if parse_mode and parse_mode.lower() in RICH_PARSE_MODES: | ||
| return await _edit_rich(cl, entity, message_id, new_text, parse_mode.lower()) | ||
| await cl.edit_message(entity, message_id, new_text, parse_mode=parse_mode) |
There was a problem hiding this comment.
When callers omit parse_mode, this now passes None to Telethon, which explicitly disables parsing; the previous call omitted the argument and therefore used Telethon's default Markdown parser. Existing edits such as bold that used to render formatted will now be saved as literal text, so please only pass parse_mode when it is provided or preserve Telethon's omitted-argument sentinel.
|
|
||
| def _touch_feed_file() -> None: | ||
| """Create the feed file owner-only (0600) if missing; contact metadata is private.""" | ||
| feed_file_path().touch(mode=0o600, exist_ok=True) |
There was a problem hiding this comment.
When the feed file already exists, Path.touch(..., exist_ok=True) does not change its mode, so a pre-created or previously rotated 0644 file will keep receiving private contact/message metadata despite the 0600 guarantee. Please chmod an existing file or create/open the file through a mode-enforcing path before appending.
| # Monitor on `tail -f`) can wake an agent per event instead of the agent | ||
| # holding a blocking wait_for_settled_message call open. | ||
| _FEED_DEFAULT = Path(__file__).resolve().parent.parent.parent / "incoming_feed.jsonl" | ||
| _feed_task: Optional[asyncio.Task] = None |
There was a problem hiding this comment.
When this server is installed as a package under a read-only site-packages or container layer and TELEGRAM_EVENT_FEED_FILE is unset, enable_incoming_feed tries to create incoming_feed.jsonl next to the installed code and the opt-in feed fails even though the process cwd or user data directory may be writable. Please default this to a runtime/configurable data location rather than deriving it from __file__.
…d file mode and location
- edit_message: only forward parse_mode when the caller set it. Telethon
treats an explicit None as "disable parsing" while an omitted argument
uses its default parser, so passing None unconditionally turned
previously formatted edits (**bold**) into literal text.
- Feed file is now opened via os.open(O_CREAT|O_APPEND, 0o600) with an
fchmod fallback, so an existing or externally rotated 0644 file is
tightened too — Path.touch(mode=...) only applies its mode on creation.
- Feed default path no longer derives from __file__ (unwritable under a
read-only site-packages or container layer): it follows XDG state
(${XDG_STATE_HOME:-~/.local/state}/telegram-mcp/incoming_feed.jsonl) and
creates that directory. An explicit TELEGRAM_EVENT_FEED_FILE still must
point at an existing directory so typos fail loudly.
Tests: 6 new (default location, dir creation, 0600 on create/existing/
rotated, parse_mode forwarding); suite 200 passed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Thanks for the review — all three points were valid and are fixed in 9b76ba1.
Suite: 200 passed, |
…d file mode and location
- edit_message: only forward parse_mode when the caller set it. Telethon
treats an explicit None as "disable parsing" while an omitted argument
uses its default parser, so passing None unconditionally turned
previously formatted edits (**bold**) into literal text.
- Feed file is now opened via os.open(O_CREAT|O_APPEND, 0o600) with an
fchmod fallback, so an existing or externally rotated 0644 file is
tightened too — Path.touch(mode=...) only applies its mode on creation.
- Feed default path no longer derives from __file__ (unwritable under a
read-only site-packages or container layer): it follows XDG state
(${XDG_STATE_HOME:-~/.local/state}/telegram-mcp/incoming_feed.jsonl) and
creates that directory. An explicit TELEGRAM_EVENT_FEED_FILE still must
point at an existing directory so typos fail loudly.
Tests: 6 new (default location, dir creation, 0600 on create/existing/
rotated, parse_mode forwarding); suite 200 passed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat: favorite aliases, incoming event feed (callback mode), rich messages with Premium gating
Two features for agent-driven use of this server, rebased on current main (the contact-aliases + session-generator fixes from this series were already merged in #173 — thank you!).
1. Incoming event feed — callback mode (opt-in, default off)
Alternative to re-calling
wait_for_settled_message: a background task consumes settled incoming bursts and appends them as JSONL lines toincoming_feed.jsonl, so clients that can wake an agent on file output (e.g. Claude Code's persistent Monitor ontail -F) get one wake-up per burst with no blocking tool call held open.enable_incoming_feed/disable_incoming_feed/incoming_feed_status(returns a shell-quotedwatch_command).TELEGRAM_EVENT_FEED=1(one-shot — never resurrects an explicit disable);TELEGRAM_EVENT_FEED_FILEoverrides the path; the feed file is created 0600.wait_for_new_message.2. Rich messages (June 2026 server-side formatting) with Premium gating
send_message/reply_to_message/edit_messagegainparse_mode='rich'|'rich_markdown'|'rich_html'— full server-parsed Markdown/HTML (tables, headings, formulas, footnotes, collapsible sections) via Telethon 1.44'sInputRichMessageMarkdown/HTMLover raw requests (the high-level Telethon API doesn't carryrich_messageyet).edit_messagealso gains the previously missing classicparse_mode.{"sent": false, "reason": "telegram_premium_required"}so the agent can reformat with classicmd/htmland retry; a Premium lapse between check and send (PREMIUM RPC error) yields the same structured refusal.PageBlockTable.Tests
30 new tests across
test_event_feed.pyandtest_rich_messages.py; suite: 194 passed.black --checkclean, flake8 critical checks clean.🤖 Generated with Claude Code