|
@@ -239,6 +239,24 @@ def _write_file_bytes(path: str, data: bytes) -> None:
|
|
|
f.write(data)
|
|
f.write(data)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+def _normalise_outbound_text(content: str) -> str:
|
|
|
|
|
+ """Normalise outgoing text for Chatto.
|
|
|
|
|
+
|
|
|
|
|
+ Chatto renders Markdown natively, so there is nothing to escape or
|
|
|
|
|
+ translate — the only transformations here are the ones that measurably
|
|
|
|
|
+ render wrong: CRLF line endings (which show up as stray blank lines)
|
|
|
|
|
+ and runs of more than two blank lines. Shared by the adapter's
|
|
|
|
|
+ ``format_message`` and the standalone cron sender, so both paths render
|
|
|
|
|
+ identically.
|
|
|
|
|
+ """
|
|
|
|
|
+ if not content:
|
|
|
|
|
+ return content
|
|
|
|
|
+ normalised = content.replace("\r\n", "\n").replace("\r", "\n")
|
|
|
|
|
+ while "\n\n\n\n" in normalised:
|
|
|
|
|
+ normalised = normalised.replace("\n\n\n\n", "\n\n\n")
|
|
|
|
|
+ return normalised
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
# --------------------------------------------------------------------------- #
|
|
# --------------------------------------------------------------------------- #
|
|
|
# Adapter
|
|
# Adapter
|
|
|
# --------------------------------------------------------------------------- #
|
|
# --------------------------------------------------------------------------- #
|
|
@@ -255,11 +273,10 @@ class ChattoAdapter(BasePlatformAdapter):
|
|
|
Receives messages via WebSocket realtime, sends via ConnectRPC.
|
|
Receives messages via WebSocket realtime, sends via ConnectRPC.
|
|
|
"""
|
|
"""
|
|
|
|
|
|
|
|
- _SPLIT_THRESHOLD = 9900
|
|
|
|
|
# Read by BasePlatformAdapter.max_message_length_for_chat(), which the
|
|
# Read by BasePlatformAdapter.max_message_length_for_chat(), which the
|
|
|
# gateway and the stream consumer use to chunk outgoing messages. Without
|
|
# gateway and the stream consumer use to chunk outgoing messages. Without
|
|
|
# it they fall back to 4096 and split Chatto messages far earlier than
|
|
# it they fall back to 4096 and split Chatto messages far earlier than
|
|
|
- # necessary — send() itself already truncates at 10000.
|
|
|
|
|
|
|
+ # necessary — send() itself already truncates at SPLIT_THRESHOLD.
|
|
|
MAX_MESSAGE_LENGTH = ChattoConstants.MAX_MESSAGE_LENGTH
|
|
MAX_MESSAGE_LENGTH = ChattoConstants.MAX_MESSAGE_LENGTH
|
|
|
splits_long_messages = True
|
|
splits_long_messages = True
|
|
|
supports_code_blocks: bool = True
|
|
supports_code_blocks: bool = True
|
|
@@ -1734,8 +1751,8 @@ class ChattoAdapter(BasePlatformAdapter):
|
|
|
if not content:
|
|
if not content:
|
|
|
return SendResult(success=False, error="Empty message")
|
|
return SendResult(success=False, error="Empty message")
|
|
|
|
|
|
|
|
- formatted = self.format_message(content)
|
|
|
|
|
- chunks = self.truncate_message(formatted, ChattoConstants.MAX_MESSAGE_LENGTH)
|
|
|
|
|
|
|
+ formatted = _normalise_outbound_text(content)
|
|
|
|
|
+ chunks = self.truncate_message(formatted, ChattoConstants.SPLIT_THRESHOLD)
|
|
|
|
|
|
|
|
thread_id = self._resolve_outbound_thread(chat_id, reply_to, metadata)
|
|
thread_id = self._resolve_outbound_thread(chat_id, reply_to, metadata)
|
|
|
room_kind = self._room_kinds.get(chat_id)
|
|
room_kind = self._room_kinds.get(chat_id)
|
|
@@ -1825,19 +1842,12 @@ class ChattoAdapter(BasePlatformAdapter):
|
|
|
def format_message(self, content: str) -> str:
|
|
def format_message(self, content: str) -> str:
|
|
|
"""Normalise outgoing text for Chatto.
|
|
"""Normalise outgoing text for Chatto.
|
|
|
|
|
|
|
|
- Chatto renders Markdown natively, so there is nothing to escape or
|
|
|
|
|
- translate — the only transformations here are the ones that measurably
|
|
|
|
|
- render wrong: CRLF line endings (which show up as stray blank lines)
|
|
|
|
|
- and runs of more than two blank lines.
|
|
|
|
|
|
|
+ The transformations live in :func:`_normalise_outbound_text`, shared
|
|
|
|
|
+ with the standalone cron sender.
|
|
|
|
|
|
|
|
BasePlatformAdapter override
|
|
BasePlatformAdapter override
|
|
|
"""
|
|
"""
|
|
|
- if not content:
|
|
|
|
|
- return content
|
|
|
|
|
- normalised = content.replace("\r\n", "\n").replace("\r", "\n")
|
|
|
|
|
- while "\n\n\n\n" in normalised:
|
|
|
|
|
- normalised = normalised.replace("\n\n\n\n", "\n\n\n")
|
|
|
|
|
- return normalised
|
|
|
|
|
|
|
+ return _normalise_outbound_text(content)
|
|
|
|
|
|
|
|
async def edit_message(
|
|
async def edit_message(
|
|
|
self,
|
|
self,
|
|
@@ -2687,7 +2697,9 @@ async def hermes_standalone_sender_fn(
|
|
|
"""Deliver a message to Chatto without a running gateway adapter. Do not modify signature.
|
|
"""Deliver a message to Chatto without a running gateway adapter. Do not modify signature.
|
|
|
|
|
|
|
|
Used by cron / scheduled routines that run out-of-process. Creates a
|
|
Used by cron / scheduled routines that run out-of-process. Creates a
|
|
|
- short-lived chattolib client, posts, and closes.
|
|
|
|
|
|
|
+ short-lived chattolib client, posts, and closes. Long messages are
|
|
|
|
|
+ normalised and split like ``send()`` does, so cron output cannot die on
|
|
|
|
|
+ the server's per-message limit.
|
|
|
"""
|
|
"""
|
|
|
chatto_config: ChattoConfiguration = ChattoConfiguration(pconfig=pconfig)
|
|
chatto_config: ChattoConfiguration = ChattoConfiguration(pconfig=pconfig)
|
|
|
|
|
|
|
@@ -2723,11 +2735,27 @@ async def hermes_standalone_sender_fn(
|
|
|
kwargs["thread_root_event_id"] = thread_id
|
|
kwargs["thread_root_event_id"] = thread_id
|
|
|
if media_files and media_files.get("attachment_asset_ids"):
|
|
if media_files and media_files.get("attachment_asset_ids"):
|
|
|
kwargs["attachment_asset_ids"] = list(media_files["attachment_asset_ids"])
|
|
kwargs["attachment_asset_ids"] = list(media_files["attachment_asset_ids"])
|
|
|
|
|
+
|
|
|
|
|
+ formatted = _normalise_outbound_text(message)
|
|
|
|
|
+ chunks = BasePlatformAdapter.truncate_message(
|
|
|
|
|
+ formatted, ChattoConstants.SPLIT_THRESHOLD
|
|
|
|
|
+ )
|
|
|
|
|
+ message_ids: list[str] = []
|
|
|
try:
|
|
try:
|
|
|
- posted = await client.post_message(chat_id, message, **kwargs)
|
|
|
|
|
|
|
+ for chunk in chunks:
|
|
|
|
|
+ posted = await client.post_message(chat_id, chunk, **kwargs)
|
|
|
|
|
+ message_ids.append(posted.id)
|
|
|
except Exception as exc:
|
|
except Exception as exc:
|
|
|
|
|
+ if message_ids:
|
|
|
|
|
+ logger.warning(
|
|
|
|
|
+ "Chatto standalone: sent %d/%d chunk(s) to %s before failing: %s",
|
|
|
|
|
+ len(message_ids),
|
|
|
|
|
+ len(chunks),
|
|
|
|
|
+ chat_id,
|
|
|
|
|
+ exc,
|
|
|
|
|
+ )
|
|
|
return SendResult(success=False, error=str(exc))
|
|
return SendResult(success=False, error=str(exc))
|
|
|
- return SendResult(success=True, message_id=posted.id)
|
|
|
|
|
|
|
+ return SendResult(success=True, message_id=message_ids[0])
|
|
|
finally:
|
|
finally:
|
|
|
try:
|
|
try:
|
|
|
await client.close()
|
|
await client.close()
|