Skip to content

Commit 6abdc94

Browse files
authored
Merge pull request chigwell#176 from ex3lite/feat/rich-messages
feat: favorite aliases, incoming event feed (callback mode), rich messages with Premium gating
2 parents 3109765 + 87c6171 commit 6abdc94

8 files changed

Lines changed: 777 additions & 38 deletions

File tree

.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
TELEGRAM_API_ID=123456
33
TELEGRAM_API_HASH=0123456789abcdef0123456789abcdef
44

5+
# --- Incoming event feed (optional, Claude Code callback mode; see README) ---
6+
# TELEGRAM_EVENT_FEED=1
7+
# TELEGRAM_EVENT_FEED_FILE=/path/to/incoming_feed.jsonl
8+
59
# --- Single account (backward-compatible) ---
610
# Option 1: File-based session (a .session file will be created)
711
TELEGRAM_SESSION_NAME=telegram_session

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,3 +200,4 @@ test_voice.ogg
200200
sticker.webp
201201
two.png
202202
aliases.json
203+
incoming_feed.jsonl

README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,26 @@ The server currently includes 80+ MCP tools grouped into these areas:
4545

4646
- **Accounts:** list configured accounts and route tool calls by account label.
4747
- **Chats and groups:** list chats, inspect metadata, create groups/channels, join or leave chats, invite users, manage admins, bans, default permissions, slow mode, topics, invite links, common chats, read receipts, and message links.
48-
- **Messages:** send, schedule, edit, delete, forward, pin, unpin, mark read, reply, search, inspect context, create polls, manage reactions, inspect inline buttons, and press inline callbacks.
48+
- **Messages:** send, schedule, edit, delete, forward, pin, unpin, mark read, reply, search, inspect context, create polls, manage reactions, inspect inline buttons, and press inline callbacks. `send_message`, `reply_to_message`, and `edit_message` support classic formatting (`parse_mode='md'`/`'html'`) and server-side rich formatting (`parse_mode='rich'`/`'rich_markdown'`/`'rich_html'` — full Markdown/HTML with tables, headings, formulas, and collapsible sections). Rich modes require Telegram Premium on the account; Premium is re-checked on every call, and without it nothing is sent — the tool returns a structured `telegram_premium_required` result so the agent can reformat with classic modes and retry.
4949
- **Contacts:** list, search, add, delete, block, unblock, import, export, inspect direct chats, find recent contact interactions, and manage favorite aliases (e.g. save "andrew" so any tool accepting a `chat_id` resolves it; searches check favorites first).
5050
- **Media:** send files, download media, upload files, send voice notes, stickers, GIFs, and inspect message media.
5151
- **Profile and privacy:** get your own account info, update profile fields, set or delete profile photos, inspect privacy settings, get user info/photos/status, and manage bot commands.
5252
- **Folders and drafts:** list, create, update, reorder, and delete Telegram folders; save, list, and clear drafts.
53+
- **Events:** wait for incoming messages with debounce (`wait_for_new_message`, `wait_for_settled_message`), or enable the opt-in incoming event feed for callback-style delivery (see below).
5354

5455
All tool results that include Telegram user-controlled content are sanitized and, where practical, returned as structured JSON.
5556

57+
### Incoming Event Feed (callback mode, Claude Code only)
58+
59+
By default, an agent waits for replies by calling `wait_for_settled_message`, which blocks up to the MCP tool timeout and must be re-called — that works everywhere (Codex, Cursor, etc.) and is unchanged.
60+
61+
Clients that can wake an agent on external output (Claude Code's persistent `Monitor` on `tail -f`) can switch to callback mode instead:
62+
63+
1. The agent calls `enable_incoming_feed` (or set `TELEGRAM_EVENT_FEED=1` in the environment to auto-enable). Each settled incoming burst is appended as one JSON line to `${XDG_STATE_HOME:-~/.local/state}/telegram-mcp/incoming_feed.jsonl`, created owner-only (0600). Override the path with `TELEGRAM_EVENT_FEED_FILE` — an explicit path's directory must already exist. `incoming_feed_status` reports the effective path and a ready-to-use watch command.
64+
2. The agent arms a persistent Monitor with the `watch_command` returned by the tool. Every new line re-invokes the agent with the burst summary; no blocking tool call is held open, and the chat stays free.
65+
66+
`disable_incoming_feed` switches back; `incoming_feed_status` reports the current mode. While the feed is enabled it consumes settled bursts, so don't combine it with `wait_for_settled_message`. Feed lines contain user-generated `name` fields — treat them as untrusted data.
67+
5668
## Requirements
5769

5870
- Python 3.10+

telegram_mcp/runtime.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -816,6 +816,46 @@ def format_entity(entity) -> Dict[str, Any]:
816816
return result
817817

818818

819+
# Parse modes that request server-side rich formatting (tables, headings,
820+
# formulas, collapsible sections — the June 2026 "Rich Messages" feature).
821+
# Sending rich messages requires Telegram Premium on the account.
822+
RICH_PARSE_MODES = {"rich", "rich_md", "rich_markdown", "rich_html"}
823+
824+
825+
async def account_is_premium(client) -> bool:
826+
"""Fresh Premium check at call time — Premium can expire or be bought anytime."""
827+
me = await client.get_me()
828+
return bool(getattr(me, "premium", False))
829+
830+
831+
def make_rich_input(parse_mode: str, text: str):
832+
"""Build the InputRichMessage payload for a rich parse mode."""
833+
if parse_mode == "rich_html":
834+
return types.InputRichMessageHTML(html=text)
835+
return types.InputRichMessageMarkdown(markdown=text)
836+
837+
838+
def premium_required_result(action: str) -> str:
839+
"""Structured refusal so the agent can degrade gracefully instead of sending garbage."""
840+
return json.dumps(
841+
{
842+
"sent": False,
843+
"reason": "telegram_premium_required",
844+
"detail": (
845+
f"{action} with rich formatting requires Telegram Premium on this account. "
846+
"Nothing was sent. Reformat without rich-only blocks (tables, headings, "
847+
"formulas) and retry with parse_mode='md' or 'html'."
848+
),
849+
},
850+
ensure_ascii=False,
851+
)
852+
853+
854+
def is_premium_rpc_error(error: Exception) -> bool:
855+
"""True when Telegram rejected a call because the account lacks Premium."""
856+
return "PREMIUM" in getattr(error, "message", str(error)).upper()
857+
858+
819859
_ALIASES_FILE = Path(__file__).resolve().parent.parent / "aliases.json"
820860

821861

0 commit comments

Comments
 (0)