Rebase onto upstream (a4d95fd) #12

Closed
wylab wants to merge 144 commits from rebase-onto-upstream into main
2 changed files with 31 additions and 58 deletions
Showing only changes of commit 49d39692bc - Show all commits
+27 -54
View File
@@ -218,70 +218,43 @@ class TelegramChannel(BaseChannel):
except Exception as e2: except Exception as e2:
logger.error(f"Error sending Telegram message: {e2}") logger.error(f"Error sending Telegram message: {e2}")
@staticmethod
def _split_message(content: str, max_len: int = 4000) -> list[str]:
"""Split content into chunks within max_len, preferring line breaks.
From upstream HKUDS/nanobot - battle-tested implementation.
Uses 4000 char limit (safer than 4096) with split priority: \n → space → hard cut.
"""
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
async def _send_text_chunks( async def _send_text_chunks(
self, self,
chat_id: int, chat_id: int,
text: str, text: str,
parse_mode: str | None = "HTML" parse_mode: str | None = "HTML"
) -> None: ) -> None:
"""Split and send long messages at sentence boundaries. """Split and send long messages.
Telegram has a 4096 character limit per message. Telegram has a 4096 character limit per message.
Per design doc: split at sentence boundaries, send as multiple messages. Uses upstream's proven implementation - splits at line breaks, then spaces.
""" """
MAX_LENGTH = 4096 chunks = self._split_message(text)
if len(text) <= MAX_LENGTH:
# Single message
await self._app.bot.send_message(
chat_id=chat_id,
text=text,
parse_mode=parse_mode
)
return
# Split at sentence boundaries
import re
# Split on sentence endings: . ! ? followed by space/newline/end
sentences = re.split(r'([.!?]+(?:\s+|$))', text)
# Rejoin sentence with its punctuation
parts = []
for i in range(0, len(sentences) - 1, 2):
parts.append(sentences[i] + (sentences[i+1] if i+1 < len(sentences) else ''))
if len(sentences) % 2 == 1: # Last part without punctuation
parts.append(sentences[-1])
# Group into chunks under MAX_LENGTH
chunks = []
current_chunk = ""
for part in parts:
# If single part exceeds limit, force split it
if len(part) > MAX_LENGTH:
if current_chunk:
chunks.append(current_chunk)
current_chunk = ""
# Hard split at MAX_LENGTH
for i in range(0, len(part), MAX_LENGTH):
chunks.append(part[i:i + MAX_LENGTH])
continue
# Try adding part to current chunk
test_chunk = current_chunk + part
if len(test_chunk) > MAX_LENGTH:
# Save current chunk, start new one
if current_chunk:
chunks.append(current_chunk)
current_chunk = part
else:
current_chunk = test_chunk
# Add final chunk
if current_chunk:
chunks.append(current_chunk)
# Send all chunks as separate messages
for chunk in chunks: for chunk in chunks:
await self._app.bot.send_message( await self._app.bot.send_message(
chat_id=chat_id, chat_id=chat_id,
+4 -4
View File
@@ -92,8 +92,8 @@ async def test_long_message_splits_at_sentences():
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_message_at_exactly_4096_chars(): async def test_message_at_exactly_4000_chars():
"""Message at exactly 4096 chars should not chunk.""" """Message at exactly 4000 chars should not chunk (safer limit)."""
config = MagicMock() config = MagicMock()
config.token = "test-token" config.token = "test-token"
bus = MagicMock() bus = MagicMock()
@@ -112,8 +112,8 @@ async def test_message_at_exactly_4096_chars():
channel._app = MockApp() channel._app = MockApp()
# Exactly 4096 chars # Exactly 4000 chars (upstream uses 4000 as safer limit vs 4096)
content = "A" * 4096 content = "A" * 4000
msg = OutboundMessage( msg = OutboundMessage(
channel="telegram", channel="telegram",