import json import logging from yuxi.channel.extensions.jd.client import JOSClient from yuxi.channel.extensions.jd.types import JD_TEXT_MAX_LENGTH logger = logging.getLogger(__name__) class JDOutbound: def __init__(self, gateway=None): self._gateway = gateway self._client: JOSClient | None = None async def send_text( self, target_id: str, content: str, *, reply_to_id: str | None = None, thread_id: str | None = None, account_id: str | None = None, chat_id: str | None = None, ) -> None: if not content: return if len(content) > JD_TEXT_MAX_LENGTH: content = content[: JD_TEXT_MAX_LENGTH - 3] + "..." await self._send_msg(target_id, content, msg_type=1, chat_id=chat_id) async def send_media( self, target_id: str, media_url: str, media_type: str, reply_to_id: str | None = None, thread_id: str | None = None, chat_id: str | None = None, ) -> None: if media_type != "image": logger.warning("JD only supports image, got media_type=%s", media_type) return img_content = json.dumps({"url": media_url}, ensure_ascii=False) await self._send_msg(target_id, img_content, msg_type=2, chat_id=chat_id) async def send_image(self, to_user: str, media_url: str, *, chat_id: str | None = None) -> bool: img_content = json.dumps({"url": media_url}, ensure_ascii=False) return await self._send_msg(to_user, img_content, msg_type=2, chat_id=chat_id) async def send_link( self, target_id: str, url: str, text: str, *, chat_id: str | None = None, ) -> bool: link_content = json.dumps({"url": url, "text": text}, ensure_ascii=False) return await self._send_msg(target_id, link_content, msg_type=3, chat_id=chat_id) async def send_product_card( self, target_id: str, url: str, pid: str, *, chat_id: str | None = None, ) -> bool: card_content = json.dumps({"url": url, "pid": pid}, ensure_ascii=False) return await self._send_msg(target_id, card_content, msg_type=4, chat_id=chat_id) async def _send_msg(self, to_user: str, content: str, msg_type: int = 1, *, chat_id: str | None = None) -> bool: gateway = self._gateway if gateway is None or not (token := gateway.get_token()) or not (account := gateway.account): logger.error("No JD gateway/token/account available") return False if self._client is None: self._client = JOSClient( app_key=account.app_key, app_secret=account.app_secret, shop_id=account.shop_id, ) biz_params = { "chat_id": chat_id or "", "from_id": account.shop_id, "to_id": to_user, "msg_type": msg_type, "content": content, } try: data = await self._client.call("jingdong.im.sendMsg", biz_params, token) result = data.get("jingdong_im_sendMsg_responce", {}).get("result", {}) logger.debug("JD message sent: msg_id=%s to=%s", result.get("msg_id", ""), to_user) return True except Exception: logger.exception("JD send_msg failed for user %s", to_user) return False async def recall_msg(self, msg_id: str, chat_id: str | None = None) -> bool: gateway = self._gateway if gateway is None or not (token := gateway.get_token()) or not (account := gateway.account): logger.error("No JD gateway/token/account available for recall") return False if self._client is None: self._client = JOSClient( app_key=account.app_key, app_secret=account.app_secret, shop_id=account.shop_id, ) biz_params = {"msg_id": msg_id, "chat_id": chat_id or ""} try: await self._client.call("jingdong.im.recallMsg", biz_params, token) logger.debug("JD message recalled: msg_id=%s", msg_id) return True except Exception: logger.exception("JD recall msg failed: msg_id=%s", msg_id) return False def chunker(self, text: str, max_chars: int = JD_TEXT_MAX_LENGTH) -> list[str]: if len(text) <= max_chars: return [text] chunks = [] while text: if len(text) <= max_chars: chunks.append(text) break split_at = text.rfind("\n", 0, max_chars) if split_at == -1: split_at = max_chars chunks.append(text[:split_at]) text = text[split_at:].lstrip() return chunks async def close(self): if self._client: await self._client.close() self._client = None