From 23b7e1ef5e944a05653342a70efa2e1fbba9109f Mon Sep 17 00:00:00 2001 From: Darye <54469750+DaryeDev@users.noreply.github.com> Date: Mon, 16 Feb 2026 16:29:03 +0100 Subject: [PATCH 1/2] Handle media files (voice messages, audio, images, documents) on Telegram Channel --- nanobot/agent/tools/message.py | 12 +++++-- nanobot/channels/telegram.py | 64 +++++++++++++++++++++++++++++----- 2 files changed, 65 insertions(+), 11 deletions(-) diff --git a/nanobot/agent/tools/message.py b/nanobot/agent/tools/message.py index 347830f..3853725 100644 --- a/nanobot/agent/tools/message.py +++ b/nanobot/agent/tools/message.py @@ -52,6 +52,11 @@ class MessageTool(Tool): "chat_id": { "type": "string", "description": "Optional: target chat/user ID" + }, + "media": { + "type": "array", + "items": {"type": "string"}, + "description": "Optional: list of file paths to attach (images, audio, documents)" } }, "required": ["content"] @@ -62,6 +67,7 @@ class MessageTool(Tool): content: str, channel: str | None = None, chat_id: str | None = None, + media: list[str] | None = None, **kwargs: Any ) -> str: channel = channel or self._default_channel @@ -76,11 +82,13 @@ class MessageTool(Tool): msg = OutboundMessage( channel=channel, chat_id=chat_id, - content=content + content=content, + media=media or [] ) try: await self._send_callback(msg) - return f"Message sent to {channel}:{chat_id}" + media_info = f" with {len(media)} attachments" if media else "" + return f"Message sent to {channel}:{chat_id}{media_info}" except Exception as e: return f"Error sending message: {str(e)}" diff --git a/nanobot/channels/telegram.py b/nanobot/channels/telegram.py index c9978c2..8f135e4 100644 --- a/nanobot/channels/telegram.py +++ b/nanobot/channels/telegram.py @@ -198,6 +198,18 @@ class TelegramChannel(BaseChannel): await self._app.shutdown() self._app = None + def _get_media_type(self, path: str) -> str: + """Guess media type from file extension.""" + path = path.lower() + if path.endswith(('.jpg', '.jpeg', '.png', '.gif', '.webp')): + return "photo" + elif path.endswith('.ogg'): + return "voice" + elif path.endswith(('.mp3', '.m4a', '.wav', '.aac')): + return "audio" + else: + return "document" + async def send(self, msg: OutboundMessage) -> None: """Send a message through Telegram.""" if not self._app: @@ -212,16 +224,50 @@ class TelegramChannel(BaseChannel): logger.error(f"Invalid chat_id: {msg.chat_id}") return - for chunk in _split_message(msg.content): - try: - html = _markdown_to_telegram_html(chunk) - await self._app.bot.send_message(chat_id=chat_id, text=html, parse_mode="HTML") - except Exception as e: - logger.warning(f"HTML parse failed, falling back to plain text: {e}") + # Handle media files + if msg.media: + for media_path in msg.media: try: - await self._app.bot.send_message(chat_id=chat_id, text=chunk) - except Exception as e2: - logger.error(f"Error sending Telegram message: {e2}") + media_type = self._get_media_type(media_path) + + # Determine caption (only for first media or if explicitly set, + # but here we keep it simple: content is sent separately if media is present + # to avoid length issues, unless we want to attach it to the first media) + # For simplicity: send media first, then text if present. + # Or: if single media, attach text as caption. + + # Let's attach content as caption to the last media if single, + # otherwise send text separately. + + with open(media_path, 'rb') as f: + if media_type == "photo": + await self._app.bot.send_photo(chat_id=chat_id, photo=f) + elif media_type == "voice": + await self._app.bot.send_voice(chat_id=chat_id, voice=f) + elif media_type == "audio": + await self._app.bot.send_audio(chat_id=chat_id, audio=f) + else: + await self._app.bot.send_document(chat_id=chat_id, document=f) + + except Exception as e: + logger.error(f"Failed to send media {media_path}: {e}") + await self._app.bot.send_message( + chat_id=chat_id, + text=f"[Failed to send file: {media_path}]" + ) + + # Send text content if present + if msg.content and msg.content != "[empty message]": + for chunk in _split_message(msg.content): + try: + html = _markdown_to_telegram_html(chunk) + await self._app.bot.send_message(chat_id=chat_id, text=html, parse_mode="HTML") + except Exception as e: + logger.warning(f"HTML parse failed, falling back to plain text: {e}") + try: + await self._app.bot.send_message(chat_id=chat_id, text=chunk) + except Exception as e2: + logger.error(f"Error sending Telegram message: {e2}") async def _on_start(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle /start command.""" From 5ad9c837df8879717a01760530b830dd3c67cd7b Mon Sep 17 00:00:00 2001 From: Re-bin Date: Tue, 17 Feb 2026 10:37:55 +0000 Subject: [PATCH 2/2] refactor: clean up telegram media sending logic --- nanobot/channels/telegram.py | 63 ++++++++++++++---------------------- 1 file changed, 24 insertions(+), 39 deletions(-) diff --git a/nanobot/channels/telegram.py b/nanobot/channels/telegram.py index 8f135e4..39924b3 100644 --- a/nanobot/channels/telegram.py +++ b/nanobot/channels/telegram.py @@ -198,17 +198,17 @@ class TelegramChannel(BaseChannel): await self._app.shutdown() self._app = None - def _get_media_type(self, path: str) -> str: + @staticmethod + def _get_media_type(path: str) -> str: """Guess media type from file extension.""" - path = path.lower() - if path.endswith(('.jpg', '.jpeg', '.png', '.gif', '.webp')): + ext = path.rsplit(".", 1)[-1].lower() if "." in path else "" + if ext in ("jpg", "jpeg", "png", "gif", "webp"): return "photo" - elif path.endswith('.ogg'): + if ext == "ogg": return "voice" - elif path.endswith(('.mp3', '.m4a', '.wav', '.aac')): + if ext in ("mp3", "m4a", "wav", "aac"): return "audio" - else: - return "document" + return "document" async def send(self, msg: OutboundMessage) -> None: """Send a message through Telegram.""" @@ -224,39 +224,24 @@ class TelegramChannel(BaseChannel): logger.error(f"Invalid chat_id: {msg.chat_id}") return - # Handle media files - if msg.media: - for media_path in msg.media: - try: - media_type = self._get_media_type(media_path) - - # Determine caption (only for first media or if explicitly set, - # but here we keep it simple: content is sent separately if media is present - # to avoid length issues, unless we want to attach it to the first media) - # For simplicity: send media first, then text if present. - # Or: if single media, attach text as caption. - - # Let's attach content as caption to the last media if single, - # otherwise send text separately. - - with open(media_path, 'rb') as f: - if media_type == "photo": - await self._app.bot.send_photo(chat_id=chat_id, photo=f) - elif media_type == "voice": - await self._app.bot.send_voice(chat_id=chat_id, voice=f) - elif media_type == "audio": - await self._app.bot.send_audio(chat_id=chat_id, audio=f) - else: - await self._app.bot.send_document(chat_id=chat_id, document=f) - - except Exception as e: - logger.error(f"Failed to send media {media_path}: {e}") - await self._app.bot.send_message( - chat_id=chat_id, - text=f"[Failed to send file: {media_path}]" - ) + # Send media files + for media_path in (msg.media or []): + try: + media_type = self._get_media_type(media_path) + sender = { + "photo": self._app.bot.send_photo, + "voice": self._app.bot.send_voice, + "audio": self._app.bot.send_audio, + }.get(media_type, self._app.bot.send_document) + param = "photo" if media_type == "photo" else media_type if media_type in ("voice", "audio") else "document" + with open(media_path, 'rb') as f: + await sender(chat_id=chat_id, **{param: f}) + except Exception as e: + filename = media_path.rsplit("/", 1)[-1] + logger.error(f"Failed to send media {media_path}: {e}") + await self._app.bot.send_message(chat_id=chat_id, text=f"[Failed to send: {filename}]") - # Send text content if present + # Send text content if msg.content and msg.content != "[empty message]": for chunk in _split_message(msg.content): try: