Merge PR #1567: refactor(channels): extract split_message utility to reduce duplication
This commit is contained in:
@@ -13,34 +13,13 @@ from nanobot.bus.events import OutboundMessage
|
|||||||
from nanobot.bus.queue import MessageBus
|
from nanobot.bus.queue import MessageBus
|
||||||
from nanobot.channels.base import BaseChannel
|
from nanobot.channels.base import BaseChannel
|
||||||
from nanobot.config.schema import DiscordConfig
|
from nanobot.config.schema import DiscordConfig
|
||||||
|
from nanobot.utils.helpers import split_message
|
||||||
|
|
||||||
DISCORD_API_BASE = "https://discord.com/api/v10"
|
DISCORD_API_BASE = "https://discord.com/api/v10"
|
||||||
MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024 # 20MB
|
MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024 # 20MB
|
||||||
MAX_MESSAGE_LEN = 2000 # Discord message character limit
|
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):
|
class DiscordChannel(BaseChannel):
|
||||||
"""Discord channel using Gateway websocket."""
|
"""Discord channel using Gateway websocket."""
|
||||||
|
|
||||||
@@ -105,7 +84,7 @@ class DiscordChannel(BaseChannel):
|
|||||||
headers = {"Authorization": f"Bot {self.config.token}"}
|
headers = {"Authorization": f"Bot {self.config.token}"}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
chunks = _split_message(msg.content or "")
|
chunks = split_message(msg.content or "", MAX_MESSAGE_LEN)
|
||||||
if not chunks:
|
if not chunks:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ from nanobot.bus.events import OutboundMessage
|
|||||||
from nanobot.bus.queue import MessageBus
|
from nanobot.bus.queue import MessageBus
|
||||||
from nanobot.channels.base import BaseChannel
|
from nanobot.channels.base import BaseChannel
|
||||||
from nanobot.config.schema import TelegramConfig
|
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:
|
def _markdown_to_telegram_html(text: str) -> str:
|
||||||
@@ -79,26 +82,6 @@ def _markdown_to_telegram_html(text: str) -> str:
|
|||||||
return text
|
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):
|
class TelegramChannel(BaseChannel):
|
||||||
"""
|
"""
|
||||||
Telegram channel using long polling.
|
Telegram channel using long polling.
|
||||||
@@ -273,8 +256,8 @@ class TelegramChannel(BaseChannel):
|
|||||||
if msg.content and msg.content != "[empty message]":
|
if msg.content and msg.content != "[empty message]":
|
||||||
is_progress = msg.metadata.get("_progress", False)
|
is_progress = msg.metadata.get("_progress", False)
|
||||||
draft_id = msg.metadata.get("message_id")
|
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:
|
try:
|
||||||
html = _markdown_to_telegram_html(chunk)
|
html = _markdown_to_telegram_html(chunk)
|
||||||
if is_progress and draft_id:
|
if is_progress and draft_id:
|
||||||
|
|||||||
@@ -47,6 +47,38 @@ def safe_filename(name: str) -> str:
|
|||||||
return _UNSAFE_CHARS.sub("_", name).strip()
|
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]:
|
def sync_workspace_templates(workspace: Path, silent: bool = False) -> list[str]:
|
||||||
"""Sync bundled templates to workspace. Only creates missing files."""
|
"""Sync bundled templates to workspace. Only creates missing files."""
|
||||||
from importlib.resources import files as pkg_files
|
from importlib.resources import files as pkg_files
|
||||||
|
|||||||
Reference in New Issue
Block a user