fix: handle edge cases in message splitting and send failure

- _split_message: return empty list for empty/None content instead
  of a list with one empty string (Discord rejects empty content)
- _split_message: use pos <= 0 fallback to prevent empty chunks
  when content starts with a newline or space
- _send_payload: return bool to indicate success/failure
- send: abort remaining chunks when a chunk fails to send,
  preventing partial/corrupted message delivery
This commit is contained in:
Nikolas de Hor
2026-02-20 10:09:04 -03:00
parent 4c75e1673f
commit 4cbd857250

View File

@@ -22,6 +22,8 @@ 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] = []
@@ -31,9 +33,9 @@ def _split_message(content: str, max_len: int = MAX_MESSAGE_LEN) -> list[str]:
break
cut = content[:max_len]
pos = cut.rfind('\n')
if pos == -1:
if pos <= 0:
pos = cut.rfind(' ')
if pos == -1:
if pos <= 0:
pos = max_len
chunks.append(content[:pos])
content = content[pos:].lstrip()
@@ -104,6 +106,9 @@ class DiscordChannel(BaseChannel):
try:
chunks = _split_message(msg.content or "")
if not chunks:
return
for i, chunk in enumerate(chunks):
payload: dict[str, Any] = {"content": chunk}
@@ -112,14 +117,18 @@ class DiscordChannel(BaseChannel):
payload["message_reference"] = {"message_id": msg.reply_to}
payload["allowed_mentions"] = {"replied_user": False}
await self._send_payload(url, headers, payload)
if not await self._send_payload(url, headers, payload):
break # Abort remaining chunks on failure
finally:
await self._stop_typing(msg.chat_id)
async def _send_payload(
self, url: str, headers: dict[str, str], payload: dict[str, Any]
) -> None:
"""Send a single Discord API payload with retry on rate-limit."""
) -> bool:
"""Send a single Discord API payload with retry on rate-limit.
Returns True on success, False if all attempts failed.
"""
for attempt in range(3):
try:
response = await self._http.post(url, headers=headers, json=payload)
@@ -130,12 +139,13 @@ class DiscordChannel(BaseChannel):
await asyncio.sleep(retry_after)
continue
response.raise_for_status()
return
return True
except Exception as e:
if attempt == 2:
logger.error("Error sending Discord message: {}", e)
else:
await asyncio.sleep(1)
return False
async def _gateway_loop(self) -> None:
"""Main gateway loop: identify, heartbeat, dispatch events."""