From 49a1f30a45a27f189e37cd425f6b998e943cb57f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bub=20=F0=9F=A4=96?= Date: Fri, 13 Mar 2026 08:20:27 +0000 Subject: [PATCH 1/2] feat(telegram): add user ID mention support via tg://user?id= links When users don't have a username, we can now mention them using tg://user?id=USER_ID links in Markdown format. Changes: - Add --source-user-id and --source-display-name args to telegram_send.py - Mention format: [Display Name](tg://user?id=USER_ID) - Falls back to @username if --source-username is provided - Falls back to @username if neither user ID nor username is provided - Update SKILL.md with documentation and examples This enables precise user mentions even for users without Telegram usernames. --- src/bub_skills/telegram/SKILL.md | 20 ++++++++--- .../telegram/scripts/telegram_send.py | 35 +++++++++++++++++-- 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/src/bub_skills/telegram/SKILL.md b/src/bub_skills/telegram/SKILL.md index e620389f..962bdd30 100644 --- a/src/bub_skills/telegram/SKILL.md +++ b/src/bub_skills/telegram/SKILL.md @@ -28,7 +28,7 @@ Collect these before execution: ## Execution Policy 1. If handling a direct user message in Telegram and `message_id` is known, send a reply message (`--reply-to`). -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 `@` (or the provided source username). +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 `@` (or the provided source username). If the user doesn't have a username, use `--source-user-id` to mention via `tg://user?id=` link. 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. @@ -91,13 +91,21 @@ uv run ./scripts/telegram_send.py \ --message "" \ --reply-to -# Source message sender is bot: no direct reply, use @user_id style +# Source message sender is bot: no direct reply, use @username style uv run ./scripts/telegram_send.py \ --chat-id \ --message "" \ --source-is-bot \ --source-username +# Source message sender is bot without username: use tg://user?id= link +uv run ./scripts/telegram_send.py \ + --chat-id \ + --message "" \ + --source-is-bot \ + --source-user-id \ + --source-display-name "Display Name" + # Edit existing message uv run ./scripts/telegram_edit.py \ --chat-id \ @@ -115,12 +123,14 @@ For other actions that not covered by these scripts, use `curl` to call Telegram - `--message`, `-m`: required - `--reply-to`, `-r`: optional - `--token`, `-t`: optional (normally not needed) -- `--source-is-bot`: optional flag, disables reply mode and switches to `@user_id` style -- `--source-user-id`: optional, required when `--source-is-bot` is set +- `--source-is-bot`: optional flag, disables reply mode and adds mention prefix +- `--source-username`: optional, uses `@username` style mention when set +- `--source-user-id`: optional, uses `tg://user?id=` link mention when username is not available +- `--source-display-name`: optional, display name for user ID mention (defaults to "User") ### `telegram_edit.py` - `--chat-id`, `-c`: required - `--message-id`, `-m`: required - `--text`, `-t`: required -- `--token`: optional (normally not needed) +- `--token`: optional (normally not needed) \ No newline at end of file diff --git a/src/bub_skills/telegram/scripts/telegram_send.py b/src/bub_skills/telegram/scripts/telegram_send.py index 94690689..e6919311 100755 --- a/src/bub_skills/telegram/scripts/telegram_send.py +++ b/src/bub_skills/telegram/scripts/telegram_send.py @@ -118,6 +118,14 @@ def send_message( return response.json() +def escape_markdown_v2(text: str) -> str: + """ + Escape special characters for Telegram MarkdownV2 format. + """ + escape_chars = r'_*[]()~`>#+-=|{}.!' + return ''.join('\\' + char if char in escape_chars else char for char in text) + + 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") @@ -138,6 +146,14 @@ def main(): "--source-username", help="Source username for @username prefix when --source-is-bot is enabled", ) + parser.add_argument( + "--source-user-id", + help="Source user ID for mention when username is not available (uses tg://user?id= link)", + ) + parser.add_argument( + "--source-display-name", + help="Display name for user ID mention (defaults to 'User')", + ) args = parser.parse_args() @@ -150,10 +166,25 @@ def main(): # Parse chat IDs chat_id = args.chat_id.strip() reply_to = args.reply_to + message = args.message + + # Handle source-is-bot mode: prefix message with mention + if args.source_is_bot: + if args.source_user_id: + # Use tg://user?id= link for mention (works without username) + display_name = args.source_display_name or "User" + # Escape the display name for MarkdownV2 + escaped_name = escape_markdown_v2(display_name) + mention = f"[{escaped_name}](tg://user?id={args.source_user_id})" + message = f"{mention}\n\n{message}" + elif args.source_username: + # Fall back to @username mention + message = f"@{args.source_username}\n\n{message}" + # If neither is provided, just send without mention prefix # Send messages try: - send_message(bot_token, chat_id, args.message, reply_to) + 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}") @@ -165,4 +196,4 @@ def main(): if __name__ == "__main__": - main() + main() \ No newline at end of file From e213beedc4427a4913f515333e5c8b7f1646dd91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bub=20=F0=9F=A4=96?= Date: Fri, 13 Mar 2026 08:29:10 +0000 Subject: [PATCH 2/2] fix: add missing newlines at end of files Fixes pre-commit end-of-file-fixer check in CI --- src/bub_skills/telegram/SKILL.md | 2 +- src/bub_skills/telegram/scripts/telegram_send.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bub_skills/telegram/SKILL.md b/src/bub_skills/telegram/SKILL.md index 962bdd30..bf0b17a4 100644 --- a/src/bub_skills/telegram/SKILL.md +++ b/src/bub_skills/telegram/SKILL.md @@ -133,4 +133,4 @@ For other actions that not covered by these scripts, use `curl` to call Telegram - `--chat-id`, `-c`: required - `--message-id`, `-m`: required - `--text`, `-t`: required -- `--token`: optional (normally not needed) \ No newline at end of file +- `--token`: optional (normally not needed) diff --git a/src/bub_skills/telegram/scripts/telegram_send.py b/src/bub_skills/telegram/scripts/telegram_send.py index e6919311..73b99565 100755 --- a/src/bub_skills/telegram/scripts/telegram_send.py +++ b/src/bub_skills/telegram/scripts/telegram_send.py @@ -196,4 +196,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main()