diff --git a/nanobot/channels/discord.py b/nanobot/channels/discord.py index 900c17b..c868bbf 100644 --- a/nanobot/channels/discord.py +++ b/nanobot/channels/discord.py @@ -13,34 +13,13 @@ from nanobot.bus.events import OutboundMessage from nanobot.bus.queue import MessageBus from nanobot.channels.base import BaseChannel from nanobot.config.schema import DiscordConfig +from nanobot.utils.helpers import split_message DISCORD_API_BASE = "https://discord.com/api/v10" MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024 # 20MB MAX_MESSAGE_LEN = 2000 # Discord message character limit -def _split_message(content: str, max_len: int = MAX_MESSAGE_LEN) -> list[str]: - """Split content into chunks within max_len, preferring line breaks.""" - if not content: - return [] - if len(content) <= max_len: - return [content] - chunks: list[str] = [] - while content: - if len(content) <= max_len: - chunks.append(content) - break - cut = content[:max_len] - pos = cut.rfind('\n') - if pos <= 0: - pos = cut.rfind(' ') - if pos <= 0: - pos = max_len - chunks.append(content[:pos]) - content = content[pos:].lstrip() - return chunks - - class DiscordChannel(BaseChannel): """Discord channel using Gateway websocket.""" @@ -105,7 +84,7 @@ class DiscordChannel(BaseChannel): headers = {"Authorization": f"Bot {self.config.token}"} try: - chunks = _split_message(msg.content or "") + chunks = split_message(msg.content or "", MAX_MESSAGE_LEN) if not chunks: return diff --git a/nanobot/channels/telegram.py b/nanobot/channels/telegram.py index 884b2d0..9097496 100644 --- a/nanobot/channels/telegram.py +++ b/nanobot/channels/telegram.py @@ -14,6 +14,9 @@ from nanobot.bus.events import OutboundMessage from nanobot.bus.queue import MessageBus from nanobot.channels.base import BaseChannel from nanobot.config.schema import TelegramConfig +from nanobot.utils.helpers import split_message + +TELEGRAM_MAX_MESSAGE_LEN = 4000 # Telegram message character limit def _markdown_to_telegram_html(text: str) -> str: @@ -79,26 +82,6 @@ def _markdown_to_telegram_html(text: str) -> str: return text -def _split_message(content: str, max_len: int = 4000) -> list[str]: - """Split content into chunks within max_len, preferring line breaks.""" - if len(content) <= max_len: - return [content] - chunks: list[str] = [] - while content: - if len(content) <= max_len: - chunks.append(content) - break - cut = content[:max_len] - pos = cut.rfind('\n') - if pos == -1: - pos = cut.rfind(' ') - if pos == -1: - pos = max_len - chunks.append(content[:pos]) - content = content[pos:].lstrip() - return chunks - - class TelegramChannel(BaseChannel): """ Telegram channel using long polling. @@ -273,8 +256,8 @@ class TelegramChannel(BaseChannel): if msg.content and msg.content != "[empty message]": is_progress = msg.metadata.get("_progress", False) draft_id = msg.metadata.get("message_id") - - for chunk in _split_message(msg.content): + + for chunk in split_message(msg.content, TELEGRAM_MAX_MESSAGE_LEN): try: html = _markdown_to_telegram_html(chunk) if is_progress and draft_id: diff --git a/nanobot/utils/helpers.py b/nanobot/utils/helpers.py index b543174..c57c365 100644 --- a/nanobot/utils/helpers.py +++ b/nanobot/utils/helpers.py @@ -47,6 +47,38 @@ def safe_filename(name: str) -> str: return _UNSAFE_CHARS.sub("_", name).strip() +def split_message(content: str, max_len: int = 2000) -> list[str]: + """ + Split content into chunks within max_len, preferring line breaks. + + Args: + content: The text content to split. + max_len: Maximum length per chunk (default 2000 for Discord compatibility). + + Returns: + List of message chunks, each within max_len. + """ + if not content: + return [] + if len(content) <= max_len: + return [content] + chunks: list[str] = [] + while content: + if len(content) <= max_len: + chunks.append(content) + break + cut = content[:max_len] + # Try to break at newline first, then space, then hard break + pos = cut.rfind('\n') + if pos <= 0: + pos = cut.rfind(' ') + if pos <= 0: + pos = max_len + chunks.append(content[:pos]) + content = content[pos:].lstrip() + return chunks + + def sync_workspace_templates(workspace: Path, silent: bool = False) -> list[str]: """Sync bundled templates to workspace. Only creates missing files.""" from importlib.resources import files as pkg_files