Coverage for agentos/channels/adapters/telegram.py: 0%
93 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-09 10:19 +0800
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-09 10:19 +0800
1"""
2Telegram Channel Adapter — Telegram Bot API.
4BotFather token → long-polling / webhook → ChannelMessage.
5"""
7from __future__ import annotations
9import json
11from agentos.channels.base import BaseChannelAdapter, ChannelConfig, ReplyResult
12from agentos.channels.message import ChannelMessage, ChannelType, MessageType
15class TelegramAdapter(BaseChannelAdapter):
16 """Telegram Bot API adapter.
18 Config fields:
19 bot_token: Telegram bot token from @BotFather
20 webhook_url: HTTPS webhook URL (leave empty for long-polling)
21 allowed_chats: list of chat IDs (empty = all)
22 parse_mode: "HTML" | "MarkdownV2" | None
23 """
25 CHANNEL_TYPE = ChannelType.TELEGRAM
27 # Telegram API base URL
28 API_BASE = "https://api.telegram.org"
30 def __init__(self, config: ChannelConfig):
31 super().__init__(config)
32 self._bot_token = config.extra.get("bot_token", "")
33 self._webhook_url = config.extra.get("webhook_url", "")
34 self._allowed_chats = config.extra.get("allowed_chats", [])
35 self._parse_mode = config.extra.get("parse_mode", "")
37 @property
38 def _api(self) -> str:
39 return f"{self.API_BASE}/bot{self._bot_token}"
41 # ── Message parsing ──
43 async def parse_incoming(self, payload: dict) -> ChannelMessage | None:
44 """Parse Telegram Update into ChannelMessage."""
45 if "message" in payload:
46 return self._parse_message(payload["message"])
47 elif "callback_query" in payload:
48 return self._parse_callback(payload["callback_query"])
49 elif "inline_query" in payload:
50 return self._parse_inline(payload["inline_query"])
51 return None
53 def _parse_message(self, msg: dict) -> ChannelMessage | None:
54 """Parse a Telegram Message object."""
55 chat = msg.get("chat", {})
56 user = msg.get("from", {})
57 chat_id = str(chat.get("id", ""))
58 user_id = str(user.get("id", ""))
60 if self._allowed_chats and chat_id not in self._allowed_chats:
61 return None
63 content = ""
64 msg_type = MessageType.TEXT
65 metadata = {
66 "chat_type": chat.get("type", "private"),
67 "chat_title": chat.get("title", ""),
68 "username": user.get("username", ""),
69 "first_name": user.get("first_name", ""),
70 }
72 if "text" in msg:
73 content = msg["text"]
74 elif "photo" in msg:
75 content = msg.get("caption", "[Photo]")
76 msg_type = MessageType.IMAGE
77 metadata["file_id"] = msg["photo"][-1]["file_id"]
78 elif "document" in msg:
79 content = msg.get("caption", "[Document]")
80 msg_type = MessageType.FILE
81 metadata["file_id"] = msg["document"]["file_id"]
82 metadata["file_name"] = msg["document"].get("file_name", "")
83 elif "voice" in msg:
84 content = "[Voice message]"
85 msg_type = MessageType.VOICE
86 elif "video" in msg:
87 content = msg.get("caption", "[Video]")
88 msg_type = MessageType.VIDEO
89 elif "sticker" in msg:
90 content = f"[Sticker: {msg['sticker'].get('emoji', '')}]"
91 msg_type = MessageType.TEXT
92 else:
93 return None
95 return ChannelMessage(
96 channel_type=ChannelType.TELEGRAM,
97 channel_id=chat_id,
98 user_id=user_id,
99 content=content,
100 message_type=msg_type,
101 raw=msg,
102 reply_token=str(msg.get("message_id", "")),
103 metadata=metadata,
104 )
106 def _parse_callback(self, cb: dict) -> ChannelMessage | None:
107 """Parse callback query (inline button press)."""
108 user = cb.get("from", {})
109 msg = cb.get("message", {})
110 return ChannelMessage(
111 channel_type=ChannelType.TELEGRAM,
112 channel_id=str(msg.get("chat", {}).get("id", "")),
113 user_id=str(user.get("id", "")),
114 content=cb.get("data", ""),
115 message_type=MessageType.INTERACTIVE,
116 raw=cb,
117 reply_token=cb.get("id", ""),
118 )
120 def _parse_inline(self, inline: dict) -> ChannelMessage | None:
121 """Parse inline query."""
122 user = inline.get("from", {})
123 return ChannelMessage(
124 channel_type=ChannelType.TELEGRAM,
125 channel_id="inline",
126 user_id=str(user.get("id", "")),
127 content=inline.get("query", ""),
128 message_type=MessageType.COMMAND,
129 raw=inline,
130 reply_token=inline.get("id", ""),
131 )
133 # ── Reply ──
135 async def reply(self, channel_id: str, content: str, **kwargs) -> ReplyResult:
136 """Send message via sendMessage."""
137 return await self._api_call(
138 "sendMessage",
139 {
140 "chat_id": channel_id,
141 "text": content,
142 "parse_mode": self._parse_mode,
143 "reply_to_message_id": kwargs.get("reply_token"),
144 },
145 )
147 async def reply_inline_keyboard(
148 self,
149 channel_id: str,
150 text: str,
151 buttons: list[list[dict]],
152 **kwargs,
153 ) -> ReplyResult:
154 """Send message with inline keyboard buttons.
156 buttons = [[{"text": "Yes", "callback_data": "yes"}], ...]
157 """
158 return await self._api_call(
159 "sendMessage",
160 {
161 "chat_id": channel_id,
162 "text": text,
163 "parse_mode": self._parse_mode,
164 "reply_markup": json.dumps({"inline_keyboard": buttons}),
165 },
166 )
168 async def reply_photo(
169 self, channel_id: str, photo_url: str, caption: str = "", **kwargs
170 ) -> ReplyResult:
171 """Send a photo."""
172 return await self._api_call(
173 "sendPhoto",
174 {
175 "chat_id": channel_id,
176 "photo": photo_url,
177 "caption": caption,
178 },
179 )
181 async def answer_callback(self, callback_query_id: str, text: str = "") -> ReplyResult:
182 """Answer a callback query (dismiss loading spinner)."""
183 return await self._api_call(
184 "answerCallbackQuery",
185 {
186 "callback_query_id": callback_query_id,
187 "text": text,
188 },
189 )
191 # ── Internal ──
193 async def _api_call(self, method: str, params: dict) -> ReplyResult:
194 """Call Telegram Bot API."""
195 url = f"{self._api}/{method}"
197 try:
198 import aiohttp
200 async with aiohttp.ClientSession() as session:
201 async with session.post(url, json=params) as resp:
202 data = await resp.json()
203 if data.get("ok"):
204 return ReplyResult(
205 success=True,
206 message_id=str(data.get("result", {}).get("message_id", "")),
207 )
208 return ReplyResult(
209 success=False,
210 error=data.get("description", "unknown"),
211 )
212 except ImportError:
213 import urllib.request
215 req = urllib.request.Request(
216 url,
217 data=json.dumps(params).encode(),
218 headers={"Content-Type": "application/json"},
219 )
220 with urllib.request.urlopen(req) as resp:
221 data = json.loads(resp.read())
222 return ReplyResult(
223 success=data.get("ok", False),
224 message_id=str(data.get("result", {}).get("message_id", "")),
225 )
227 # ── Webhook ──
229 async def set_webhook(self, url: str) -> bool:
230 """Register webhook URL with Telegram."""
231 result = await self._api_call("setWebhook", {"url": url})
232 return result.success
234 async def delete_webhook(self) -> bool:
235 """Remove webhook (switch to long-polling)."""
236 result = await self._api_call("deleteWebhook", {})
237 return result.success