from __future__ import annotations import asyncio import logging import threading logger = logging.getLogger(__name__) class DingTalkWsConnection: def __init__(self, *, client_id: str, client_secret: str) -> None: self._client_id = client_id self._client_secret = client_secret self._stream_client = None self._loop: asyncio.AbstractEventLoop | None = None self._on_message = None self._connected = False self._thread: threading.Thread | None = None @property def channel_type(self) -> str: return "dingtalk" @property def is_connected(self) -> bool: return self._connected async def start( self, loop: asyncio.AbstractEventLoop, on_message, ) -> None: self._loop = loop self._on_message = on_message from dingtalk_stream import AuthClientCredential, DingTalkStreamClient credential = AuthClientCredential(self._client_id, self._client_secret) self._stream_client = DingTalkStreamClient(credential) self._stream_client.register_callback_listener( "/v1.0/im/bot/messages/get", self._on_dingtalk_event, ) self._thread = threading.Thread(target=self._run_stream_client, daemon=True, name="dingtalk-ws") self._thread.start() await self._wait_for_connection(timeout=10.0) logger.info( "dingtalk ws started, client_id=%s", self._client_id[:4] + "..." if self._client_id else "N/A", ) def _run_stream_client(self) -> None: try: self._connected = True self._stream_client.start() except Exception: self._connected = False logger.exception("dingtalk ws client crashed") async def _wait_for_connection(self, timeout: float = 10.0) -> None: elapsed = 0.0 interval = 0.5 while elapsed < timeout: if self._thread and not self._thread.is_alive(): self._connected = False logger.error("dingtalk ws thread exited prematurely") return await asyncio.sleep(interval) elapsed += interval async def stop(self) -> None: self._connected = False if self._stream_client and hasattr(self._stream_client, "close"): self._stream_client.close() def _on_dingtalk_event(self, data: dict) -> None: raw = self._event_to_raw(data) if self._loop and self._on_message: try: future = asyncio.run_coroutine_threadsafe(self._on_message(raw), self._loop) future.add_done_callback(self._callback_done) except RuntimeError: logger.warning("dingtalk ws bridge: event loop is closed") def _event_to_raw(self, data: dict) -> dict: return { "channel_type": "dingtalk", "header": {"event_id": data.get("headers", {}).get("eventId", "")}, "event": { "message": { "message_id": data.get("data", {}).get("messageId", ""), "chat_id": data.get("data", {}).get("conversationId", ""), "chat_type": data.get("data", {}).get("conversationType", ""), "message_type": data.get("data", {}).get("msgtype", "text"), "content": data.get("data", {}).get("text", {}).get("content", ""), }, "sender": { "sender_id": { "user_id": data.get("data", {}).get("senderId", ""), }, }, }, } @staticmethod def _callback_done(future): try: future.result() except Exception: logger.exception("ws→asyncio bridge error")