from __future__ import annotations import logging from typing import TYPE_CHECKING, Any import httpx from yuxi.channels.models import DeliveryResult if TYPE_CHECKING: from yuxi.channels.adapters.dingding.token import DingDingTokenManager logger = logging.getLogger(__name__) DINGDING_EMOJI_MAP: dict[str, tuple[str, str]] = { "👍": ("101", "like"), "👎": ("102", "dislike"), "❤️": ("103", "heart"), "😂": ("104", "laugh"), "😮": ("105", "surprise"), "😢": ("106", "sad"), "👀": ("107", "looking"), "✅": ("108", "done"), "❌": ("109", "error"), "🤔": ("110", "thinking"), "🎉": ("111", "celebrate"), "🔥": ("112", "fire"), "💯": ("113", "hundred"), "🙏": ("114", "pray"), } EMOTION_TYPE_TO_EMOJI: dict[str, str] = {emotion_type: emoji for emoji, (emotion_type, _) in DINGDING_EMOJI_MAP.items()} EMOTION_NAME_TO_EMOJI: dict[str, str] = {name: emoji for emoji, (_, name) in DINGDING_EMOJI_MAP.items()} def resolve_emoji(emoji_or_name: str) -> tuple[str, str] | None: if emoji_or_name in DINGDING_EMOJI_MAP: return DINGDING_EMOJI_MAP[emoji_or_name] lowered = emoji_or_name.lower() for emoji, (emotion_type, name) in DINGDING_EMOJI_MAP.items(): if name == lowered: return (emotion_type, name) return None async def send_reaction( token_manager: DingDingTokenManager, open_conversation_id: str, robot_code: str, msg_id: str, emoji: str, *, http_client: httpx.AsyncClient | None = None, ) -> DeliveryResult: emotion_info = DINGDING_EMOJI_MAP.get(emoji) if not emotion_info: emotion_info = resolve_emoji(emoji) if not emotion_info: return DeliveryResult(success=False, error=f"Unsupported emoji: {emoji}") emotion_type, emotion_name = emotion_info try: token = await token_manager.get_token() url = "https://api.dingtalk.com/v1.0/robot/emotion/reply" headers = { "x-acs-dingtalk-access-token": token, "Content-Type": "application/json", } payload = { "robotCode": robot_code, "openMsgId": msg_id, "openConversationId": open_conversation_id, "emotionType": int(emotion_type), "emotionName": emotion_name, } if http_client is not None: resp = await http_client.post(url, json=payload, headers=headers) else: async with httpx.AsyncClient(timeout=httpx.Timeout(15)) as client: resp = await client.post(url, json=payload, headers=headers) if resp.status_code != 200: return DeliveryResult( success=False, error=f"DingDing emotion API HTTP {resp.status_code}: {resp.text[:200]}" ) return DeliveryResult(success=True) except Exception as e: logger.error(f"[DingDing] send_reaction failed: {e}") return DeliveryResult(success=False, error=str(e)) async def remove_reaction( token_manager: DingDingTokenManager, open_conversation_id: str, robot_code: str, msg_id: str, emoji: str, *, http_client: httpx.AsyncClient | None = None, ) -> DeliveryResult: emotion_info = DINGDING_EMOJI_MAP.get(emoji) if not emotion_info: emotion_info = resolve_emoji(emoji) if not emotion_info: return DeliveryResult(success=False, error=f"Unsupported emoji: {emoji}") emotion_type, emotion_name = emotion_info try: token = await token_manager.get_token() url = "https://api.dingtalk.com/v1.0/robot/emotion/remove" headers = { "x-acs-dingtalk-access-token": token, "Content-Type": "application/json", } payload = { "robotCode": robot_code, "openMsgId": msg_id, "openConversationId": open_conversation_id, "emotionType": int(emotion_type), } if http_client is not None: resp = await http_client.post(url, json=payload, headers=headers) else: async with httpx.AsyncClient(timeout=httpx.Timeout(15)) as client: resp = await client.post(url, json=payload, headers=headers) if resp.status_code != 200: return DeliveryResult( success=False, error=f"DingDing emotion remove API HTTP {resp.status_code}: {resp.text[:200]}" ) return DeliveryResult(success=True) except Exception as e: logger.error(f"[DingDing] remove_reaction failed: {e}") return DeliveryResult(success=False, error=str(e)) async def list_reactions( token_manager: DingDingTokenManager, open_conversation_id: str, robot_code: str, msg_id: str, *, http_client: httpx.AsyncClient | None = None, ) -> dict[str, Any]: try: token = await token_manager.get_token() url = "https://api.dingtalk.com/v1.0/robot/emotion/query" headers = { "x-acs-dingtalk-access-token": token, "Content-Type": "application/json", } payload = { "robotCode": robot_code, "openMsgId": msg_id, "openConversationId": open_conversation_id, } if http_client is not None: resp = await http_client.post(url, json=payload, headers=headers) else: async with httpx.AsyncClient(timeout=httpx.Timeout(15)) as client: resp = await client.post(url, json=payload, headers=headers) if resp.status_code != 200: return {"reactions": [], "total": 0, "error": f"HTTP {resp.status_code}: {resp.text[:200]}"} data = resp.json() items = [] for item in data.get("emotionList", data.get("items", [])): emotion_type = str(item.get("emotionType", "")) items.append({ "emotion_type": emotion_type, "emoji": EMOTION_TYPE_TO_EMOJI.get(emotion_type, ""), "emotion_name": item.get("emotionName", ""), "count": item.get("count", 0), "user_id": item.get("userId", ""), }) return { "reactions": items, "total": data.get("totalCount", len(items)), } except Exception as e: logger.error(f"[DingDing] list_reactions failed: {e}") return {"reactions": [], "total": 0, "error": str(e)} async def clear_all_bot_reactions( token_manager: DingDingTokenManager, open_conversation_id: str, robot_code: str, msg_id: str, *, http_client: httpx.AsyncClient | None = None, ) -> DeliveryResult: result = await list_reactions( token_manager, open_conversation_id, robot_code, msg_id, http_client=http_client ) items = result.get("reactions", []) removed = 0 errors = 0 for item in items: emotion_type = item.get("emotion_type", "") emoji = EMOTION_TYPE_TO_EMOJI.get(emotion_type, "") if not emoji: continue rm_result = await remove_reaction( token_manager, open_conversation_id, robot_code, msg_id, emoji, http_client=http_client ) if rm_result.success: removed += 1 else: errors += 1 logger.debug(f"[DingDing] Failed to remove reaction {emoji}: {rm_result.error}") if errors > 0: logger.warning(f"[DingDing] clear_all_bot_reactions: removed={removed}, errors={errors}") return DeliveryResult(success=removed > 0, error=f"Removed {removed}/{removed + errors}") return DeliveryResult(success=True)