Parcourir la source

Keep chattolib Messages out of SendResult.raw_response

The cron scheduler calls .get() on raw_response when a job targets a
thread (thread_fallback contract, gateway/platforms/base.py). Handing it
the chattolib Message crashed delivery bookkeeping after the send had
already succeeded — the job fell back to the standalone path and the
room saw the brief twice ('Message' object has no attribute 'get').

All four SendResult sites now leave raw_response unset; regression test
pins the dict-shaped contract.
Paul Klumpp il y a 1 semaine
Parent
commit
c68ff54050
2 fichiers modifiés avec 29 ajouts et 10 suppressions
  1. 18 10
      adapter.py
  2. 11 0
      test_adapter.py

+ 18 - 10
adapter.py

@@ -1242,11 +1242,12 @@ class ChattoAdapter(BasePlatformAdapter):
         # Auto-thread: by default, Chatto creates a thread for replies to room
         # messages (not DMs, not already in a thread). This keeps conversations
         # organized in the room. Can be disabled via extra.auto_thread=false.
-        use_auto_thread = self.chatto_config.auto_thread.value and not thread_id and not is_dm
+        use_auto_thread = (
+            self.chatto_config.auto_thread.value and not thread_id and not is_dm
+        )
 
-        message_ids: List[str] = []
-        last_resp: Optional[Any] = None
-        last_error: Optional[str] = None
+        message_ids: list[str] = []
+        last_error: str | None = None
         retryable = False
 
         try:
@@ -1270,7 +1271,6 @@ class ChattoAdapter(BasePlatformAdapter):
                 retryable = True
                 break
 
-            last_resp = msg_obj
             self._mark_seen(msg_obj.id)
             message_ids.append(msg_obj.id)
             self._our_message_ids.add(msg_obj.id)
@@ -1306,10 +1306,18 @@ class ChattoAdapter(BasePlatformAdapter):
         if last_error:
             logger.warning(
                 "Chatto: sent %d/%d chunk(s) to %s before failing: %s",
-                len(message_ids), len(chunks), chat_id, last_error,
+                len(message_ids),
+                len(chunks),
+                chat_id,
+                last_error,
             )
 
-        return SendResult(success=True, message_id=first_id, raw_response=last_resp)
+        # raw_response stays unset (dict-shaped per the SendResult contract):
+        # gateway consumers such as the cron scheduler call .get() on it, so a
+        # chattolib Message here would crash delivery bookkeeping *after* the
+        # send already succeeded — the job then falls back to the standalone
+        # path and the room sees the message twice.
+        return SendResult(success=True, message_id=first_id)
 
     def format_message(self, content: str) -> str:
         """Normalise outgoing text for Chatto.
@@ -1390,7 +1398,7 @@ class ChattoAdapter(BasePlatformAdapter):
         # is never mistaken for inbound traffic.
         edited_id = getattr(msg, "id", "") or str(message_id)
         self._mark_seen(edited_id)
-        return SendResult(success=True, message_id=edited_id, raw_response=msg)
+        return SendResult(success=True, message_id=edited_id)
 
     async def delete_message(self, chat_id: str, message_id: str) -> bool:
         """Delete a message via MessageService/DeleteMessage.
@@ -1817,7 +1825,7 @@ class ChattoAdapter(BasePlatformAdapter):
             )
             self._mark_seen(msg.id)
             self._our_message_ids.add(msg.id)
-            return SendResult(success=True, message_id=msg.id, raw_response=msg)
+            return SendResult(success=True, message_id=msg.id)
         except ChattoError as e:
             return SendResult(success=False, error=str(e), retryable=True)
         except Exception as e:
@@ -2164,7 +2172,7 @@ async def hermes_standalone_sender_fn(
             posted = await client.post_message(chat_id, message, **kwargs)
         except Exception as exc:
             return SendResult(success=False, error=str(exc))
-        return SendResult(success=True, message_id=posted.id, raw_response=posted)
+        return SendResult(success=True, message_id=posted.id)
     finally:
         try:
             await client.close()

+ 11 - 0
test_adapter.py

@@ -342,6 +342,17 @@ class TestSend:
         call_kwargs = adapter._chatto_client.post_message.call_args.kwargs
         assert call_kwargs["thread_root_event_id"] == "thread-123"
 
+    async def test_send_raw_response_stays_dict_shaped(self, adapter):
+        # The cron scheduler calls .get() on SendResult.raw_response when a job
+        # targets a thread; a chattolib Message there crashes delivery
+        # bookkeeping after the send and duplicates the message standalone.
+        adapter._chatto_client.post_message.return_value = _make_message(
+            body="Hello world", message_id="msg-789"
+        )
+        result = await adapter.send("room-1", "Hello world")
+        assert result.success is True
+        assert not result.raw_response or isinstance(result.raw_response, dict)
+
 
 # -- Reactions --