|
@@ -13,6 +13,7 @@ including both outbound messaging and realtime WebSocket connections.
|
|
|
from __future__ import annotations
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
import inspect
|
|
import inspect
|
|
|
|
|
+import random
|
|
|
import sys
|
|
import sys
|
|
|
import os
|
|
import os
|
|
|
from pathlib import Path
|
|
from pathlib import Path
|
|
@@ -32,9 +33,8 @@ import hashlib
|
|
|
import logging
|
|
import logging
|
|
|
import mimetypes
|
|
import mimetypes
|
|
|
import os
|
|
import os
|
|
|
-from collections import OrderedDict
|
|
|
|
|
from datetime import datetime, timezone
|
|
from datetime import datetime, timezone
|
|
|
-from typing import Any, Dict, List, Optional, Tuple, cast
|
|
|
|
|
|
|
+from typing import Any, Dict, List, Literal, Optional, Tuple, cast
|
|
|
from urllib.parse import urlsplit
|
|
from urllib.parse import urlsplit
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
logger = logging.getLogger(__name__)
|
|
@@ -63,7 +63,7 @@ try:
|
|
|
)
|
|
)
|
|
|
from .vendor.chattolib.realtime import (
|
|
from .vendor.chattolib.realtime import (
|
|
|
ChattoRealtimeError,
|
|
ChattoRealtimeError,
|
|
|
- ChattoRealtimeCloseError,
|
|
|
|
|
|
|
+ ChattoRealtimeCloseError, RealtimeEvent,
|
|
|
stream_events
|
|
stream_events
|
|
|
)
|
|
)
|
|
|
from .vendor.chattolib.realtime_types import (
|
|
from .vendor.chattolib.realtime_types import (
|
|
@@ -117,11 +117,10 @@ class ChattoAdapter(BasePlatformAdapter):
|
|
|
self._user_id: str = ""
|
|
self._user_id: str = ""
|
|
|
self._user_display: str = ""
|
|
self._user_display: str = ""
|
|
|
self._room_names: Dict[str, str] = {}
|
|
self._room_names: Dict[str, str] = {}
|
|
|
- self._room_kinds: Dict[str, str] = {}
|
|
|
|
|
|
|
+ self._room_kinds: Dict[str, RoomKind] = {}
|
|
|
self._our_thread_roots: set = set() # thread root event IDs we created
|
|
self._our_thread_roots: set = set() # thread root event IDs we created
|
|
|
self._our_message_ids: set = set() # message IDs we sent (for thread root detection)
|
|
self._our_message_ids: set = set() # message IDs we sent (for thread root detection)
|
|
|
- self._seen: list[str] = [] # room_id -> OrderedDict(event_id -> None)
|
|
|
|
|
- self._rt_events_seen: List[str] = [] # Plain RealtimeEvent-id list
|
|
|
|
|
|
|
+ self._seen: list[str] = [] # Plain RealtimeEvent-id list
|
|
|
self._resume_cursor: Optional[str] = None
|
|
self._resume_cursor: Optional[str] = None
|
|
|
self._watch_room_ids: List[str] = []
|
|
self._watch_room_ids: List[str] = []
|
|
|
self._ws_task: Optional[asyncio.Task] = None
|
|
self._ws_task: Optional[asyncio.Task] = None
|
|
@@ -390,13 +389,11 @@ class ChattoAdapter(BasePlatformAdapter):
|
|
|
return
|
|
return
|
|
|
|
|
|
|
|
if message.actor_id in self._user_cache:
|
|
if message.actor_id in self._user_cache:
|
|
|
|
|
+ # try the user cache.
|
|
|
user = self._user_cache.get(message.actor_id)
|
|
user = self._user_cache.get(message.actor_id)
|
|
|
else:
|
|
else:
|
|
|
# get the user and update cache.
|
|
# get the user and update cache.
|
|
|
- try:
|
|
|
|
|
- directory_member = await client.get_user(user_id=message.actor_id)
|
|
|
|
|
- except Exception:
|
|
|
|
|
- pass
|
|
|
|
|
|
|
+ directory_member = await client.get_user(user_id=message.actor_id)
|
|
|
if directory_member is None:
|
|
if directory_member is None:
|
|
|
return
|
|
return
|
|
|
user = directory_member.user
|
|
user = directory_member.user
|
|
@@ -412,29 +409,42 @@ class ChattoAdapter(BasePlatformAdapter):
|
|
|
|
|
|
|
|
# For DMs, always respond. For rooms with require_mention, only respond when mentioned.
|
|
# For DMs, always respond. For rooms with require_mention, only respond when mentioned.
|
|
|
# Strip the mention from the text for the agent
|
|
# Strip the mention from the text for the agent
|
|
|
|
|
+ # Todo: use a function that either reads from cache or gets room kind again.
|
|
|
|
|
+ if self._room_kinds.get(message.room_id) is None:
|
|
|
|
|
+ room_viewer_state = await client.get_room(message.room_id)
|
|
|
|
|
+ if room_viewer_state is None:
|
|
|
|
|
+ return
|
|
|
|
|
+ if room_viewer_state.room is None:
|
|
|
|
|
+ return
|
|
|
|
|
+ self._room_kinds[message.room_id] = room_viewer_state.room.kind or RoomKind.UNSPECIFIED
|
|
|
|
|
+
|
|
|
room_kind = self._room_kinds.get(message.room_id)
|
|
room_kind = self._room_kinds.get(message.room_id)
|
|
|
- chat_type = "dm" if room_kind == RoomKind.DM else room_kind or RoomKind.UNSPECIFIED # Only "dm" seems to be a reserved keyword from Base adapter class.
|
|
|
|
|
|
|
+
|
|
|
|
|
+ logger.info("message_body: %s room_kind: %s", message_body, room_kind)
|
|
|
|
|
|
|
|
mentioned = False
|
|
mentioned = False
|
|
|
- if (room_kind is RoomKind.CHANNEL and self.chatto_config.require_mention.value):
|
|
|
|
|
|
|
+ if (room_kind == RoomKind.CHANNEL and self.chatto_config.require_mention.value):
|
|
|
if self.me.login and not mentioned:
|
|
if self.me.login and not mentioned:
|
|
|
- mentioned = f"@{self.me.login}" in message_body
|
|
|
|
|
|
|
+ mentioned = bool(f"@{self.me.login}" in message_body)
|
|
|
if self.me.display_name and not mentioned:
|
|
if self.me.display_name and not mentioned:
|
|
|
- mentioned = f"@{self.me.display_name}" in message_body
|
|
|
|
|
- if not mentioned:
|
|
|
|
|
|
|
+ mentioned = bool(f"@{self.me.display_name}" in message_body)
|
|
|
|
|
+ if mentioned is False:
|
|
|
|
|
+ logger.debug("Discarding message. Bot was not mentionend but require_mention is '%s'.", self.chatto_config.require_mention.value)
|
|
|
return
|
|
return
|
|
|
|
|
+
|
|
|
|
|
+ logger.info("mentioned: %s", mentioned)
|
|
|
|
|
|
|
|
# Thread anchoring — if the incoming message is inside a thread, we
|
|
# Thread anchoring — if the incoming message is inside a thread, we
|
|
|
# keep that thread by default; otherwise leave thread_id unset so
|
|
# keep that thread by default; otherwise leave thread_id unset so
|
|
|
# replies land at the root.
|
|
# replies land at the root.
|
|
|
thread_id = payload.thread_root_event_id or None
|
|
thread_id = payload.thread_root_event_id or None
|
|
|
- if not thread_id and chat_type != "dm" and self.chatto_config.auto_thread.value:
|
|
|
|
|
|
|
+ if not thread_id and room_kind != RoomKind.DM and self.chatto_config.auto_thread.value:
|
|
|
thread_id = message.id
|
|
thread_id = message.id
|
|
|
|
|
|
|
|
source = self.build_source(
|
|
source = self.build_source(
|
|
|
chat_id=payload.room_id,
|
|
chat_id=payload.room_id,
|
|
|
chat_name=self._room_names.get(message.room_id),
|
|
chat_name=self._room_names.get(message.room_id),
|
|
|
- chat_type=chat_type,
|
|
|
|
|
|
|
+ chat_type="dm" if room_kind == RoomKind.DM else room_kind or RoomKind.UNSPECIFIED, # Only "dm" seems to be a reserved keyword from Base adapter class.,
|
|
|
user_id=message.actor_id,
|
|
user_id=message.actor_id,
|
|
|
user_name=user.login, # use login, because display_name is changeable by anyone.
|
|
user_name=user.login, # use login, because display_name is changeable by anyone.
|
|
|
thread_id=thread_id,
|
|
thread_id=thread_id,
|
|
@@ -462,7 +472,34 @@ class ChattoAdapter(BasePlatformAdapter):
|
|
|
|
|
|
|
|
await self.handle_message(message_event)
|
|
await self.handle_message(message_event)
|
|
|
return
|
|
return
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
|
|
+ async def _handle_realtime_event(self, event: RealtimeEvent) -> None:
|
|
|
|
|
+ if self._is_seen(event.id):
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ if event.actor_id is None:
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ logger.info("EVENT happened: '%s' from %s", event.kind, event.actor_id)
|
|
|
|
|
+
|
|
|
|
|
+ if (event_payload := event.get("message_posted")) is not None:
|
|
|
|
|
+
|
|
|
|
|
+ # Self-event filter — the actor_id on the envelope is authoritative
|
|
|
|
|
+ # (chattolib does NOT filter this itself; see chatto-bridge notes).
|
|
|
|
|
+ actor_id = event.actor_id
|
|
|
|
|
+ if actor_id and actor_id == self.me.id:
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ await self._dispatch_message_posted(event_payload)
|
|
|
|
|
+
|
|
|
|
|
+ # confirmed:
|
|
|
|
|
+ elif event.kind in ("mention_notification", "projection_event", "presence_changed", "notification_dismissed",
|
|
|
|
|
+ "room_marked_as_read", "user_typing", "notification_created", "new_direct_message_notification",
|
|
|
|
|
+ 'reaction_removed', 'reaction_added'):
|
|
|
|
|
+ logger.debug("Chatto: '%s' event received. Not yet implemented or not needed.", event.kind)
|
|
|
|
|
+ else:
|
|
|
|
|
+ logger.error("Chatto: unknown event kind: '%s'", event.kind)
|
|
|
|
|
+
|
|
|
|
|
|
|
|
async def _chattolib_event_loop(self) -> None:
|
|
async def _chattolib_event_loop(self) -> None:
|
|
|
"""Event loop using chattolib's stream_events.
|
|
"""Event loop using chattolib's stream_events.
|
|
@@ -470,82 +507,74 @@ class ChattoAdapter(BasePlatformAdapter):
|
|
|
This replaces the manual WebSocket loop with chattolib's high-level
|
|
This replaces the manual WebSocket loop with chattolib's high-level
|
|
|
stream_events() which provides pre-decoded RealtimeEvent objects.
|
|
stream_events() which provides pre-decoded RealtimeEvent objects.
|
|
|
"""
|
|
"""
|
|
|
- try:
|
|
|
|
|
- client = await self._require_client()
|
|
|
|
|
- except RuntimeError:
|
|
|
|
|
- logger.warning("Chatto: chattolib event loop aborted - no client available")
|
|
|
|
|
- return
|
|
|
|
|
|
|
+
|
|
|
# Ensure ws_ready is available for synchronization with starter
|
|
# Ensure ws_ready is available for synchronization with starter
|
|
|
- assert self._ws_ready is not None
|
|
|
|
|
backoff = ChattoConstants.WS_RECONNECT_INITIAL_BACKOFF
|
|
backoff = ChattoConstants.WS_RECONNECT_INITIAL_BACKOFF
|
|
|
-
|
|
|
|
|
- await self._refresh_rooms()
|
|
|
|
|
- logger.info("Chatto: starting chattolib event stream with %d rooms", len(self._watch_room_ids))
|
|
|
|
|
|
|
|
|
|
|
|
+ delay = ChattoConstants.WS_RECONNECT_INITIAL_BACKOFF
|
|
|
while not self._closing:
|
|
while not self._closing:
|
|
|
-
|
|
|
|
|
- backoff = ChattoConstants.WS_RECONNECT_INITIAL_BACKOFF
|
|
|
|
|
- logger.info("Outside of event stream")
|
|
|
|
|
-
|
|
|
|
|
try:
|
|
try:
|
|
|
- # Start streaming events
|
|
|
|
|
- async for event in stream_events(client):
|
|
|
|
|
-
|
|
|
|
|
- if self._is_seen(event.id):
|
|
|
|
|
- continue
|
|
|
|
|
-
|
|
|
|
|
- # Signal that we're connected and ready
|
|
|
|
|
- if not self._ws_ready.is_set():
|
|
|
|
|
- self._ws_active = True
|
|
|
|
|
- self._ws_ready.set()
|
|
|
|
|
|
|
+ client = await self._require_client()
|
|
|
|
|
|
|
|
-
|
|
|
|
|
- logger.info("EVENT happened: event: %s from %s", event.kind, event.actor_id)
|
|
|
|
|
|
|
+ await self._refresh_rooms()
|
|
|
|
|
+ logger.info("Chatto: starting chattolib event stream with %d rooms", len(self._watch_room_ids))
|
|
|
|
|
|
|
|
- if (event_payload := event.get("message_posted")) is not None:
|
|
|
|
|
|
|
+ async for event in stream_events(client):
|
|
|
|
|
+ if self._closing:
|
|
|
|
|
+ return
|
|
|
|
|
+ await self._handle_realtime_event(event)
|
|
|
|
|
+ # Iterator exited cleanly — treat as a normal close and reconnect
|
|
|
|
|
+ # with the local backoff (no server hint available).
|
|
|
|
|
+ logger.info("Chatto: realtime stream ended, reconnecting")
|
|
|
|
|
+ except asyncio.CancelledError:
|
|
|
|
|
+ return
|
|
|
|
|
+ except ChattoRealtimeCloseError as exc:
|
|
|
|
|
+ if not exc.reconnect:
|
|
|
|
|
+ logger.error(
|
|
|
|
|
+ "Chatto: realtime closed by server (%s: %s), not reconnecting",
|
|
|
|
|
+ exc.code, exc.message,
|
|
|
|
|
+ )
|
|
|
|
|
+ return
|
|
|
|
|
+ wait = max(exc.retry_after_ms / 1000.0, ChattoConstants.WS_RECONNECT_INITIAL_BACKOFF)
|
|
|
|
|
+ logger.warning(
|
|
|
|
|
+ "Chatto: realtime closed by server (%s), reconnecting in %.1fs",
|
|
|
|
|
+ exc.code, wait,
|
|
|
|
|
+ )
|
|
|
|
|
+ delay = ChattoConstants.WS_RECONNECT_INITIAL_BACKOFF # server hint supersedes local backoff
|
|
|
|
|
+ await self._sleep_interruptible(wait)
|
|
|
|
|
+ continue
|
|
|
|
|
+ except ChattoRealtimeError as exc:
|
|
|
|
|
+ if getattr(exc, "fatal", False):
|
|
|
|
|
+ logger.error("Chatto: fatal realtime error (%s): %s", exc.code, exc.message)
|
|
|
|
|
+ return
|
|
|
|
|
+ logger.warning(
|
|
|
|
|
+ "Chatto: realtime error (%s: %s), reconnecting in %.1fs",
|
|
|
|
|
+ exc.code, exc.message, delay,
|
|
|
|
|
+ )
|
|
|
|
|
+ except Exception as exc:
|
|
|
|
|
+ logger.warning(
|
|
|
|
|
+ "Chatto: unexpected realtime error: %s, reconnecting in %.1fs",
|
|
|
|
|
+ exc, delay,
|
|
|
|
|
+ )
|
|
|
|
|
|
|
|
- # Self-event filter — the actor_id on the envelope is authoritative
|
|
|
|
|
- # (chattolib does NOT filter this itself; see chatto-bridge notes).
|
|
|
|
|
- actor_id = event.actor_id
|
|
|
|
|
- if actor_id and actor_id == self.me.id:
|
|
|
|
|
- return
|
|
|
|
|
|
|
+ if self._closing:
|
|
|
|
|
+ return
|
|
|
|
|
|
|
|
- await self._dispatch_message_posted(event_payload)
|
|
|
|
|
-
|
|
|
|
|
- # confirmed:
|
|
|
|
|
- elif event.kind in ("mention_notification", "projection_event", "presence_changed", "notification_dismissed",
|
|
|
|
|
- "room_marked_as_read", "user_typing", "notification_created"):
|
|
|
|
|
- logger.debug("Chatto: '%s' event received. Not yet implemented or not needed.", event.kind)
|
|
|
|
|
- else:
|
|
|
|
|
- logger.error("Chatto: unknown event kind: '%s'", event.kind)
|
|
|
|
|
-
|
|
|
|
|
- except ChattoRealtimeCloseError as e:
|
|
|
|
|
- logger.warning("Chatto: realtime closed by server: %s (reconnect=%s)", e.message, e.reconnect)
|
|
|
|
|
- if e.reconnect:
|
|
|
|
|
- self._ws_active = False
|
|
|
|
|
- await asyncio.sleep(backoff)
|
|
|
|
|
- backoff = min(backoff * 2, ChattoConstants.WS_RECONNECT_MAX_BACKOFF)
|
|
|
|
|
- continue
|
|
|
|
|
- raise
|
|
|
|
|
- except ChattoRealtimeError as e:
|
|
|
|
|
- logger.warning("Chatto: realtime error: %s (fatal=%s)", e.message, e.fatal)
|
|
|
|
|
- if e.fatal:
|
|
|
|
|
- raise
|
|
|
|
|
- except (ChattoError, asyncio.CancelledError):
|
|
|
|
|
- raise
|
|
|
|
|
- except Exception as e:
|
|
|
|
|
- self._ws_active = False
|
|
|
|
|
- logger.warning("Chatto: event stream error: %s, retrying in %.1fs", e, backoff)
|
|
|
|
|
- await asyncio.sleep(backoff)
|
|
|
|
|
- backoff = min(backoff * 2, ChattoConstants.WS_RECONNECT_MAX_BACKOFF)
|
|
|
|
|
|
|
+ jitter = delay * 0.2 * random.random()
|
|
|
|
|
+ await self._sleep_interruptible(delay + jitter)
|
|
|
|
|
+ delay = min(delay * 2, ChattoConstants.WS_RECONNECT_MAX_BACKOFF)
|
|
|
|
|
|
|
|
await asyncio.sleep(backoff)
|
|
await asyncio.sleep(backoff)
|
|
|
|
|
|
|
|
|
|
|
|
|
- # If we didn't find the event, refresh known rooms
|
|
|
|
|
- # by delegating to a dedicated helper. This avoids duplicate logic
|
|
|
|
|
- # across transient/projection event handlers.
|
|
|
|
|
- await self._refresh_rooms()
|
|
|
|
|
|
|
+ async def _sleep_interruptible(self, seconds: float) -> None:
|
|
|
|
|
+ """Sleep in short slices so disconnect() cancels promptly."""
|
|
|
|
|
+ end = asyncio.get_running_loop().time() + seconds
|
|
|
|
|
+ while not self._closing:
|
|
|
|
|
+ remaining = end - asyncio.get_running_loop().time()
|
|
|
|
|
+ if remaining <= 0:
|
|
|
|
|
+ return
|
|
|
|
|
+ await asyncio.sleep(min(remaining, 0.5))
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _refresh_rooms(self) -> None:
|
|
async def _refresh_rooms(self) -> None:
|
|
@@ -582,7 +611,6 @@ class ChattoAdapter(BasePlatformAdapter):
|
|
|
for rid in new_room_ids:
|
|
for rid in new_room_ids:
|
|
|
if self._room_kinds.get(rid) != RoomKind.DM:
|
|
if self._room_kinds.get(rid) != RoomKind.DM:
|
|
|
await client.join_room(rid) # but list_rooms() would not return any if we were not already joined?!
|
|
await client.join_room(rid) # but list_rooms() would not return any if we were not already joined?!
|
|
|
- self._seen.append(rid)
|
|
|
|
|
await self._seed_room(rid)
|
|
await self._seed_room(rid)
|
|
|
self._watch_room_ids.append(rid)
|
|
self._watch_room_ids.append(rid)
|
|
|
|
|
|
|
@@ -653,8 +681,8 @@ class ChattoAdapter(BasePlatformAdapter):
|
|
|
if not thread_id:
|
|
if not thread_id:
|
|
|
thread_id = reply_to
|
|
thread_id = reply_to
|
|
|
# Check if this is a DM room — DMs don't support threads
|
|
# Check if this is a DM room — DMs don't support threads
|
|
|
- room_kind = self._room_kinds.get(str(chat_id), "")
|
|
|
|
|
- is_dm = room_kind == "ROOM_KIND_DM" or room_kind == "dm"
|
|
|
|
|
|
|
+ room_kind = self._room_kinds.get(chat_id)
|
|
|
|
|
+ is_dm = room_kind == RoomKind.DM
|
|
|
if is_dm:
|
|
if is_dm:
|
|
|
thread_id = None
|
|
thread_id = None
|
|
|
|
|
|
|
@@ -676,7 +704,7 @@ class ChattoAdapter(BasePlatformAdapter):
|
|
|
for i, chunk in enumerate(chunks):
|
|
for i, chunk in enumerate(chunks):
|
|
|
try:
|
|
try:
|
|
|
msg_obj = await client.post_message(
|
|
msg_obj = await client.post_message(
|
|
|
- room_id=str(chat_id),
|
|
|
|
|
|
|
+ room_id=chat_id,
|
|
|
body=chunk,
|
|
body=chunk,
|
|
|
thread_root_event_id=str(thread_id) if thread_id else "",
|
|
thread_root_event_id=str(thread_id) if thread_id else "",
|
|
|
)
|
|
)
|
|
@@ -710,11 +738,7 @@ class ChattoAdapter(BasePlatformAdapter):
|
|
|
# Thread following (best-effort, Chatto-unique)
|
|
# Thread following (best-effort, Chatto-unique)
|
|
|
# ------------------------------------------------------------------ #
|
|
# ------------------------------------------------------------------ #
|
|
|
if thread_id and message_ids:
|
|
if thread_id and message_ids:
|
|
|
- try:
|
|
|
|
|
await client.follow_thread(chat_id, thread_id)
|
|
await client.follow_thread(chat_id, thread_id)
|
|
|
- except Exception:
|
|
|
|
|
- logger.debug("Chatto: _follow_thread failed for room=%s thread=%s",
|
|
|
|
|
- chat_id, thread_id, exc_info=True)
|
|
|
|
|
|
|
|
|
|
return SendResult(success=True, message_id=first_id, raw_response=last_resp)
|
|
return SendResult(success=True, message_id=first_id, raw_response=last_resp)
|
|
|
|
|
|
|
@@ -795,25 +819,26 @@ class ChattoAdapter(BasePlatformAdapter):
|
|
|
# Already a shortcode like "thumbsup" — return as-is
|
|
# Already a shortcode like "thumbsup" — return as-is
|
|
|
return emoji
|
|
return emoji
|
|
|
|
|
|
|
|
- async def add_reaction(self, chat_id: str, message_id: str, emoji: str) -> bool:
|
|
|
|
|
|
|
+ async def add_reaction(self, room_id: str, message_id: str, emoji: str) -> bool:
|
|
|
"""Add a reaction to a message via MessageService/AddReaction."""
|
|
"""Add a reaction to a message via MessageService/AddReaction."""
|
|
|
shortcode = self._emoji_to_shortcode(emoji)
|
|
shortcode = self._emoji_to_shortcode(emoji)
|
|
|
try:
|
|
try:
|
|
|
try:
|
|
try:
|
|
|
client = await self._require_client()
|
|
client = await self._require_client()
|
|
|
except RuntimeError:
|
|
except RuntimeError:
|
|
|
|
|
+ logger.warning("Chatto: AddReaction — client unavailable")
|
|
|
return False
|
|
return False
|
|
|
result = await client.add_reaction(
|
|
result = await client.add_reaction(
|
|
|
- room_id=chat_id,
|
|
|
|
|
|
|
+ room_id=room_id,
|
|
|
message_event_id=message_id,
|
|
message_event_id=message_id,
|
|
|
emoji=shortcode,
|
|
emoji=shortcode,
|
|
|
)
|
|
)
|
|
|
return result
|
|
return result
|
|
|
except ChattoError as e:
|
|
except ChattoError as e:
|
|
|
- logger.debug("Chatto: AddReaction failed: %s", e)
|
|
|
|
|
|
|
+ logger.warning("Chatto: AddReaction failed: %s", e)
|
|
|
return False
|
|
return False
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
- logger.debug("Chatto: AddReaction error: %s", e)
|
|
|
|
|
|
|
+ logger.warning("Chatto: AddReaction error: %s", e)
|
|
|
return False
|
|
return False
|
|
|
|
|
|
|
|
async def remove_reaction(self, chat_id: str, message_id: str, emoji: str) -> bool:
|
|
async def remove_reaction(self, chat_id: str, message_id: str, emoji: str) -> bool:
|
|
@@ -823,6 +848,7 @@ class ChattoAdapter(BasePlatformAdapter):
|
|
|
try:
|
|
try:
|
|
|
client = await self._require_client()
|
|
client = await self._require_client()
|
|
|
except RuntimeError:
|
|
except RuntimeError:
|
|
|
|
|
+ logger.warning("Chatto: RemoveReaction — client unavailable")
|
|
|
return False
|
|
return False
|
|
|
result = await client.remove_reaction(
|
|
result = await client.remove_reaction(
|
|
|
room_id=str(chat_id),
|
|
room_id=str(chat_id),
|
|
@@ -831,10 +857,10 @@ class ChattoAdapter(BasePlatformAdapter):
|
|
|
)
|
|
)
|
|
|
return result
|
|
return result
|
|
|
except ChattoError as e:
|
|
except ChattoError as e:
|
|
|
- logger.debug("Chatto: RemoveReaction failed: %s", e)
|
|
|
|
|
|
|
+ logger.warning("Chatto: RemoveReaction failed: %s", e)
|
|
|
return False
|
|
return False
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
- logger.debug("Chatto: RemoveReaction error: %s", e)
|
|
|
|
|
|
|
+ logger.warning("Chatto: RemoveReaction error: %s", e)
|
|
|
return False
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
@@ -855,8 +881,8 @@ class ChattoAdapter(BasePlatformAdapter):
|
|
|
except RuntimeError:
|
|
except RuntimeError:
|
|
|
return None
|
|
return None
|
|
|
room = await client.start_dm(participant_ids=[str(user_id)])
|
|
room = await client.start_dm(participant_ids=[str(user_id)])
|
|
|
- self._room_names[room.id] = self._room_names.get(room.id, "")
|
|
|
|
|
- self._room_kinds[room.id] = "ROOM_KIND_DM"
|
|
|
|
|
|
|
+ self._room_names[room.id] = room.name
|
|
|
|
|
+ self._room_kinds[room.id] = room.kind
|
|
|
return room.id
|
|
return room.id
|
|
|
except ChattoError as e:
|
|
except ChattoError as e:
|
|
|
logger.debug("Chatto: StartDM failed: %s", e)
|
|
logger.debug("Chatto: StartDM failed: %s", e)
|
|
@@ -895,8 +921,8 @@ class ChattoAdapter(BasePlatformAdapter):
|
|
|
)
|
|
)
|
|
|
rid = str(room.id) if room else ""
|
|
rid = str(room.id) if room else ""
|
|
|
if rid:
|
|
if rid:
|
|
|
- self._room_names[rid] = name
|
|
|
|
|
- self._room_kinds[rid] = "ROOM_KIND_GROUP"
|
|
|
|
|
|
|
+ self._room_names[rid] = room.name
|
|
|
|
|
+ self._room_kinds[rid] = room.kind
|
|
|
return rid
|
|
return rid
|
|
|
logger.debug("Chatto: CreateRoom returned no room id")
|
|
logger.debug("Chatto: CreateRoom returned no room id")
|
|
|
return None
|
|
return None
|
|
@@ -925,9 +951,14 @@ class ChattoAdapter(BasePlatformAdapter):
|
|
|
|
|
|
|
|
BasePlatformAdapter override
|
|
BasePlatformAdapter override
|
|
|
"""
|
|
"""
|
|
|
|
|
+ logger.info("self.chatto_config.reactions.value: %s", self.chatto_config.reactions.value)
|
|
|
if not self.chatto_config.reactions.value:
|
|
if not self.chatto_config.reactions.value:
|
|
|
return
|
|
return
|
|
|
|
|
+
|
|
|
chat_id, message_id = self._event_room_and_message_id(event)
|
|
chat_id, message_id = self._event_room_and_message_id(event)
|
|
|
|
|
+ if not chat_id or not message_id:
|
|
|
|
|
+ logger.warning("Chatto: on_processing_start — empty chat_id or message_id, skipping reaction")
|
|
|
|
|
+ return
|
|
|
await self.add_reaction(chat_id, message_id, "👀")
|
|
await self.add_reaction(chat_id, message_id, "👀")
|
|
|
|
|
|
|
|
async def on_processing_complete(
|
|
async def on_processing_complete(
|