-
Notifications
You must be signed in to change notification settings - Fork 56
feat: Add Telegram transport support (Issue #11) #161
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -129,7 +129,7 @@ def _maybe_udp_emit(cfg: Dict[str, Any], event: Dict[str, Any]) -> None: | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _ALL_KINDS = ["like", "want", "bounty", "ad", "hello", "link", "event", "pay", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "pulse", "offer", "accept", "deliver", "confirm", "subscribe", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "mayday", "heartbeat", "accord"] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _ALL_TRANSPORTS = ["udp", "webhook", "discord", "bottube", "moltbook", "clawcities", "clawsta", "fourclaw", "pinchedin", "clawtasks", "clawnews", "rustchain"] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _ALL_TRANSPORTS = ["udp", "webhook", "telegram", "discord", "bottube", "moltbook", "clawcities", "clawsta", "fourclaw", "pinchedin", "clawtasks", "clawnews", "rustchain"] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _TOPIC_SUGGESTIONS = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "ai", "blockchain", "gaming", "vintage-hardware", "music", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "art", "science", "finance", "devtools", "security", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1139,6 +1139,71 @@ def cmd_clawnews_search(args: argparse.Namespace) -> int: | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # ── Discord ── | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def _telegram_client(cfg: Dict[str, Any], bot_token: Optional[str] = None) -> Any: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from .transports.telegram import TelegramClient | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| token = bot_token or _cfg_get(cfg, "telegram", "bot_token", default="") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not token: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.error("Missing telegram.bot_token in config or --bot-token.") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sys.exit(1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| timeout_s = int(_cfg_get(cfg, "telegram", "timeout_s", default=20) or 20) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return TelegramClient(bot_token=token, timeout_s=timeout_s) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def cmd_telegram_send(args: argparse.Namespace) -> int: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cfg = _load_config(args.config) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setup_logging(args.verbose) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from .transports.telegram import TelegramClient | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| extra, links = _parse_extra_and_links(args) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| kind = getattr(args, "kind", "hello") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| identity, _ = _ensure_identity(cfg, args) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| env = _build_envelope(cfg, kind, "telegram:webhook", links, extra, identity=identity) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| env = _build_envelope(cfg, kind, "telegram:webhook", links, extra, identity=identity) | |
| env = _build_envelope(cfg, kind, f"telegram:chat:{args.chat_id}", links, extra, identity=identity) |
Copilot
AI
Mar 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cmd_telegram_send/cmd_telegram_listen currently call helpers that don't exist in this file (_load_config, _parse_extra_and_links, _ensure_identity). This will raise NameError at runtime. Align this code with existing CLI patterns (e.g. use load_config() and _load_identity() and build extra/links from args directly) or add the missing helper implementations.
| cfg = _load_config(args.config) | |
| setup_logging(args.verbose) | |
| from .transports.telegram import TelegramClient | |
| extra, links = _parse_extra_and_links(args) | |
| kind = getattr(args, "kind", "hello") | |
| identity, _ = _ensure_identity(cfg, args) | |
| env = _build_envelope(cfg, kind, "telegram:webhook", links, extra, identity=identity) | |
| token = args.bot_token or _cfg_get(cfg, "telegram", "bot_token", default="") | |
| if not token: | |
| logger.error("Missing bot_token.") | |
| return 1 | |
| cfg = load_config(args.config) | |
| setup_logging(args.verbose) | |
| from .transports.telegram import TelegramClient | |
| # Build extra and links directly from args, following existing CLI patterns. | |
| extra = getattr(args, "extra", None) or {} | |
| links = getattr(args, "links", None) or [] | |
| kind = getattr(args, "kind", "hello") | |
| identity = _load_identity(args) | |
| env = _build_envelope(cfg, kind, "telegram:webhook", links, extra, identity=identity) | |
| token = args.bot_token or _cfg_get(cfg, "telegram", "bot_token", default="") | |
| if not token: | |
| logger.error("Missing bot_token.") | |
| return 1 |
Copilot
AI
Mar 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
env = _build_envelope(...) returns an encoded envelope string, but it's passed as envelope=env into TelegramClient.send_message(), which expects a dict and uses .get(...). Either pass the envelope as part of the message text (e.g. payload_text = f"{text}\n\n{env}") or change both sides to use a consistent envelope representation (string vs decoded dict).
Copilot
AI
Mar 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
New Telegram transport and CLI entrypoints aren't covered by tests, while similar transports (e.g. tests/test_discord_transport.py) and CLI behaviors (e.g. tests/test_cli_json_flag.py) are tested. Add unit tests for (1) TelegramClient request/response handling (mocking requests.Session) and (2) CLI parsing/dispatch for beacon telegram send/listen to prevent regressions like missing set_defaults(func=...).
Copilot
AI
Mar 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The help text says "Listen for Telegram webhook updates", but the implementation uses long-polling via /getUpdates (not webhooks). Update the CLI help/command naming to reflect polling, or implement webhook-based receiving if that's the intent.
| sp_listen = tg_sub.add_parser("listen", help="Listen for Telegram webhook updates") | |
| sp_listen = tg_sub.add_parser("listen", help="Listen for Telegram updates via long polling") |
Copilot
AI
Mar 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The telegram send / telegram listen subparsers don't call set_defaults(func=...). Since main() unconditionally executes args.func(args), running beacon telegram ... will fail with AttributeError: 'Namespace' object has no attribute 'func'. Add sp_send.set_defaults(func=cmd_telegram_send) and sp_listen.set_defaults(func=cmd_telegram_listen) (and any other needed defaults).
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,75 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||
| """Telegram transport for Beacon.""" | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| import logging | ||||||||||||||||||||||||||||||||||||||||||||||||
| import requests | ||||||||||||||||||||||||||||||||||||||||||||||||
| import time | ||||||||||||||||||||||||||||||||||||||||||||||||
| from typing import Dict, Any, List | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
| from typing import Dict, Any, List | |
| from typing import Dict, Any |
Copilot
AI
Mar 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TelegramClient.send_message() treats envelope as a dict and calls .get('id'/'kind'), but callers (e.g. cmd_telegram_send) pass the encoded envelope string from _build_envelope(). This will raise at runtime, and even a decoded envelope dict does not contain an id field (the codec uses fields like nonce, agent_id, kind). Consider changing send_message() to accept an encoded envelope str (and append it to the message text like the Discord CLI does), or change the CLI to pass a decoded envelope dict and read the correct fields.
| def send_message(self, chat_id: str, text: str, envelope: Dict[str, Any] = None) -> Dict[str, Any]: | |
| url = f"{self.base_url}/sendMessage" | |
| message_text = text | |
| if envelope: | |
| message_text += f"\n\n[Beacon]: {envelope.get('id', 'N/A')} ({envelope.get('kind', 'N/A')})" | |
| def send_message(self, chat_id: str, text: str, envelope: Any = None) -> Dict[str, Any]: | |
| url = f"{self.base_url}/sendMessage" | |
| message_text = text | |
| if envelope: | |
| # Handle both encoded envelope strings and decoded envelope dicts. | |
| if isinstance(envelope, str): | |
| # Encoded envelope string, e.g. from _build_envelope(). | |
| message_text += f"\n\n[Beacon]: {envelope}" | |
| elif isinstance(envelope, dict): | |
| # Decoded envelope dict; use known fields when available. | |
| envelope_id = envelope.get("nonce") or envelope.get("agent_id") or "N/A" | |
| envelope_kind = envelope.get("kind", "N/A") | |
| message_text += f"\n\n[Beacon]: {envelope_id} ({envelope_kind})" | |
| else: | |
| # Fallback for unexpected envelope types. | |
| message_text += f"\n\n[Beacon]: {str(envelope)}" |
Copilot
AI
Mar 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All other HTTP transports in this repo wrap requests with the shared with_retry helper (e.g. beacon_skill/transports/bottube.py:28-55) and set a User-Agent on the session. This transport currently does a single post()/get() with minimal error parsing, which makes it less resilient to transient network failures and rate limits. Consider adding a small _request() wrapper using with_retry, parsing Telegram error JSON (description), and setting a consistent User-Agent header on the session.
Copilot
AI
Mar 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When Telegram returns HTTP 200 but {"ok": false} (e.g., invalid token), the listener does continue without any backoff/logging. That can create a tight loop hammering the API. Add logging of the error payload and sleep/backoff (or raise) when ok is false.
| if not data.get("ok"): | |
| if not data.get("ok"): | |
| logger.error("Telegram getUpdates returned ok=false: %s", data) | |
| time.sleep(5) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_telegram_client()is currently unused (only defined, never called) and also callssys.exit(1)on missing config, which is inconsistent with most other client helpers in this file (they return an error code via the command functions). Consider removing it until needed, or refactoringcmd_telegram_send/listento use it and return error codes instead of exiting.