|
|
@@ -1781,6 +1781,168 @@ class ChattoAdapter(BasePlatformAdapter):
|
|
|
"""
|
|
|
return self._is_authorized(user_id)
|
|
|
|
|
|
+ def _interactive_prompt_expired(self, prompt: Any) -> bool:
|
|
|
+ """Whether an interactive prompt has passed its expiry deadline."""
|
|
|
+ try:
|
|
|
+ return time.monotonic() > float(prompt.expires_at)
|
|
|
+ except (TypeError, ValueError, AttributeError):
|
|
|
+ return False
|
|
|
+
|
|
|
+ async def _try_handle_interactive_reaction(
|
|
|
+ self,
|
|
|
+ room_id: Any,
|
|
|
+ message_id: Any,
|
|
|
+ emoji: Any,
|
|
|
+ actor_id: Any,
|
|
|
+ ) -> bool:
|
|
|
+ """Route a reaction on a tracked interactive prompt.
|
|
|
+
|
|
|
+ Returns True when the message is a tracked prompt (consumed), False
|
|
|
+ otherwise so the caller forwards normally.
|
|
|
+ """
|
|
|
+ if not room_id or not message_id or not emoji or not actor_id:
|
|
|
+ return False
|
|
|
+ room_id = str(room_id)
|
|
|
+ message_id = str(message_id)
|
|
|
+ emoji = str(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
|
|
|
+
|
|
|
+ if is_clarify:
|
|
|
+ prompt = self._clarify_prompts.get(message_id)
|
|
|
+ else:
|
|
|
+ prompt = self._approval_prompts.get(message_id)
|
|
|
+ if prompt is None:
|
|
|
+ return False
|
|
|
+
|
|
|
+ # Room must match.
|
|
|
+ if str(getattr(prompt, "chat_id", "")) != room_id:
|
|
|
+ return True
|
|
|
+
|
|
|
+ # Expiry handling.
|
|
|
+ if self._interactive_prompt_expired(prompt):
|
|
|
+ try:
|
|
|
+ await self.edit_message(
|
|
|
+ room_id, message_id, "⏰ This prompt has expired."
|
|
|
+ )
|
|
|
+ except Exception:
|
|
|
+ logger.debug("Chatto: expired prompt edit failed", exc_info=True)
|
|
|
+ if is_clarify:
|
|
|
+ self._clarify_prompts.pop(message_id, None)
|
|
|
+ cid = getattr(prompt, "clarify_id", None)
|
|
|
+ if cid is not None:
|
|
|
+ self._clarify_id_to_message.pop(cid, None)
|
|
|
+ else:
|
|
|
+ self._approval_prompts.pop(message_id, None)
|
|
|
+ return True
|
|
|
+
|
|
|
+ # Authorization (fail-closed).
|
|
|
+ if not self._is_allowed_user_id(actor_id):
|
|
|
+ return True
|
|
|
+
|
|
|
+ # Double-click guard.
|
|
|
+ if getattr(prompt, "resolved", False):
|
|
|
+ return True
|
|
|
+ if is_clarify and getattr(prompt, "awaiting_text", False):
|
|
|
+ return True
|
|
|
+
|
|
|
+ user = self._user_cache.get(actor_id)
|
|
|
+ user_label = (
|
|
|
+ getattr(user, "name", None) or getattr(user, "login", None) or actor_id
|
|
|
+ )
|
|
|
+
|
|
|
+ if is_clarify:
|
|
|
+ number_emojis = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣"]
|
|
|
+ other_emoji = "✏️"
|
|
|
+ choices = list(getattr(prompt, "choices", []) or [])
|
|
|
+
|
|
|
+ if emoji == other_emoji:
|
|
|
+ setattr(prompt, "awaiting_text", True)
|
|
|
+ try:
|
|
|
+ import tools.clarify_gateway as clarify_gateway
|
|
|
+
|
|
|
+ clarify_gateway.mark_awaiting_text(getattr(prompt, "clarify_id"))
|
|
|
+ except Exception:
|
|
|
+ logger.debug("Chatto: mark_awaiting_text failed", exc_info=True)
|
|
|
+ try:
|
|
|
+ await self.edit_message(
|
|
|
+ room_id, message_id, f"awaiting text from {user_label}"
|
|
|
+ )
|
|
|
+ except Exception:
|
|
|
+ logger.debug("Chatto: prompt edit failed", exc_info=True)
|
|
|
+ return True
|
|
|
+
|
|
|
+ if emoji in number_emojis:
|
|
|
+ idx = number_emojis.index(emoji)
|
|
|
+ if 0 <= idx < len(choices):
|
|
|
+ choice = choices[idx]
|
|
|
+ prompt.resolved = True
|
|
|
+ self._clarify_prompts.pop(message_id, None)
|
|
|
+ cid = getattr(prompt, "clarify_id", None)
|
|
|
+ if cid is not None:
|
|
|
+ self._clarify_id_to_message.pop(cid, None)
|
|
|
+ try:
|
|
|
+ import tools.clarify_gateway as clarify_gateway
|
|
|
+
|
|
|
+ clarify_gateway.resolve_gateway_clarify(
|
|
|
+ getattr(prompt, "clarify_id"), choice
|
|
|
+ )
|
|
|
+ except Exception:
|
|
|
+ logger.exception("Chatto: resolve_gateway_clarify failed")
|
|
|
+ try:
|
|
|
+ await self.edit_message(
|
|
|
+ room_id,
|
|
|
+ message_id,
|
|
|
+ f"✅ {user_label} chose: {choice}",
|
|
|
+ )
|
|
|
+ except Exception:
|
|
|
+ logger.debug("Chatto: prompt edit failed", exc_info=True)
|
|
|
+ return True
|
|
|
+
|
|
|
+ # Invalid emoji feedback.
|
|
|
+ try:
|
|
|
+ await self.send(
|
|
|
+ room_id, "That reaction isn't a valid option for this prompt."
|
|
|
+ )
|
|
|
+ except Exception:
|
|
|
+ logger.debug("Chatto: invalid reaction feedback failed", exc_info=True)
|
|
|
+ return True
|
|
|
+
|
|
|
+ # Approval prompt.
|
|
|
+ choices_map = getattr(prompt, "choices_map", {}) or {}
|
|
|
+ if emoji in choices_map:
|
|
|
+ choice = choices_map[emoji]
|
|
|
+ prompt.resolved = True
|
|
|
+ self._approval_prompts.pop(message_id, None)
|
|
|
+ try:
|
|
|
+ import tools.approval as approval_mod
|
|
|
+
|
|
|
+ approval_mod.resolve_gateway_approval(
|
|
|
+ str(getattr(prompt, "session_key", "")), choice
|
|
|
+ )
|
|
|
+ except Exception:
|
|
|
+ logger.exception("Chatto: resolve_gateway_approval failed")
|
|
|
+ try:
|
|
|
+ await self.edit_message(
|
|
|
+ room_id, message_id, f"✅ {user_label}: {choice}"
|
|
|
+ )
|
|
|
+ except Exception:
|
|
|
+ logger.debug("Chatto: prompt edit failed", exc_info=True)
|
|
|
+ return True
|
|
|
+
|
|
|
+ # Invalid emoji feedback.
|
|
|
+ try:
|
|
|
+ await self.send(
|
|
|
+ room_id, "That reaction isn't a valid option for this prompt."
|
|
|
+ )
|
|
|
+ except Exception:
|
|
|
+ logger.debug("Chatto: invalid reaction feedback failed", exc_info=True)
|
|
|
+ return True
|
|
|
+
|
|
|
def _evict_roster(self, room_id: str) -> None:
|
|
|
"""Drop all roster state for a room.
|
|
|
|
|
|
@@ -2283,6 +2445,22 @@ class ChattoAdapter(BasePlatformAdapter):
|
|
|
await self._dispatch_message_edited(event.payload)
|
|
|
|
|
|
elif event.kind in ("reaction_added", "reaction_removed"):
|
|
|
+ # Interactive prompts (clarify / exec-approval) are driven by
|
|
|
+ # reactions. Route a reaction on a tracked prompt before
|
|
|
+ # forwarding — the prompt is consumed either way, but the reaction
|
|
|
+ # still reaches the gateway hook surface afterward.
|
|
|
+ if event.kind == "reaction_added":
|
|
|
+ try:
|
|
|
+ await self._try_handle_interactive_reaction(
|
|
|
+ getattr(event.payload, "room_id", None),
|
|
|
+ getattr(event.payload, "message_event_id", None),
|
|
|
+ getattr(event.payload, "emoji", None),
|
|
|
+ actor_id,
|
|
|
+ )
|
|
|
+ except Exception:
|
|
|
+ logger.debug(
|
|
|
+ "Chatto: interactive reaction routing failed", exc_info=True
|
|
|
+ )
|
|
|
await self._forward_reaction(
|
|
|
event,
|
|
|
event.payload,
|