From 4fe5410d5789b3a8909018a91475fdaff5a21dfb Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Wed, 29 Jul 2026 09:39:01 -0700 Subject: [PATCH] refactor(gateway): shared reaction-ack policy in base adapter - base.on_processing_complete implements the opt-in remove-ack/add-outcome flow driven by _OK_EMOJI/_FAIL_EMOJI class attrs and the _add_reaction(chat_id, message_id, emoji)/_remove_reaction(chat_id, message_id) primitive shape; default stays a no-op. - photon drops its override (exact behavioral match). - slack/discord/feishu/matrix/telegram/google_chat keep overrides: divergent primitive signatures (team_id routing, raw message objects, reaction-id handles, replace-semantics setMessageReaction) or extra state protocols. --- gateway/platforms/base.py | 43 ++++++++++++++++++++++++++++- plugins/platforms/photon/adapter.py | 28 +++++-------------- 2 files changed, 49 insertions(+), 22 deletions(-) diff --git a/gateway/platforms/base.py b/gateway/platforms/base.py index 761d997aadc..3963cef866d 100644 --- a/gateway/platforms/base.py +++ b/gateway/platforms/base.py @@ -4793,11 +4793,52 @@ class BasePlatformAdapter(ABC): # Subclasses override these to react to message processing events # (e.g. Discord adds 👀/✅/❌ reactions). + # Opt-in emoji set for the shared reaction-ack flow in + # ``on_processing_complete``. Adapters whose reaction primitives follow + # the ``_add_reaction(chat_id, message_id, emoji)`` / + # ``_remove_reaction(chat_id, message_id)`` shape can set these class + # attributes instead of overriding the hook. Left as ``None`` the hook + # stays a no-op (historical default). + _ACK_EMOJI: Optional[str] = None + _OK_EMOJI: Optional[str] = None + _FAIL_EMOJI: Optional[str] = None + async def on_processing_start(self, event: MessageEvent) -> None: """Hook called when background processing begins.""" async def on_processing_complete(self, event: MessageEvent, outcome: ProcessingOutcome) -> None: - """Hook called when background processing completes.""" + """Hook called when background processing completes. + + Default: shared reaction-ack flow — swap the in-progress reaction for + a final success/failure reaction. Runs only when the adapter opts in + by setting ``_OK_EMOJI`` / ``_FAIL_EMOJI`` class attributes AND + defines ``_add_reaction`` / ``_remove_reaction`` primitives taking + ``(chat_id, message_id[, emoji])``. Otherwise this is a no-op, as it + always was. Remove-then-add rather than a bare replace: deterministic + whether the platform replaces a sender's previous reaction or stacks + them. CANCELLED outcomes leave the message unreacted. + """ + if self._OK_EMOJI is None and self._FAIL_EMOJI is None: + return + add: Any = getattr(self, "_add_reaction", None) + remove: Any = getattr(self, "_remove_reaction", None) + if not callable(add) or not callable(remove): + return + enabled = getattr(self, "_reactions_enabled", None) + if callable(enabled) and not enabled(): + return + chat_id = getattr(event.source, "chat_id", None) + message_id = getattr(event, "message_id", None) + if not chat_id or not message_id: + return + await remove(chat_id, message_id) + if outcome == ProcessingOutcome.SUCCESS: + if self._OK_EMOJI: + await add(chat_id, message_id, self._OK_EMOJI) + elif outcome == ProcessingOutcome.FAILURE: + if self._FAIL_EMOJI: + await add(chat_id, message_id, self._FAIL_EMOJI) + # CANCELLED: leave the message unreacted. async def _run_processing_hook(self, hook_name: str, *args: Any, **kwargs: Any) -> None: """Run a lifecycle hook without letting failures break message flow.""" diff --git a/plugins/platforms/photon/adapter.py b/plugins/platforms/photon/adapter.py index 79e10c2d593..abebe3669f9 100644 --- a/plugins/platforms/photon/adapter.py +++ b/plugins/platforms/photon/adapter.py @@ -2295,27 +2295,13 @@ class PhotonAdapter(BasePlatformAdapter): if chat_id and message_id: await self._add_reaction(chat_id, message_id, "\U0001f440") - async def on_processing_complete( - self, event: MessageEvent, outcome: ProcessingOutcome - ) -> None: - """Swap the 👀 progress tapback for a 👍/👎 result. - - Remove-then-add rather than a bare replace: deterministic whether the - platform replaces a sender's previous tapback or stacks them, and it - keeps the sidecar's reaction-handle slot coherent. - """ - if not self._reactions_enabled(): - return - chat_id = getattr(event.source, "chat_id", None) - message_id = getattr(event, "message_id", None) - if not chat_id or not message_id: - return - await self._remove_reaction(chat_id, message_id) - if outcome == ProcessingOutcome.SUCCESS: - await self._add_reaction(chat_id, message_id, "\U0001f44d") - elif outcome == ProcessingOutcome.FAILURE: - await self._add_reaction(chat_id, message_id, "\U0001f44e") - # CANCELLED: leave the message unreacted. + # Shared reaction-ack flow (base.on_processing_complete): swap the 👀 + # progress tapback for a 👍/👎 result. Remove-then-add rather than a bare + # replace: deterministic whether the platform replaces a sender's previous + # tapback or stacks them, and it keeps the sidecar's reaction-handle slot + # coherent. CANCELLED: leave the message unreacted. + _OK_EMOJI = "\U0001f44d" + _FAIL_EMOJI = "\U0001f44e" async def get_chat_info(self, chat_id: str) -> Dict[str, Any]: """Return whatever we know about a Spectrum space id.