Skip to content

feat: favorite aliases, incoming event feed (callback mode), rich messages with Premium gating - #176

Merged
chigwell merged 4 commits into
chigwell:mainfrom
ex3lite:feat/rich-messages
Aug 3, 2026
Merged

feat: favorite aliases, incoming event feed (callback mode), rich messages with Premium gating#176
chigwell merged 4 commits into
chigwell:mainfrom
ex3lite:feat/rich-messages

Conversation

@ex3lite

@ex3lite ex3lite commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

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 to incoming_feed.jsonl, so clients that can wake an agent on file output (e.g. Claude Code's persistent Monitor on tail -F) get one wake-up per burst with no blocking tool call held open.

  • Tools: enable_incoming_feed / disable_incoming_feed / incoming_feed_status (returns a shell-quoted watch_command).
  • Env autostart TELEGRAM_EVENT_FEED=1 (one-shot — never resurrects an explicit disable); TELEGRAM_EVENT_FEED_FILE overrides the path; the feed file is created 0600.
  • Hardened via adversarial review: feed path validated before the consumer starts (no orphan task on bad paths), bursts popped only after a successful write (write errors retry, never drop), monotonic clock for settle math, min/max ids under handler interleaving, sender names sanitized in feed lines and wait_for_new_message.
  • With the feed disabled (default) behavior is exactly as before — non-Claude clients are unaffected.
  • Live-tested: a 5-message burst debounced into one JSONL event that woke the agent once.

2. Rich messages (June 2026 server-side formatting) with Premium gating

send_message / reply_to_message / edit_message gain parse_mode='rich'|'rich_markdown'|'rich_html' — full server-parsed Markdown/HTML (tables, headings, formulas, footnotes, collapsible sections) via Telethon 1.44's InputRichMessageMarkdown/HTML over raw requests (the high-level Telethon API doesn't carry rich_message yet). edit_message also gains the previously missing classic parse_mode.

  • Rich sending requires Telegram Premium, and Premium is transient — the premium flag is re-checked on every rich call (never cached), so both "Premium expired" and "Premium just bought" behave correctly.
  • Without Premium nothing is sent: the tool returns {"sent": false, "reason": "telegram_premium_required"} so the agent can reformat with classic md/html and retry; a Premium lapse between check and send (PREMIUM RPC error) yields the same structured refusal.
  • Live-verified from a user account (not a bot): the server parses a Markdown table into PageBlockTable.

Tests

30 new tests across test_event_feed.py and test_rich_messages.py; suite: 194 passed. black --check clean, flake8 critical checks clean.

🤖 Generated with Claude Code

ex3lite and others added 3 commits August 2, 2026 12:44
…(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>
@ex3lite
ex3lite force-pushed the feat/rich-messages branch from 6ccbc7f to 9fc340b Compare August 2, 2026 04:44
Comment thread telegram_mcp/tools/messages.py Outdated
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)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread telegram_mcp/tools/events.py Outdated

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)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@ex3lite

ex3lite commented Aug 2, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review — all three points were valid and are fixed in 9b76ba1.

  1. edit_message parse_mode regression — confirmed against Telethon 1.44 (edit_message(..., parse_mode=()) uses () as the client-default sentinel, so an explicit None disabled parsing. The tool now forwards parse_mode only when the caller set it, preserving the previous Markdown-by-default behavior for existing callers; a test asserts the kwarg is omitted when unset and forwarded when given.

  2. 0600 not enforced on existing/rotated files — right, Path.touch(mode=...) only applies its mode on creation. Both the initial touch and every append now go through os.open(..., O_CREAT|O_APPEND, 0o600) with an fchmod on the open descriptor when the mode is looser (no TOCTOU window). Tests cover create, pre-existing 0644, and a file rotated to 0644 mid-run.

  3. Default path derived from __file__ — changed to the XDG state convention: ${XDG_STATE_HOME:-~/.local/state}/telegram-mcp/incoming_feed.jsonl, with that directory created on demand, so a read-only site-packages or container layer is no longer a problem. An explicit TELEGRAM_EVENT_FEED_FILE still requires its directory to exist, so a typo fails loudly instead of scattering directories. README updated.

Suite: 200 passed, black --check and flake8 critical checks clean.

@ex3lite
ex3lite requested a review from chigwell August 3, 2026 05:33
@chigwell
chigwell merged commit 01a3044 into chigwell:main Aug 3, 2026
4 checks passed
KiaroSama pushed a commit to KiaroSama/telegram-mcp that referenced this pull request Aug 25, 2026
…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>
KiaroSama pushed a commit to KiaroSama/telegram-mcp that referenced this pull request Aug 25, 2026
feat: favorite aliases, incoming event feed (callback mode), rich messages with Premium gating
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants