ソースを参照

Fix presence_changed crash: wire carries the proto enum as an int

The presence_changed handler read event.payload.status.name directly. On
the wire the status arrives as a plain int (protobuf runtime without
native enum wrappers), so every presence change raised 'int' object has
no attribute 'name'. The realtime event loop cannot tell a handler bug
from wire damage, so it reconnected with 30s backoff — and every inbound
message that arrived inside such a gap was silently dropped. A message
addressed to the bot was lost exactly that way (2026-10-24 10:28 UTC,
inside the gap after a presence_changed crash).

Coerce the int through presence_pb2.PresenceStatus.Name() and map it to
chattolib's PresenceStatus by value, the same coercion chattolib's own
bot.py applies to the same field. Add a regression test that mocks the
payload with a raw int, matching the wire shape — the existing tests
mocked a StrEnum with .name, which is why they never caught this.
Liv Klumpp 1 週間 前
親
コミット
cdb8aca547
2 ファイル変更、36 行追加、2 行削除
  1. 22 2
      adapter.py
  2. 14 0
      test_adapter.py

+ 22 - 2
adapter.py

@@ -77,6 +77,7 @@ try:
         MessageMention,  # type: ignore[import-untyped]
         MessagePostedEvent,  # type: ignore[import-untyped]
     )
+    from chattolib._pb.chatto.api.v1 import presence_pb2  # type: ignore[import-untyped]
     from chattolib.client import (
         ChattoClient,
     )
@@ -2664,11 +2665,30 @@ class ChattoAdapter(BasePlatformAdapter):
                 changed_user_id
                 and (user := self._user_cache.get(changed_user_id)) is not None
             ):
-                user.presence_status = event.payload.status
+                # The realtime payload carries the proto enum as a plain int
+                # (or proto enum wrapper); chattolib's own bot.py coerces the
+                # same field the same way — read the wire shape, never
+                # ``.name`` directly, which raised AttributeError on every
+                # presence change and dropped inbound messages inside the
+                # reconnect gaps that followed.
+                raw_status = event.payload.status
+                if isinstance(raw_status, PresenceStatus):
+                    status = raw_status
+                else:
+                    proto_name = presence_pb2.PresenceStatus.Name(int(raw_status))
+                    status = next(
+                        (
+                            member
+                            for member in PresenceStatus
+                            if member.value == proto_name
+                        ),
+                        PresenceStatus.UNSPECIFIED,
+                    )
+                user.presence_status = status
                 logger.debug(
                     "Chatto: patched presence of %s to %s",
                     changed_user_id,
-                    event.payload.status.name,
+                    status.name,
                 )
 
         elif event.kind in ("user_joined_room", "user_left_room"):

+ 14 - 0
test_adapter.py

@@ -1274,6 +1274,20 @@ class TestReactionForwarding:
 
         assert adapter._user_cache["human-1"].presence_status is PresenceStatus.OFFLINE
 
+    async def test_presence_event_with_int_status_does_not_raise(self, adapter):
+        """The wire carries the proto enum as a plain int (protobuf runtime
+        without native enums), so ``status`` has no ``.name`` attribute. A
+        ``.name`` read raised AttributeError on every presence change, and
+        each one cost a 30s reconnect gap that swallowed inbound messages."""
+        event = self._event("presence_changed", actor_id="human-1")
+        event.payload = MagicMock(spec=["status"])
+        event.payload.status = 1  # PRESENCE_STATUS_ONLINE as a raw int
+        adapter._user_cache["human-1"] = _make_user("human-1", "human")
+
+        await adapter._handle_realtime_event(event)
+
+        assert adapter._user_cache["human-1"].presence_status is PresenceStatus.ONLINE
+
     async def test_presence_of_an_uncached_user_is_skipped(self, adapter):
         """Presence of someone the adapter has not cached is not an error —
         there is nothing to patch, and the event is not interesting either."""