from __future__ import annotations from typing import Any import httpx from yuxi.channels.models import DeliveryResult from yuxi.channels.adapters.wechat.format import truncate_text from yuxi.channels.adapters.wechat.errors import is_token_expired, parse_mp_error from yuxi.channels.adapters.wechat.retry import retry_with_backoff from .client import MPClient async def send_mp_custom_message( client: MPClient, http_client: httpx.AsyncClient, to_user: str, content: str, reply_to_msg_id: str | None = None, reply_to_user: str | None = None, ) -> DeliveryResult: token = await client.get_access_token() display_content = content if reply_to_msg_id and reply_to_user: quoted_prefix = f"「回复 @{reply_to_user}」\n" if len(quoted_prefix + content) <= 2048: display_content = quoted_prefix + content payload = { "touser": to_user, "msgtype": "text", "text": {"content": truncate_text(display_content, 2048)}, } if reply_to_msg_id: payload["_reply_to_msg_id"] = reply_to_msg_id try: result = await _post_with_token_retry(client, http_client, token, payload) return result except httpx.HTTPError as e: return DeliveryResult(success=False, error=str(e)) async def send_mp_template_message( client: MPClient, http_client: httpx.AsyncClient, to_user: str, template_id: str, data: dict[str, Any], url: str | None = None, ) -> DeliveryResult: token = await client.get_access_token() payload: dict[str, Any] = { "touser": to_user, "template_id": template_id, "data": data, } if url: payload["url"] = url try: result = await _post_with_token_retry(client, http_client, token, payload) return result except httpx.HTTPError as e: return DeliveryResult(success=False, error=str(e)) _MP_CUSTOM_SEND_URL = "https://api.weixin.qq.com/cgi-bin/message/custom/send" _MP_TEMPLATE_SEND_URL = "https://api.weixin.qq.com/cgi-bin/message/template/send" async def _post_with_token_retry( client: MPClient, http_client: httpx.AsyncClient, token: str, payload: dict[str, Any], ) -> DeliveryResult: api_url = f"{_MP_CUSTOM_SEND_URL}?access_token={token}" if "template_id" in payload: api_url = f"{_MP_TEMPLATE_SEND_URL}?access_token={token}" async def _post(): return await http_client.post(api_url, json=payload) resp = await retry_with_backoff(_post) data = resp.json() if data.get("errcode") == 0: return DeliveryResult(success=True, message_id=str(data.get("msgid", ""))) if is_token_expired(data.get("errcode", 0)): client.invalidate_token() token = await client.get_access_token() api_url = f"{_MP_CUSTOM_SEND_URL}?access_token={token}" if "template_id" in payload: api_url = f"{_MP_TEMPLATE_SEND_URL}?access_token={token}" async def _retry_post(): return await http_client.post(api_url, json=payload) resp = await retry_with_backoff(_retry_post) data = resp.json() if data.get("errcode") == 0: return DeliveryResult(success=True, message_id=str(data.get("msgid", ""))) _, err_detail = parse_mp_error(data) return DeliveryResult(success=False, error=err_detail) async def send_mp_image( client: MPClient, http_client: httpx.AsyncClient, to_user: str, image_data: bytes, filename: str = "image.png", ) -> DeliveryResult: try: media_id = await client.upload_temp_media(image_data, filename, "image") except Exception as e: return DeliveryResult(success=False, error=f"MP image upload failed: {e}") token = await client.get_access_token() payload = { "touser": to_user, "msgtype": "image", "image": {"media_id": media_id}, } try: return await _post_with_token_retry(client, http_client, token, payload) except httpx.HTTPError as e: return DeliveryResult(success=False, error=str(e)) async def send_mp_voice( client: MPClient, http_client: httpx.AsyncClient, to_user: str, voice_data: bytes, ) -> DeliveryResult: try: media_id = await client.upload_temp_media(voice_data, "voice.amr", "voice") except Exception as e: return DeliveryResult(success=False, error=f"MP voice upload failed: {e}") token = await client.get_access_token() payload = { "touser": to_user, "msgtype": "voice", "voice": {"media_id": media_id}, } try: return await _post_with_token_retry(client, http_client, token, payload) except httpx.HTTPError as e: return DeliveryResult(success=False, error=str(e)) async def send_mp_video( client: MPClient, http_client: httpx.AsyncClient, to_user: str, video_data: bytes, title: str = "", description: str = "", ) -> DeliveryResult: try: media_id = await client.upload_temp_media(video_data, "video.mp4", "video") except Exception as e: return DeliveryResult(success=False, error=f"MP video upload failed: {e}") token = await client.get_access_token() payload = { "touser": to_user, "msgtype": "video", "video": {"media_id": media_id, "title": title, "description": description}, } try: return await _post_with_token_retry(client, http_client, token, payload) except httpx.HTTPError as e: return DeliveryResult(success=False, error=str(e)) def build_mp_news_payload( to_user: str, articles: list[dict[str, Any]], ) -> dict[str, Any]: return { "touser": to_user, "msgtype": "news", "news": {"articles": articles}, } async def send_mp_news( client: MPClient, http_client: httpx.AsyncClient, payload: dict[str, Any], ) -> DeliveryResult: token = await client.get_access_token() try: return await _post_with_token_retry(client, http_client, token, payload) except httpx.HTTPError as e: return DeliveryResult(success=False, error=str(e))