from __future__ import annotations import json import logging import os from typing import Any from yuxi.channels.models import DeliveryResult from .cards import build_feishu_card, build_feishu_post_content, build_feishu_text_content logger = logging.getLogger(__name__) try: import lark_oapi HAS_LARK_SDK = True except ImportError: HAS_LARK_SDK = False lark_oapi = None # type: ignore IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp"} REPLY_DEGRADE_CODES = {230011, 231003} def _resolve_receive_id_type(chat_type: str) -> str: if chat_type in ("direct", "private", "p2p"): return "open_id" if chat_type in ("group", "thread"): return "chat_id" return "open_id" async def reply_message( client: Any, message_id: str, content: str, *, buttons: list[dict[str, str]] | None = None, thread_id: str | None = None, use_post_format: bool = False, silent: bool = False, ) -> DeliveryResult: if not HAS_LARK_SDK: return DeliveryResult(success=False, error="lark-oapi SDK not available") try: if buttons: card = build_feishu_card(content, buttons=buttons) msg_content = json.dumps(card, ensure_ascii=False) msg_type = "interactive" elif use_post_format: msg_content = json.dumps(build_feishu_post_content(content), ensure_ascii=False) msg_type = "post" else: msg_content = json.dumps(build_feishu_text_content(content), ensure_ascii=False) msg_type = "text" request_body = ( lark_oapi.api.im.v1.ReplyMessageRequestBody.builder().content(msg_content).msg_type(msg_type).build() ) request = ( lark_oapi.api.im.v1.ReplyMessageRequest.builder().message_id(message_id).request_body(request_body).build() ) if thread_id: request.root_id = thread_id resp = await client.im.v1.message.reply(request) if resp.success(): return DeliveryResult(success=True, message_id=resp.data.get("message_id", "")) error_code = getattr(resp, "code", 0) if error_code in REPLY_DEGRADE_CODES: logger.info(f"[Feishu] Reply target revoked (code={error_code}), falling back to direct send") return await send_text( client, message_id, content, chat_type="private", thread_id=thread_id, ) return DeliveryResult(success=False, error=f"Feishu API error: {resp.msg}") except AttributeError as e: return DeliveryResult(success=False, error=f"SDK error: {e}") except Exception as e: logger.error(f"Feishu reply_message failed: {e}") return DeliveryResult(success=False, error=str(e)) async def send_text( client: Any, receive_id: str, content: str, *, chat_type: str = "private", thread_id: str | None = None, use_post_format: bool = False, silent: bool = False, ) -> DeliveryResult: if not HAS_LARK_SDK: return DeliveryResult(success=False, error="lark-oapi SDK not available") try: if use_post_format: content_json = build_feishu_post_content(content) msg_type = "post" else: content_json = build_feishu_text_content(content) msg_type = "text" msg_content = json.dumps(content_json, ensure_ascii=False) request = _make_create_msg_request(receive_id, chat_type, msg_type, msg_content) if thread_id: request.root_id = thread_id resp = await client.im.message.create(request) if resp.success(): return DeliveryResult(success=True, message_id=resp.data.get("message_id", "")) code = getattr(resp, "code", -1) msg = getattr(resp, "msg", str(resp)) if code == 401: return DeliveryResult(success=False, error=f"Auth failed: {msg}", error_code="auth_expired") return DeliveryResult(success=False, error=f"Feishu API error: {msg}") except AttributeError as e: return DeliveryResult(success=False, error=f"SDK error: {e}") except Exception as e: logger.error(f"Feishu send_text failed: {e}") return DeliveryResult(success=False, error=str(e)) async def read_message(client: Any, message_id: str) -> dict: if not HAS_LARK_SDK: return {} try: request = lark_oapi.api.im.v1.GetMessageRequest.builder().message_id(message_id).build() resp = await client.im.v1.message.get(request) if resp.success(): items = resp.data.get("items", []) if items: item = items[0] return { "message_id": item.get("message_id", ""), "msg_type": item.get("msg_type", ""), "chat_id": item.get("chat_id", ""), "chat_type": item.get("chat_type", ""), "content": item.get("body", {}).get("content", ""), "root_id": item.get("root_id", ""), "parent_id": item.get("parent_id", ""), "sender_id": item.get("sender", {}).get("id", ""), "create_time": item.get("create_time", ""), "mentions": item.get("mentions", []), } return {} except Exception as e: logger.error(f"[Feishu] read_message failed: {e}") return {} async def send_card( client: Any, receive_id: str, content: str, *, chat_type: str = "private", title: str = "AI 助手", buttons: list[dict[str, str]] | None = None, streaming: bool = False, thread_id: str | None = None, raw_card: bool = False, template: str | None = None, tone: str | None = None, url_unfurl: list[str] | None = None, silent: bool = False, ) -> DeliveryResult: if not HAS_LARK_SDK: return DeliveryResult(success=False, error="lark-oapi SDK not available") try: if raw_card: msg_content = content else: card = build_feishu_card( content, title=title, buttons=buttons, streaming=streaming, template=template, tone=tone, url_unfurl=url_unfurl, ) msg_content = json.dumps(card, ensure_ascii=False) request = _make_create_msg_request(receive_id, chat_type, "interactive", msg_content) if thread_id: request.root_id = thread_id resp = await client.im.message.create(request) if resp.success(): return DeliveryResult(success=True, message_id=resp.data.get("message_id", "")) code = getattr(resp, "code", -1) msg = getattr(resp, "msg", str(resp)) if code == 401: return DeliveryResult(success=False, error=f"Auth failed: {msg}", error_code="auth_expired") return DeliveryResult(success=False, error=f"Feishu API error: {msg}") except AttributeError as e: return DeliveryResult(success=False, error=f"SDK error: {e}") except Exception as e: logger.error(f"Feishu send_card failed: {e}") return DeliveryResult(success=False, error=str(e)) async def send_reaction( client: Any, message_id: str, reaction: str, *, chat_type: str = "private", ) -> DeliveryResult: if not HAS_LARK_SDK: return DeliveryResult(success=False, error="lark-oapi SDK not available") try: req = ( lark_oapi.api.im.v1.CreateMessageReactionRequest.builder() .message_id(message_id) .request_body( lark_oapi.api.im.v1.CreateMessageReactionRequestBody.builder() .reaction_type(lark_oapi.api.im.v1.CreateMessageReactionRequestBodyReactionType(emoji_type=reaction)) .build() ) .build() ) resp = await client.im.message.reaction.create(req) if resp.success(): return DeliveryResult(success=True) return DeliveryResult(success=False, error=resp.msg) except AttributeError: return DeliveryResult( success=False, error="SDK version incompatible: CreateMessageReactionRequestReactionType not found", ) except Exception as e: logger.error(f"Feishu send_reaction failed: {e}") return DeliveryResult(success=False, error=str(e)) def _make_create_msg_request( receive_id: str, chat_type: str, msg_type: str, content_json: str, *, silent: bool = False ) -> Any: receive_id_type = _resolve_receive_id_type(chat_type) body_builder = ( lark_oapi.api.im.v1.CreateMessageRequestBody.builder() .receive_id(receive_id) .msg_type(msg_type) .content(content_json) ) req_builder = ( lark_oapi.api.im.v1.CreateMessageRequest.builder() .receive_id_type(receive_id_type) .request_body(body_builder.build()) ) return req_builder.build() def is_local_image_path(text: str) -> str | None: cleaned = text.strip() if os.path.isabs(cleaned) and os.path.splitext(cleaned)[1].lower() in IMAGE_EXTENSIONS: if os.path.isfile(cleaned): return cleaned return None async def update_card_message(client: Any, message_id: str, content: str) -> DeliveryResult: if not HAS_LARK_SDK: return DeliveryResult(success=False, error="lark-oapi SDK not available") try: content_json = json.dumps( { "config": {"wide_screen_mode": True}, "header": {"title": {"tag": "plain_text", "content": content[:100]}}, "elements": [{"tag": "markdown", "content": content}], }, ensure_ascii=False, ) request = ( lark_oapi.api.im.v1.PatchMessageRequest.builder() .message_id(message_id) .request_body(lark_oapi.api.im.v1.PatchMessageRequestBody.builder().content(content_json).build()) .build() ) resp = await client.im.v1.message.patch(request) if resp.success(): return DeliveryResult(success=True, message_id=message_id) code = getattr(resp, "code", -1) msg = getattr(resp, "msg", str(resp)) return DeliveryResult(success=False, error=f"Feishu API error: {msg}") except Exception as e: logger.error(f"Feishu update_card_message failed: {e}") return DeliveryResult(success=False, error=str(e))