diff --git a/modules/command_manager.py b/modules/command_manager.py index 198e911..230bdb4 100644 --- a/modules/command_manager.py +++ b/modules/command_manager.py @@ -5,6 +5,7 @@ Handles all bot commands, keyword matching, and response generation """ import asyncio +from datetime import datetime import random import time from dataclasses import dataclass @@ -1061,6 +1062,7 @@ class CommandManager: skip_user_rate_limit: bool = False, rate_limit_key: str | None = None, scope: str | None = None, + timestamp: datetime | None = None, ) -> bool: """Send a channel message using meshcore_py (optional flood scope). @@ -1133,7 +1135,10 @@ class CommandManager: _max_retries = 2 for _attempt in range(_max_retries + 1): try: - result = await self.bot.meshcore.commands.send_chan_msg(channel_num, content) + result = await self.bot.meshcore.commands.send_chan_msg( + channel_num, content, + timestamp=int(timestamp.timestamp()) if timestamp else None, + ) finally: if not scope_is_global and hasattr(self.bot.meshcore.commands, "set_flood_scope"): await self.bot.meshcore.commands.set_flood_scope("*") diff --git a/modules/service_plugins/darc_mowas_service.py b/modules/service_plugins/darc_mowas_service.py index 38a7e14..b480ea4 100644 --- a/modules/service_plugins/darc_mowas_service.py +++ b/modules/service_plugins/darc_mowas_service.py @@ -19,7 +19,7 @@ import time import xml.dom.minidom from asyncio import AbstractEventLoop from dataclasses import dataclass -from datetime import datetime +from datetime import datetime, timedelta from typing import Any, cast import aiohttp @@ -176,11 +176,14 @@ class DARC_MoWaS_Service(BaseServicePlugin): Send all chunks, each with async retry if configured. Note, that we cannot guarantee that the messages arrive in-order, but as the chunks have a (x/n) identifier at the end, the user still - should be able to grasp the message correctly. + should be able to grasp the message correctly. We further add + an ascending timestamp to each message (also used on re-transmission) to + let the client restore the order, as well as deduplicate retransmissions. Ideally this chunking should be implemented at protocol level to guarantee the atomicity and order of the full message. """ + ts_now = datetime.now() for i, chunk in enumerate(chunks): asyncio.create_task( self._send_chunk_with_retry( @@ -188,6 +191,7 @@ class DARC_MoWaS_Service(BaseServicePlugin): chunk, i, len(chunks), + ts_now + timedelta(seconds=1) ) ) @@ -197,6 +201,7 @@ class DARC_MoWaS_Service(BaseServicePlugin): chunk: str, index: int, total: int, + timestamp: datetime, ) -> None: """Send a chunk and retry until acked or retries exhausted.""" tracker = getattr(self.bot, "transmission_tracker", None) @@ -210,6 +215,7 @@ class DARC_MoWaS_Service(BaseServicePlugin): chunk, command_id=cmd_id, skip_user_rate_limit=True, + timestamp=timestamp ): self.logger.warning("Send failed for '%s'", channel) return