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