From f0cb7748bf5797842df553061968568a1dea2a9f Mon Sep 17 00:00:00 2001 From: suselee Date: Sat, 11 Apr 2026 10:06:09 +0800 Subject: [PATCH 1/2] fix: use dynamic bash lookup and stdin message passing for Telegram scripts - shell_manager.py: replace hardcoded /bin/bash with shutil.which() lookup so it works on FreeBSD (/usr/local/bin/bash) and other non-Linux systems - telegram_send.py / telegram_edit.py: support --message - / --text - to read content from stdin, avoiding shell escaping issues with backticks, $variables, and quotes - SKILL.md: update command templates to prefer stdin piping via heredoc Co-Authored-By: Claude Opus 4.6 (1M context) --- src/bub/builtin/shell_manager.py | 3 +- src/skills/telegram/SKILL.md | 242 +++++++------ src/skills/telegram/scripts/telegram_edit.py | 211 +++++------ src/skills/telegram/scripts/telegram_send.py | 350 +++++++++---------- 4 files changed, 400 insertions(+), 406 deletions(-) diff --git a/src/bub/builtin/shell_manager.py b/src/bub/builtin/shell_manager.py index 41578f2a..3471d65d 100644 --- a/src/bub/builtin/shell_manager.py +++ b/src/bub/builtin/shell_manager.py @@ -3,6 +3,7 @@ import asyncio import contextlib import os +import shutil import uuid from dataclasses import dataclass, field @@ -39,7 +40,7 @@ async def start(self, *, cmd: str, cwd: str | None) -> ManagedShell: cwd=cwd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, - executable="/bin/bash" if os.name != "nt" else None, + executable=(shutil.which("bash") or shutil.which("sh")) if os.name != "nt" else None, ) shell = ManagedShell(shell_id=f"bash-{uuid.uuid4().hex[:8]}", cmd=cmd, cwd=cwd, process=process) shell.read_tasks.extend([ diff --git a/src/skills/telegram/SKILL.md b/src/skills/telegram/SKILL.md index ec0a2b5b..646a7ce9 100644 --- a/src/skills/telegram/SKILL.md +++ b/src/skills/telegram/SKILL.md @@ -1,125 +1,117 @@ ---- -name: telegram -description: | - Telegram Bot skill for sending and editing Telegram messages via Bot API. - Use when Bub needs to: (1) Send a message to a Telegram user/group/channel, - (2) Reply to a specific Telegram message with reply_to_message_id, - (3) Edit an existing Telegram message, or (4) Push proactive Telegram notifications. -metadata: - channel: telegram ---- - -# Telegram Skill - -Agent-facing execution guide for Telegram outbound communication. - -Assumption: `BUB_TELEGRAM_TOKEN` is already available. - -## Required Inputs -Collect these before execution: - -- `chat_id` (required) -- `message_id` (required for edit or reply) -- message content (required for send/edit) -- `reply_to_message_id` (required when you need a threaded reply) - -## Execution Policy - -1. If handling a Telegram message and `message_id` is known, send a reply message with `--reply-to`. -2. If there is no message to reply to, send a normal message to `chat_id`. -3. For long-running tasks, optionally send one progress message, then edit that same message for final status. -4. For multi-line text, pass the content via heredoc command substitution instead of embedding raw line breaks in quoted strings. -5. Avoid emitting HTML tags in message content; use Markdown for formatting instead. - -## Bot to co-Bot Communication - -In Telegram groups, communicate with another bot using only these patterns: - -1. Reply directly to the other bot's message when `message_id` is available. -2. Use an explicit command mention such as `/command@OtherBot` when you need to invoke that bot intentionally. -3. Do not assume free-form group text will reach another bot. - -## Active Response Policy - -When this skill is in scope, prefer proactive and timely Telegram updates: - -- Send an immediate acknowledgment for newly assigned tasks -- Send progress updates for long-running operations using message edits -- Send completion notifications when work finishes -- Send important status or failure notifications without waiting for follow-up prompts -- If execution is blocked or fails, send a problem report immediately with cause, impact, and next action - -Recommended pattern: - -1. Send a short acknowledgment reply -2. Continue processing -3. If blocked, edit or send an issue update immediately -4. Edit the acknowledgment message with final result when possible - -## Voice Message Policy - -When the inbound Telegram message is voice: - -1. Transcribe the voice input first (use STT skill if available) -2. Prepare response content based on transcription -3. Prefer voice response output (use TTS skill if available) -4. If voice output is unavailable, send a concise text fallback and state limitation - -## Reaction Policy - -When an inbound Telegram message warrants acknowledgment but does not merit a full reply, use a Telegram reaction as the response. -But when any explanation or details are needed, use a normal reply instead. - -## Command Templates - -Paths are relative to this skill directory. - -```bash -# Send simple text with SINGLE quotes -uv run ${SKILL_DIR}/scripts/telegram_send.py \ - --chat-id \ - --message '' - -# Or, send multi-line message using heredoc and double quotes -uv run ${SKILL_DIR}/scripts/telegram_send.py \ - --chat-id \ - --message "$(cat <<'EOF' -Build finished successfully. -Summary: -- 12 tests passed -- 0 failures -EOF -)" - -# Send reply to a specific message -uv run ${SKILL_DIR}/scripts/telegram_send.py \ - --chat-id \ - --message '' \ - --reply-to - -# Edit existing message -uv run ${SKILL_DIR}/scripts/telegram_edit.py \ - --chat-id \ - --message-id \ - --text '' -``` - -When sending message to a bot, either use `--reply-to` argument or pass `--source-is-bot` with `--source-username` otherwise the bot will not receive the message. - -For other actions that not covered by these scripts, use `curl` to call Telegram Bot API directly with the provided token. - -## Script Interface Reference - -### `telegram_send.py` - -- `--chat-id`, `-c`: required, supports comma-separated ids -- `--message`, `-m`: required -- `--reply-to`, `-r`: optional -- `--token`, `-t`: optional (normally not needed) - -### `telegram_edit.py` - -- `--chat-id`, `-c`: required -- `--message-id`, `-m`: required -- `--text`, `-t`: required -- `--token`: optional (normally not needed) +--- +name: telegram +description: | + Telegram Bot skill for sending and editing Telegram messages via Bot API. + Use when Bub needs to: (1) Send a message to a Telegram user/group/channel, + (2) Reply to a specific Telegram message with reply_to_message_id, + (3) Edit an existing Telegram message, or (4) Push proactive Telegram notifications. +metadata: + channel: telegram +--- + +# Telegram Skill + +Agent-facing execution guide for Telegram outbound communication. + +Assumption: `BUB_TELEGRAM_TOKEN` is already available. + +## Required Inputs +Collect these before execution: + +- `chat_id` (required) +- `message_id` (required for edit or reply) +- message content (required for send/edit) +- `reply_to_message_id` (required when you need a threaded reply) + +## Execution Policy + +1. If handling a Telegram message and `message_id` is known, send a reply message with `--reply-to`. +2. If there is no message to reply to, send a normal message to `chat_id`. +3. For long-running tasks, optionally send one progress message, then edit that same message for final status. +4. For multi-line text, pass the content via heredoc command substitution instead of embedding raw line breaks in quoted strings. +5. Avoid emitting HTML tags in message content; use Markdown for formatting instead. + +## Bot to co-Bot Communication + +In Telegram groups, communicate with another bot using only these patterns: + +1. Reply directly to the other bot's message when `message_id` is available. +2. Use an explicit command mention such as `/command@OtherBot` when you need to invoke that bot intentionally. +3. Do not assume free-form group text will reach another bot. + +## Active Response Policy + +When this skill is in scope, prefer proactive and timely Telegram updates: + +- Send an immediate acknowledgment for newly assigned tasks +- Send progress updates for long-running operations using message edits +- Send completion notifications when work finishes +- Send important status or failure notifications without waiting for follow-up prompts +- If execution is blocked or fails, send a problem report immediately with cause, impact, and next action + +Recommended pattern: + +1. Send a short acknowledgment reply +2. Continue processing +3. If blocked, edit or send an issue update immediately +4. Edit the acknowledgment message with final result when possible + +## Voice Message Policy + +When the inbound Telegram message is voice: + +1. Transcribe the voice input first (use STT skill if available) +2. Prepare response content based on transcription +3. Prefer voice response output (use TTS skill if available) +4. If voice output is unavailable, send a concise text fallback and state limitation + +## Reaction Policy + +When an inbound Telegram message warrants acknowledgment but does not merit a full reply, use a Telegram reaction as the response. +But when any explanation or details are needed, use a normal reply instead. + +## Command Templates + +Paths are relative to this skill directory. + +```bash +# Preferred: pipe message via stdin to avoid shell escaping issues +cat <<'EOF' | uv run ${SKILL_DIR}/scripts/telegram_send.py --chat-id --message - +Your message content here, with `backticks`, $variables, and "quotes" safely preserved. +EOF + +# Simple text (no special characters) +uv run ${SKILL_DIR}/scripts/telegram_send.py \ + --chat-id \ + --message 'simple text' + +# Reply via stdin +cat <<'EOF' | uv run ${SKILL_DIR}/scripts/telegram_send.py --chat-id --reply-to --message - +Reply content here. +EOF + +# Edit via stdin +cat <<'EOF' | uv run ${SKILL_DIR}/scripts/telegram_edit.py --chat-id --message-id --text - +Updated content here. +EOF +``` + +When sending message to a bot, either use `--reply-to` argument or pass `--source-is-bot` with `--source-username` otherwise the bot will not receive the message. + +For other actions that not covered by these scripts, use `curl` to call Telegram Bot API directly with the provided token. + +## Script Interface Reference + +### `telegram_send.py` + +- `--chat-id`, `-c`: required, supports comma-separated ids +- `--message`, `-m`: required (use `-` to read from stdin) +- `--reply-to`, `-r`: optional +- `--token`, `-t`: optional (normally not needed) + +### `telegram_edit.py` + +- `--chat-id`, `-c`: required +- `--message-id`, `-m`: required +- `--text`, `-t`: required (use `-` to read from stdin) +- `--token`: optional (normally not needed) diff --git a/src/skills/telegram/scripts/telegram_edit.py b/src/skills/telegram/scripts/telegram_edit.py index 1daf3893..f08305ce 100644 --- a/src/skills/telegram/scripts/telegram_edit.py +++ b/src/skills/telegram/scripts/telegram_edit.py @@ -1,105 +1,106 @@ -#!/usr/bin/env uv run -# /// script -# requires-python = ">=3.10" -# dependencies = [ -# "requests>=2.31.0", -# "telegramify-markdown>=0.5.0", -# ] -# /// - -""" -Telegram Bot Message Editor - -Edit an existing message via Telegram Bot API. -Uses telegramify_markdown to convert markdown to Telegram MarkdownV2 format. - -""" - -import argparse -import os -import sys - -import requests - -try: - from telegramify_markdown import markdownify -except ImportError: - print("❌ Error: telegramify_markdown not installed. Run: pip install telegramify-markdown") - sys.exit(1) - - -def unescape_newlines(text: str) -> str: - """ - Convert escaped newline sequences to real newlines. - Handles \\n -> \n, \\r\\n -> \r\n, etc. - """ - # First unescape \\n to real newline - result = text.replace("\\n", "\n") - result = result.replace("\\r\\n", "\r\n") - result = result.replace("\\r", "\r") - return result - - -def edit_message(bot_token: str, chat_id: str, message_id: int, text: str) -> dict: - """ - Edit an existing message via Telegram Bot API. - - Args: - bot_token: Telegram bot token - chat_id: Target chat ID - message_id: ID of the message to edit - text: New message text (will be converted to MarkdownV2) - - Returns: - API response as dict - """ - url = f"https://api.telegram.org/bot{bot_token}/editMessageText" - - # Unescape \\n sequences to real newlines (bash/argparse converts real newlines to \\n) - text = unescape_newlines(text) - - # Convert markdown to Telegram MarkdownV2 format - converted_text = markdownify(text).rstrip("\n") - - payload = { - "chat_id": chat_id, - "message_id": message_id, - "text": converted_text, - "parse_mode": "MarkdownV2", - } - - response = requests.post(url, json=payload, timeout=30) - response.raise_for_status() - - return response.json() - - -def main(): - parser = argparse.ArgumentParser(description="Edit an existing message via Telegram Bot API") - parser.add_argument("--chat-id", "-c", required=True, help="Target chat ID") - parser.add_argument("--message-id", "-m", type=int, required=True, help="ID of the message to edit") - parser.add_argument("--text", "-t", required=True, help="New message text (markdown supported)") - parser.add_argument("--token", help="Bot token (defaults to BUB_TELEGRAM_TOKEN env var)") - - args = parser.parse_args() - - # Get bot token - bot_token = args.token or os.environ.get("BUB_TELEGRAM_TOKEN") - if not bot_token: - print("❌ Error: Bot token required. Set BUB_TELEGRAM_TOKEN env var or use --token") - sys.exit(1) - - try: - edit_message(bot_token, args.chat_id, args.message_id, args.text) - print(f"✅ Message {args.message_id} edited successfully") - except requests.HTTPError as e: - print(f"❌ HTTP Error: {e}") - print(f" Response: {e.response.text}") - sys.exit(1) - except Exception as e: - print(f"❌ Error: {e}") - sys.exit(1) - - -if __name__ == "__main__": - main() +#!/usr/bin/env uv run +# /// script +# requires-python = ">=3.10" +# dependencies = [ +# "requests>=2.31.0", +# "telegramify-markdown>=0.5.0", +# ] +# /// + +""" +Telegram Bot Message Editor + +Edit an existing message via Telegram Bot API. +Uses telegramify_markdown to convert markdown to Telegram MarkdownV2 format. + +""" + +import argparse +import os +import sys + +import requests + +try: + from telegramify_markdown import markdownify +except ImportError: + print("❌ Error: telegramify_markdown not installed. Run: pip install telegramify-markdown") + sys.exit(1) + + +def unescape_newlines(text: str) -> str: + """ + Convert escaped newline sequences to real newlines. + Handles \\n -> \n, \\r\\n -> \r\n, etc. + """ + # First unescape \\n to real newline + result = text.replace("\\n", "\n") + result = result.replace("\\r\\n", "\r\n") + result = result.replace("\\r", "\r") + return result + + +def edit_message(bot_token: str, chat_id: str, message_id: int, text: str) -> dict: + """ + Edit an existing message via Telegram Bot API. + + Args: + bot_token: Telegram bot token + chat_id: Target chat ID + message_id: ID of the message to edit + text: New message text (will be converted to MarkdownV2) + + Returns: + API response as dict + """ + url = f"https://api.telegram.org/bot{bot_token}/editMessageText" + + # Unescape \\n sequences to real newlines (bash/argparse converts real newlines to \\n) + text = unescape_newlines(text) + + # Convert markdown to Telegram MarkdownV2 format + converted_text = markdownify(text).rstrip("\n") + + payload = { + "chat_id": chat_id, + "message_id": message_id, + "text": converted_text, + "parse_mode": "MarkdownV2", + } + + response = requests.post(url, json=payload, timeout=30) + response.raise_for_status() + + return response.json() + + +def main(): + parser = argparse.ArgumentParser(description="Edit an existing message via Telegram Bot API") + parser.add_argument("--chat-id", "-c", required=True, help="Target chat ID") + parser.add_argument("--message-id", "-m", type=int, required=True, help="ID of the message to edit") + parser.add_argument("--text", "-t", required=True, help="New message text (markdown supported)") + parser.add_argument("--token", help="Bot token (defaults to BUB_TELEGRAM_TOKEN env var)") + + args = parser.parse_args() + + # Get bot token + bot_token = args.token or os.environ.get("BUB_TELEGRAM_TOKEN") + if not bot_token: + print("❌ Error: Bot token required. Set BUB_TELEGRAM_TOKEN env var or use --token") + sys.exit(1) + + try: + text = sys.stdin.read() if args.text == "-" else args.text + edit_message(bot_token, args.chat_id, args.message_id, text) + print(f"✅ Message {args.message_id} edited successfully") + except requests.HTTPError as e: + print(f"❌ HTTP Error: {e}") + print(f" Response: {e.response.text}") + sys.exit(1) + except Exception as e: + print(f"❌ Error: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/src/skills/telegram/scripts/telegram_send.py b/src/skills/telegram/scripts/telegram_send.py index a4c25ff0..d4ed76fa 100755 --- a/src/skills/telegram/scripts/telegram_send.py +++ b/src/skills/telegram/scripts/telegram_send.py @@ -1,175 +1,175 @@ -#!/usr/bin/env uv run -# /// script -# requires-python = ">=3.10" -# dependencies = [ -# "requests>=2.31.0", -# "telegramify-markdown>=0.5.0", -# ] -# /// - -""" -Telegram Bot Message Sender - -A simple script to send messages via Telegram Bot API. -Uses telegramify_markdown to convert markdown to Telegram MarkdownV2 format. -""" - -import argparse -import os -import sys - -import requests - -try: - from telegramify_markdown import markdownify -except ImportError: - print("❌ Error: telegramify_markdown not installed. Run: pip install telegramify-markdown") - sys.exit(1) - - -def unescape_newlines(text: str) -> str: - """ - Convert escaped newline sequences to real newlines. - Handles \\n -> \n, \\r\\n -> \r\n, etc. - """ - # First unescape \\n to real newline - result = text.replace("\\n", "\n") - result = result.replace("\\r\\n", "\r\n") - result = result.replace("\\r", "\r") - return result - - -def edit_message(bot_token: str, chat_id: str, message_id: int, text: str) -> dict: - """ - Edit an existing message via Telegram Bot API. - - Uses telegramify_markdown to convert text to MarkdownV2 format. - - Args: - bot_token: Telegram bot token - chat_id: Target chat ID - message_id: ID of the message to edit - text: New message text (will be converted to MarkdownV2) - - Returns: - API response as dict - """ - url = f"https://api.telegram.org/bot{bot_token}/editMessageText" - - # Convert markdown to Telegram MarkdownV2 format - converted_text = markdownify(text) - - payload = { - "chat_id": chat_id, - "message_id": message_id, - "text": converted_text, - "parse_mode": "MarkdownV2", - } - - response = requests.post(url, json=payload, timeout=30) - response.raise_for_status() - - return response.json() - - -def send_message( - bot_token: str, - chat_id: str, - text: str, - reply_to_message_id: int | None = None, -) -> dict: - """ - Send a message via Telegram Bot API. - - Uses telegramify_markdown to convert text to MarkdownV2 format. - - Args: - bot_token: Telegram bot token - chat_id: Target chat ID - text: Message text (will be converted to MarkdownV2) - reply_to_message_id: Optional message ID to reply to - - Returns: - API response as dict - """ - url = f"https://api.telegram.org/bot{bot_token}/sendMessage" - - # Unescape \\n sequences to real newlines (bash/argparse converts real newlines to \\n) - text = unescape_newlines(text) - - # Convert markdown to Telegram MarkdownV2 format - converted_text = markdownify(text).rstrip("\n") - - payload = { - "chat_id": chat_id, - "text": converted_text, - "parse_mode": "MarkdownV2", - } - - if reply_to_message_id: - payload["reply_to_message_id"] = reply_to_message_id - - response = requests.post(url, json=payload, timeout=30) - if response.status_code == 400 and reply_to_message_id: - payload.pop("reply_to_message_id", None) - response = requests.post(url, json=payload, timeout=30) - response.raise_for_status() - - return response.json() - - -def main(): - parser = argparse.ArgumentParser(description="Send messages via Telegram Bot API (auto-converts to MarkdownV2)") - parser.add_argument("--chat-id", "-c", required=True, help="Target chat ID") - parser.add_argument( - "--message", - "-m", - required=True, - help="Message text to send (markdown supported, will be converted to MarkdownV2)", - ) - parser.add_argument("--token", "-t", help="Bot token (defaults to BUB_TELEGRAM_TOKEN env var)") - parser.add_argument("--reply-to", "-r", type=int, help="Message ID to reply to (creates threaded conversation)") - parser.add_argument( - "--source-is-bot", - action="store_true", - help="Set when source message sender is a bot; disables reply mode and switches to @username style send", - ) - parser.add_argument( - "--source-username", - help="Source username for @username prefix when --source-is-bot is enabled", - ) - - args = parser.parse_args() - - # Get bot token - bot_token = args.token or os.environ.get("BUB_TELEGRAM_TOKEN") - if not bot_token: - print("❌ Error: Bot token required. Set BUB_TELEGRAM_TOKEN env var or use --token") - sys.exit(1) - - # Parse chat IDs - chat_id = args.chat_id.strip() - reply_to = args.reply_to - message = args.message - - if args.source_is_bot and not reply_to and not message.startswith("/"): - if not args.source_username: - print("❌ Error: --source-username is required when --source-is-bot is set without --reply-to") - sys.exit(1) - message = f"/bot@{args.source_username} {message}" - - # Send messages - try: - send_message(bot_token, chat_id, message, reply_to) - print(f"✅ Message sent successfully to {chat_id} (MarkdownV2)") - except requests.HTTPError as e: - print(f"❌ HTTP Error: {e}") - print(f" Response: {e.response.text}") - sys.exit(1) - except Exception as e: - print(f"❌ Error: {e}") - sys.exit(1) - - -if __name__ == "__main__": - main() +#!/usr/bin/env uv run +# /// script +# requires-python = ">=3.10" +# dependencies = [ +# "requests>=2.31.0", +# "telegramify-markdown>=0.5.0", +# ] +# /// + +""" +Telegram Bot Message Sender + +A simple script to send messages via Telegram Bot API. +Uses telegramify_markdown to convert markdown to Telegram MarkdownV2 format. +""" + +import argparse +import os +import sys + +import requests + +try: + from telegramify_markdown import markdownify +except ImportError: + print("❌ Error: telegramify_markdown not installed. Run: pip install telegramify-markdown") + sys.exit(1) + + +def unescape_newlines(text: str) -> str: + """ + Convert escaped newline sequences to real newlines. + Handles \\n -> \n, \\r\\n -> \r\n, etc. + """ + # First unescape \\n to real newline + result = text.replace("\\n", "\n") + result = result.replace("\\r\\n", "\r\n") + result = result.replace("\\r", "\r") + return result + + +def edit_message(bot_token: str, chat_id: str, message_id: int, text: str) -> dict: + """ + Edit an existing message via Telegram Bot API. + + Uses telegramify_markdown to convert text to MarkdownV2 format. + + Args: + bot_token: Telegram bot token + chat_id: Target chat ID + message_id: ID of the message to edit + text: New message text (will be converted to MarkdownV2) + + Returns: + API response as dict + """ + url = f"https://api.telegram.org/bot{bot_token}/editMessageText" + + # Convert markdown to Telegram MarkdownV2 format + converted_text = markdownify(text) + + payload = { + "chat_id": chat_id, + "message_id": message_id, + "text": converted_text, + "parse_mode": "MarkdownV2", + } + + response = requests.post(url, json=payload, timeout=30) + response.raise_for_status() + + return response.json() + + +def send_message( + bot_token: str, + chat_id: str, + text: str, + reply_to_message_id: int | None = None, +) -> dict: + """ + Send a message via Telegram Bot API. + + Uses telegramify_markdown to convert text to MarkdownV2 format. + + Args: + bot_token: Telegram bot token + chat_id: Target chat ID + text: Message text (will be converted to MarkdownV2) + reply_to_message_id: Optional message ID to reply to + + Returns: + API response as dict + """ + url = f"https://api.telegram.org/bot{bot_token}/sendMessage" + + # Unescape \\n sequences to real newlines (bash/argparse converts real newlines to \\n) + text = unescape_newlines(text) + + # Convert markdown to Telegram MarkdownV2 format + converted_text = markdownify(text).rstrip("\n") + + payload = { + "chat_id": chat_id, + "text": converted_text, + "parse_mode": "MarkdownV2", + } + + if reply_to_message_id: + payload["reply_to_message_id"] = reply_to_message_id + + response = requests.post(url, json=payload, timeout=30) + if response.status_code == 400 and reply_to_message_id: + payload.pop("reply_to_message_id", None) + response = requests.post(url, json=payload, timeout=30) + response.raise_for_status() + + return response.json() + + +def main(): + parser = argparse.ArgumentParser(description="Send messages via Telegram Bot API (auto-converts to MarkdownV2)") + parser.add_argument("--chat-id", "-c", required=True, help="Target chat ID") + parser.add_argument( + "--message", + "-m", + required=True, + help="Message text to send (markdown supported, will be converted to MarkdownV2)", + ) + parser.add_argument("--token", "-t", help="Bot token (defaults to BUB_TELEGRAM_TOKEN env var)") + parser.add_argument("--reply-to", "-r", type=int, help="Message ID to reply to (creates threaded conversation)") + parser.add_argument( + "--source-is-bot", + action="store_true", + help="Set when source message sender is a bot; disables reply mode and switches to @username style send", + ) + parser.add_argument( + "--source-username", + help="Source username for @username prefix when --source-is-bot is enabled", + ) + + args = parser.parse_args() + + # Get bot token + bot_token = args.token or os.environ.get("BUB_TELEGRAM_TOKEN") + if not bot_token: + print("❌ Error: Bot token required. Set BUB_TELEGRAM_TOKEN env var or use --token") + sys.exit(1) + + # Parse chat IDs + chat_id = args.chat_id.strip() + reply_to = args.reply_to + message = sys.stdin.read() if args.message == "-" else args.message + + if args.source_is_bot and not reply_to and not message.startswith("/"): + if not args.source_username: + print("❌ Error: --source-username is required when --source-is-bot is set without --reply-to") + sys.exit(1) + message = f"/bot@{args.source_username} {message}" + + # Send messages + try: + send_message(bot_token, chat_id, message, reply_to) + print(f"✅ Message sent successfully to {chat_id} (MarkdownV2)") + except requests.HTTPError as e: + print(f"❌ HTTP Error: {e}") + print(f" Response: {e.response.text}") + sys.exit(1) + except Exception as e: + print(f"❌ Error: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main() From c77290920aee724f75bac9c89e55c824de672601 Mon Sep 17 00:00:00 2001 From: suselee Date: Sat, 11 Apr 2026 13:35:36 +0800 Subject: [PATCH 2/2] docs: enforce heredoc stdin for telegram scripts to prevent shell escaping errors --- src/skills/telegram/SKILL.md | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/skills/telegram/SKILL.md b/src/skills/telegram/SKILL.md index 646a7ce9..777ce8af 100644 --- a/src/skills/telegram/SKILL.md +++ b/src/skills/telegram/SKILL.md @@ -28,7 +28,7 @@ Collect these before execution: 1. If handling a Telegram message and `message_id` is known, send a reply message with `--reply-to`. 2. If there is no message to reply to, send a normal message to `chat_id`. 3. For long-running tasks, optionally send one progress message, then edit that same message for final status. -4. For multi-line text, pass the content via heredoc command substitution instead of embedding raw line breaks in quoted strings. +4. **ALWAYS pass message content via stdin using heredoc pipe and `--message -` (or `--text -`).** NEVER embed message text directly in shell arguments — special characters like `'`, `"`, `$`, `!` will be mangled or cause syntax errors. 5. Avoid emitting HTML tags in message content; use Markdown for formatting instead. ## Bot to co-Bot Communication @@ -75,23 +75,19 @@ But when any explanation or details are needed, use a normal reply instead. Paths are relative to this skill directory. ```bash -# Preferred: pipe message via stdin to avoid shell escaping issues -cat <<'EOF' | uv run ${SKILL_DIR}/scripts/telegram_send.py --chat-id --message - -Your message content here, with `backticks`, $variables, and "quotes" safely preserved. +# Send message (ALWAYS use heredoc stdin, never inline text in arguments) +cat << 'EOF' | uv run ${SKILL_DIR}/scripts/telegram_send.py --chat-id --message - +Your message content here. +Special characters are safe: $100, "quotes", 'apostrophes', !exclamation EOF -# Simple text (no special characters) -uv run ${SKILL_DIR}/scripts/telegram_send.py \ - --chat-id \ - --message 'simple text' - -# Reply via stdin -cat <<'EOF' | uv run ${SKILL_DIR}/scripts/telegram_send.py --chat-id --reply-to --message - +# Reply to a specific message +cat << 'EOF' | uv run ${SKILL_DIR}/scripts/telegram_send.py --chat-id --reply-to --message - Reply content here. EOF -# Edit via stdin -cat <<'EOF' | uv run ${SKILL_DIR}/scripts/telegram_edit.py --chat-id --message-id --text - +# Edit an existing message +cat << 'EOF' | uv run ${SKILL_DIR}/scripts/telegram_edit.py --chat-id --message-id --text - Updated content here. EOF ```