from __future__ import annotations import logging from yuxi.channel.extensions.line.bot import LineBotClient from yuxi.channel.extensions.line.config import LineConfigAdapter from yuxi.channel.extensions.line.format import chunk_text from yuxi.channel.protocols import OutboundDeliveryMode logger = logging.getLogger(__name__) class LineOutboundAdapter: delivery_mode = OutboundDeliveryMode.DIRECT chunker_mode = "newline" text_chunk_limit: int = 5000 poll_max_options: int | None = None supports_poll_duration_seconds = False supports_anonymous_polls = False extract_markdown_images = False presentation_capabilities = None delivery_capabilities = None def __init__(self): self._config = LineConfigAdapter() 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, ) -> None: account = await self._resolve_account(account_id) if not account: logger.error("LINE send_text: account not resolved") return token = account.get("channel_access_token", "") if not token: return client = LineBotClient(channel_access_token=token) chunks = chunk_text(content, account.get("text_chunk_limit", 5000)) messages = [{"type": "text", "text": chunk} for chunk in chunks] if reply_to_id: await client.reply_message(reply_to_id, messages[:5]) if len(messages) > 5: for batch in _batch_list(messages[5:], 5): await client.push_message(target_id, batch) else: for batch in _batch_list(messages, 5): await client.push_message(target_id, batch) 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, account_id: str | None = None, duration_ms: int | None = None, file_name: str | None = None, ) -> None: account = await self._resolve_account(account_id) if not account: return token = account.get("channel_access_token", "") if not token: return client = LineBotClient(channel_access_token=token) message = self._build_media_message(media_url, media_type, duration_ms, file_name) if not message: return if reply_to_id: await client.reply_message(reply_to_id, [message]) else: await client.push_message(target_id, [message]) async def send_flex( self, target_id: str, alt_text: str, contents: dict, *, reply_to_id: str | None = None, account_id: str | None = None, ) -> bool: account = await self._resolve_account(account_id) if not account: return False token = account.get("channel_access_token", "") if not token: return False client = LineBotClient(channel_access_token=token) message = { "type": "flex", "altText": alt_text[:400], "contents": contents, } if reply_to_id: return await client.reply_message(reply_to_id, [message]) return await client.push_message(target_id, [message]) async def send_template( self, target_id: str, alt_text: str, template: dict, *, reply_to_id: str | None = None, account_id: str | None = None, ) -> bool: account = await self._resolve_account(account_id) if not account: return False token = account.get("channel_access_token", "") if not token: return False client = LineBotClient(channel_access_token=token) message = { "type": "template", "altText": alt_text[:400], "template": template, } if reply_to_id: return await client.reply_message(reply_to_id, [message]) return await client.push_message(target_id, [message]) async def send_location( self, target_id: str, title: str, address: str, latitude: float, longitude: float, *, reply_to_id: str | None = None, account_id: str | None = None, ) -> bool: account = await self._resolve_account(account_id) if not account: return False token = account.get("channel_access_token", "") if not token: return False client = LineBotClient(channel_access_token=token) message = { "type": "location", "title": title, "address": address, "latitude": latitude, "longitude": longitude, } if reply_to_id: return await client.reply_message(reply_to_id, [message]) return await client.push_message(target_id, [message]) async def send_quick_replies( self, target_id: str, text: str, items: list[str], *, reply_to_id: str | None = None, account_id: str | None = None, ) -> bool: account = await self._resolve_account(account_id) if not account: return False token = account.get("channel_access_token", "") if not token: return False client = LineBotClient(channel_access_token=token) quick_reply_items = [ {"type": "action", "action": {"type": "message", "label": item[:20], "text": item}} for item in items[:13] ] message = { "type": "text", "text": text, "quickReply": {"items": quick_reply_items}, } if reply_to_id: return await client.reply_message(reply_to_id, [message]) return await client.push_message(target_id, [message]) def chunker(self, text: str, limit: int, ctx: object | None = None) -> list[str]: return chunk_text(text, limit) def sanitize_text(self, text: str, payload: object) -> str: return text def should_skip_plain_text_sanitization(self, payload: object) -> bool: return True def resolve_target( self, to: str | None = None, *, config: dict | None = None, allow_from: list[str] | None = None, account_id: str | None = None, mode: str | None = None, ) -> tuple[bool, str]: if not to: return False, "target required" return True, to async def _resolve_account(self, account_id: str | None) -> dict | None: if not account_id: account_id = "default" try: return await self._config.resolve_account(account_id) except Exception: logger.exception("LINE resolve_account error") return None @staticmethod def _build_media_message( media_url: str, media_type: str, duration_ms: int | None = None, file_name: str | None = None, ) -> dict | None: match media_type: case "image": return { "type": "image", "originalContentUrl": media_url, "previewImageUrl": media_url, } case "video": return { "type": "video", "originalContentUrl": media_url, "previewImageUrl": media_url, } case "audio": return { "type": "audio", "originalContentUrl": media_url, "duration": duration_ms or 60000, } case "file": return { "type": "file", "fileName": file_name or (media_url.rsplit("/", 1)[-1] if "/" in media_url else "file"), "originalContentUrl": media_url, } case _: return None async def send_sticker( self, target_id: str, package_id: str, sticker_id: str, *, reply_to_id: str | None = None, account_id: str | None = None, ) -> bool: account = await self._resolve_account(account_id) if not account: return False token = account.get("channel_access_token", "") if not token: return False client = LineBotClient(channel_access_token=token) message = { "type": "sticker", "packageId": package_id, "stickerId": sticker_id, } if reply_to_id: return await client.reply_message(reply_to_id, [message]) return await client.push_message(target_id, [message]) async def send_imagemap( self, target_id: str, base_url: str, alt_text: str, base_width: int, base_height: int, actions: list[dict], *, video: dict | None = None, reply_to_id: str | None = None, account_id: str | None = None, ) -> bool: account = await self._resolve_account(account_id) if not account: return False token = account.get("channel_access_token", "") if not token: return False client = LineBotClient(channel_access_token=token) message: dict = { "type": "imagemap", "baseUrl": base_url, "altText": alt_text[:400], "baseSize": {"width": base_width, "height": base_height}, "actions": actions, } if video: message["video"] = video if reply_to_id: return await client.reply_message(reply_to_id, [message]) return await client.push_message(target_id, [message]) async def send_text_v2( self, target_id: str, content: str, *, emojis: list[dict] | None = None, mentions: list[dict] | None = None, quick_reply: dict | None = None, reply_to_id: str | None = None, account_id: str | None = None, ) -> bool: account = await self._resolve_account(account_id) if not account: return False token = account.get("channel_access_token", "") if not token: return False client = LineBotClient(channel_access_token=token) message: dict = {"type": "textV2", "text": content} if emojis: substitution = {} for i, e in enumerate(emojis): substitution[f"${i}$"] = {"productId": e["productId"], "emojiId": e["emojiId"]} message["substitution"] = substitution if mentions: message["mentions"] = mentions if quick_reply: message["quickReply"] = quick_reply if reply_to_id: return await client.reply_message(reply_to_id, [message]) return await client.push_message(target_id, [message]) def _batch_list(items: list, batch_size: int) -> list[list]: return [items[i: i + batch_size] for i in range(0, len(items), batch_size)]