Pārlūkot izejas kodu

Make reaction routing visible in logs and quiet the read-state noise

Diagnosing the rejected-check report from a live log was blind: the
router exited silently on every guard (room mismatch, unauthorized
actor, already resolved), and the one card log normalized the emoji
before printing — hiding the exact codepoint a misbehaving client sent.
Each guard now logs why it stopped, and the card line prints the raw
codepoints. The per-viewer notification/thread bookkeeping events fire
on every read-state change in every followed room; they need no action,
so they move from WARNING to the ignored-kinds list.
Paul Klumpp 5 dienas atpakaļ
vecāks
revīzija
fd92f1d758
2 mainītis faili ar 59 papildinājumiem un 1 dzēšanām
  1. 47 1
      adapter.py
  2. 12 0
      test_adapter.py

+ 47 - 1
adapter.py

@@ -431,6 +431,12 @@ def _normalize_reaction_emoji(emoji: str) -> str:
     return "✅" if stripped in _CHECK_EMOJI_ALIASES else stripped
 
 
+def _log_emoji_codepoints(emoji: str) -> str:
+    """An emoji rendered as codepoints, for log lines that must distinguish
+    look-alikes — ✔️ (U+2714 FE0F) and ✅ (U+2705) differ only here."""
+    return " ".join(f"U+{ord(char):04X}" for char in emoji)
+
+
 # Reaction glyphs for the interactive prompt cards. The display forms keep
 # their variation selectors — they are what the card text and the seed
 # reactions show, and that is what should render as colour emoji. Reaction
@@ -2021,13 +2027,22 @@ class ChattoAdapter(BasePlatformAdapter):
             return False
         room_id = str(room_id)
         message_id = str(message_id)
-        emoji = _normalize_reaction_emoji(str(emoji))
+        # Keep the raw form for the log: normalizing first would hide exactly
+        # the codepoint a misbehaving client sent.
+        raw_emoji = str(emoji)
+        emoji = _normalize_reaction_emoji(raw_emoji)
         actor_id = str(actor_id)
 
         is_clarify = message_id in self._clarify_prompts
         is_approval = message_id in self._approval_prompts
         if not is_clarify and not is_approval:
             return False
+        logger.debug(
+            "Chatto: reaction on prompt card %s: %s (raw %s)",
+            message_id,
+            emoji,
+            _log_emoji_codepoints(raw_emoji),
+        )
 
         if is_clarify:
             prompt = self._clarify_prompts.get(message_id)
@@ -2038,6 +2053,12 @@ class ChattoAdapter(BasePlatformAdapter):
 
         # Room must match.
         if str(getattr(prompt, "chat_id", "")) != room_id:
+            logger.debug(
+                "Chatto: reaction on prompt %s ignored - room %s, card lives in %s",
+                message_id,
+                room_id,
+                getattr(prompt, "chat_id", ""),
+            )
             return True
 
         # Expiry handling.
@@ -2059,10 +2080,18 @@ class ChattoAdapter(BasePlatformAdapter):
 
         # Authorization (fail-closed).
         if not self._is_allowed_user_id(actor_id):
+            logger.debug(
+                "Chatto: reaction on prompt %s by unauthorized actor %s ignored",
+                message_id,
+                actor_id,
+            )
             return True
 
         # Double-click guard.
         if getattr(prompt, "resolved", False):
+            logger.debug(
+                "Chatto: prompt %s already resolved - reaction ignored", message_id
+            )
             return True
         if is_clarify and getattr(prompt, "awaiting_text", False):
             return True
@@ -2076,6 +2105,11 @@ class ChattoAdapter(BasePlatformAdapter):
             from tools import clarify_gateway  # type: ignore[import-not-found]
 
             choices = list(getattr(prompt, "choices", []) or [])
+            logger.debug(
+                "Chatto: clarify prompt %s accepts: %s",
+                message_id,
+                [*choices, "Other"],
+            )
 
             if emoji == _OTHER_EMOJI_KEY:
                 prompt.awaiting_text = True  # type: ignore[assignment]
@@ -2132,6 +2166,11 @@ class ChattoAdapter(BasePlatformAdapter):
 
         # Approval prompt.
         choices_map = getattr(prompt, "choices_map", {}) or {}
+        logger.debug(
+            "Chatto: approval prompt %s accepts: %s",
+            message_id,
+            [*choices_map],
+        )
         if emoji in choices_map:
             choice = choices_map[emoji]
             prompt.resolved = True
@@ -2820,9 +2859,16 @@ class ChattoAdapter(BasePlatformAdapter):
         # NOTE: "projection_event" (and "caught_up") belong to realtime protocol
         # v2 on unreleased Chatto main — chattolib speaks v1 and can never
         # deliver them here. Revisit when vendored chattolib gains v2 typing.
+        # The notification_* / viewer-state kinds are the server's per-viewer
+        # bookkeeping: they fire on every read-state change in every followed
+        # room, need no adapter action, and warning on each one buried real
+        # signals in noise.
         elif event.kind in (
             "mention_notification",
             "notification_dismissed",
+            "notification_occurrences_changed",
+            "notification_unread_state_changed",
+            "thread_viewer_state_changed",
             "room_marked_as_read",
             "user_typing",
             "notification_created",

+ 12 - 0
test_adapter.py

@@ -16,6 +16,7 @@ All network calls are mocked — no real HTTP or WebSocket connections.
 import asyncio
 import inspect
 import json
+import logging
 import os
 import sys
 from datetime import UTC, datetime, timedelta
@@ -1496,6 +1497,17 @@ class TestInteractiveReactionRouting:
         body = adapter._chatto_client.post_message.await_args.kwargs["body"]
         assert "✅ once" in body and "❌ deny" in body
 
+    async def test_a_routed_reaction_is_logged_with_its_codepoints(
+        self, adapter, caplog
+    ):
+        """Diagnosing a rejected reaction from a live log requires seeing the
+        exact codepoints the client sent — ✔️ and ✅ differ only there."""
+        self._approval(adapter)
+        with caplog.at_level(logging.DEBUG, logger="adapter"):
+            await adapter._handle_realtime_event(self._event("✔️"))
+
+        assert any("U+2714" in record.getMessage() for record in caplog.records)
+
 
 # -- Prompt card threading --