from __future__ import annotations import logging from dataclasses import dataclass import httpx from yuxi.channel.extensions.line.types import LineBotProfile logger = logging.getLogger(__name__) LINE_API_BASE = "https://api.line.me" LINE_API_DATA_BASE = "https://api-data.line.me" @dataclass class LineBotClient: channel_access_token: str timeout: float = 10.0 async def get_bot_info(self) -> LineBotProfile | None: try: async with httpx.AsyncClient(timeout=httpx.Timeout(self.timeout)) as client: resp = await client.get( f"{LINE_API_BASE}/v2/bot/info", headers={"Authorization": f"Bearer {self.channel_access_token}"}, ) if resp.status_code == 200: return LineBotProfile.from_api_response(resp.json()) logger.warning("LINE get_bot_info failed: status=%s body=%s", resp.status_code, resp.text[:200]) except Exception: logger.exception("LINE get_bot_info error") return None async def get_message_content(self, message_id: str) -> bytes | None: try: async with httpx.AsyncClient(timeout=httpx.Timeout(self.timeout)) as client: resp = await client.get( f"{LINE_API_DATA_BASE}/v2/bot/message/{message_id}/content", headers={"Authorization": f"Bearer {self.channel_access_token}"}, ) if resp.status_code == 200: return resp.content logger.warning("LINE get_message_content failed: status=%s", resp.status_code) except Exception: logger.exception("LINE get_message_content error") return None async def get_message_content_preview(self, message_id: str) -> bytes | None: try: async with httpx.AsyncClient(timeout=httpx.Timeout(self.timeout)) as client: resp = await client.get( f"{LINE_API_DATA_BASE}/v2/bot/message/{message_id}/content/preview", headers={"Authorization": f"Bearer {self.channel_access_token}"}, ) if resp.status_code == 200: return resp.content logger.warning("LINE get_message_content_preview failed: status=%s", resp.status_code) except Exception: logger.exception("LINE get_message_content_preview error") return None async def verify_content_transcoding(self, message_id: str) -> dict | None: try: async with httpx.AsyncClient(timeout=httpx.Timeout(self.timeout)) as client: resp = await client.get( f"{LINE_API_DATA_BASE}/v2/bot/message/{message_id}/content/transcoding", headers={"Authorization": f"Bearer {self.channel_access_token}"}, ) if resp.status_code == 200: return resp.json() logger.warning("LINE verify_content_transcoding failed: status=%s", resp.status_code) except Exception: logger.exception("LINE verify_content_transcoding error") return None async def show_loading_animation(self, user_id: str, loading_seconds: int = 20) -> bool: try: async with httpx.AsyncClient(timeout=httpx.Timeout(self.timeout)) as client: resp = await client.post( f"{LINE_API_BASE}/v2/bot/chat/loading/start", headers={ "Authorization": f"Bearer {self.channel_access_token}", "Content-Type": "application/json", }, json={"chatId": user_id, "loadingSeconds": loading_seconds}, ) return resp.status_code == 202 except Exception: logger.exception("LINE show_loading_animation error") return False async def reply_message(self, reply_token: str, messages: list[dict], *, quote_token: str | None = None) -> bool: try: body: dict = {"replyToken": reply_token, "messages": messages} if quote_token: body["quoteToken"] = quote_token async with httpx.AsyncClient(timeout=httpx.Timeout(self.timeout)) as client: resp = await client.post( f"{LINE_API_BASE}/v2/bot/message/reply", headers={ "Authorization": f"Bearer {self.channel_access_token}", "Content-Type": "application/json", }, json=body, ) if resp.status_code == 200: return True body_data = resp.json() logger.warning( "LINE reply_message failed: status=%s message=%s", resp.status_code, body_data.get("message", "") if isinstance(body_data, dict) else "", ) except Exception: logger.exception("LINE reply_message error") return False async def push_message(self, to: str, messages: list[dict], *, sender: dict | None = None) -> bool: try: body: dict = {"to": to, "messages": messages} if sender: body["sender"] = sender async with httpx.AsyncClient(timeout=httpx.Timeout(self.timeout)) as client: resp = await client.post( f"{LINE_API_BASE}/v2/bot/message/push", headers={ "Authorization": f"Bearer {self.channel_access_token}", "Content-Type": "application/json", }, json=body, ) if resp.status_code == 200: return True logger.warning( "LINE push_message failed: status=%s body=%s", resp.status_code, resp.text[:200] ) except Exception: logger.exception("LINE push_message error") return False async def multicast_message( self, to: list[str], messages: list[dict], *, notification_disabled: bool = False, sender: dict | None = None, ) -> bool: try: body: dict = { "to": to[:500], "messages": messages, "notificationDisabled": notification_disabled, } if sender: body["sender"] = sender async with httpx.AsyncClient(timeout=httpx.Timeout(self.timeout)) as client: resp = await client.post( f"{LINE_API_BASE}/v2/bot/message/multicast", headers={ "Authorization": f"Bearer {self.channel_access_token}", "Content-Type": "application/json", }, json=body, ) if resp.status_code == 200: return True logger.warning("LINE multicast_message failed: status=%s body=%s", resp.status_code, resp.text[:200]) except Exception: logger.exception("LINE multicast_message error") return False async def broadcast_message(self, messages: list[dict], *, notification_disabled: bool = False) -> bool: try: async with httpx.AsyncClient(timeout=httpx.Timeout(self.timeout)) as client: resp = await client.post( f"{LINE_API_BASE}/v2/bot/message/broadcast", headers={ "Authorization": f"Bearer {self.channel_access_token}", "Content-Type": "application/json", }, json={"messages": messages, "notificationDisabled": notification_disabled}, ) if resp.status_code == 200: return True logger.warning("LINE broadcast_message failed: status=%s body=%s", resp.status_code, resp.text[:200]) except Exception: logger.exception("LINE broadcast_message error") return False async def narrowcast_message( self, messages: list[dict], *, recipient: dict | None = None, filter: dict | None = None, limit: dict | None = None, notification_disabled: bool = False, ) -> bool: try: body: dict = {"messages": messages, "notificationDisabled": notification_disabled} if recipient: body["recipient"] = recipient if filter: body["filter"] = filter if limit: body["limit"] = limit async with httpx.AsyncClient(timeout=httpx.Timeout(self.timeout)) as client: resp = await client.post( f"{LINE_API_BASE}/v2/bot/message/narrowcast", headers={ "Authorization": f"Bearer {self.channel_access_token}", "Content-Type": "application/json", }, json=body, ) if resp.status_code in (200, 202): return True logger.warning("LINE narrowcast_message failed: status=%s body=%s", resp.status_code, resp.text[:200]) except Exception: logger.exception("LINE narrowcast_message error") return False async def get_user_profile(self, user_id: str) -> dict | None: try: async with httpx.AsyncClient(timeout=httpx.Timeout(self.timeout)) as client: resp = await client.get( f"{LINE_API_BASE}/v2/bot/profile/{user_id}", headers={"Authorization": f"Bearer {self.channel_access_token}"}, ) if resp.status_code == 200: return resp.json() logger.warning("LINE get_user_profile failed: status=%s", resp.status_code) except Exception: logger.exception("LINE get_user_profile error") return None async def get_group_member_profile(self, group_id: str, user_id: str) -> dict | None: try: async with httpx.AsyncClient(timeout=httpx.Timeout(self.timeout)) as client: resp = await client.get( f"{LINE_API_BASE}/v2/bot/group/{group_id}/member/{user_id}", headers={"Authorization": f"Bearer {self.channel_access_token}"}, ) if resp.status_code == 200: return resp.json() logger.warning("LINE get_group_member_profile failed: status=%s", resp.status_code) except Exception: logger.exception("LINE get_group_member_profile error") return None async def get_room_member_profile(self, room_id: str, user_id: str) -> dict | None: try: async with httpx.AsyncClient(timeout=httpx.Timeout(self.timeout)) as client: resp = await client.get( f"{LINE_API_BASE}/v2/bot/room/{room_id}/member/{user_id}", headers={"Authorization": f"Bearer {self.channel_access_token}"}, ) if resp.status_code == 200: return resp.json() logger.warning("LINE get_room_member_profile failed: status=%s", resp.status_code) except Exception: logger.exception("LINE get_room_member_profile error") return None async def get_group_member_ids(self, group_id: str, continuation_token: str | None = None) -> dict | None: try: params = {} if continuation_token: params["start"] = continuation_token async with httpx.AsyncClient(timeout=httpx.Timeout(self.timeout)) as client: resp = await client.get( f"{LINE_API_BASE}/v2/bot/group/{group_id}/members/ids", headers={"Authorization": f"Bearer {self.channel_access_token}"}, params=params, ) if resp.status_code == 200: return resp.json() logger.warning("LINE get_group_member_ids failed: status=%s", resp.status_code) except Exception: logger.exception("LINE get_group_member_ids error") return None async def get_room_member_ids(self, room_id: str, continuation_token: str | None = None) -> dict | None: try: params = {} if continuation_token: params["start"] = continuation_token async with httpx.AsyncClient(timeout=httpx.Timeout(self.timeout)) as client: resp = await client.get( f"{LINE_API_BASE}/v2/bot/room/{room_id}/members/ids", headers={"Authorization": f"Bearer {self.channel_access_token}"}, params=params, ) if resp.status_code == 200: return resp.json() logger.warning("LINE get_room_member_ids failed: status=%s", resp.status_code) except Exception: logger.exception("LINE get_room_member_ids error") return None async def get_followers_count(self) -> dict | None: try: async with httpx.AsyncClient(timeout=httpx.Timeout(self.timeout)) as client: resp = await client.get( f"{LINE_API_BASE}/v2/bot/followers/count", headers={"Authorization": f"Bearer {self.channel_access_token}"}, ) if resp.status_code == 200: return resp.json() logger.warning("LINE get_followers_count failed: status=%s", resp.status_code) except Exception: logger.exception("LINE get_followers_count error") return None async def get_friend_demographics(self) -> dict | None: try: async with httpx.AsyncClient(timeout=httpx.Timeout(self.timeout)) as client: resp = await client.get( f"{LINE_API_BASE}/v2/bot/demographic", headers={"Authorization": f"Bearer {self.channel_access_token}"}, ) if resp.status_code == 200: return resp.json() logger.warning("LINE get_friend_demographics failed: status=%s", resp.status_code) except Exception: logger.exception("LINE get_friend_demographics error") return None async def mark_as_read(self, user_id: str) -> bool: try: async with httpx.AsyncClient(timeout=httpx.Timeout(self.timeout)) as client: resp = await client.post( f"{LINE_API_BASE}/v2/bot/chat/members/markAsRead", headers={ "Authorization": f"Bearer {self.channel_access_token}", "Content-Type": "application/json", }, json={"userId": user_id}, ) return resp.status_code == 200 except Exception: logger.exception("LINE mark_as_read error") return False async def get_message_quota(self) -> dict | None: try: async with httpx.AsyncClient(timeout=httpx.Timeout(self.timeout)) as client: resp = await client.get( f"{LINE_API_BASE}/v2/bot/message/quota", headers={"Authorization": f"Bearer {self.channel_access_token}"}, ) if resp.status_code == 200: return resp.json() logger.warning("LINE get_message_quota failed: status=%s", resp.status_code) except Exception: logger.exception("LINE get_message_quota error") return None async def get_message_consumption(self) -> dict | None: try: async with httpx.AsyncClient(timeout=httpx.Timeout(self.timeout)) as client: resp = await client.get( f"{LINE_API_BASE}/v2/bot/message/quota/consumption", headers={"Authorization": f"Bearer {self.channel_access_token}"}, ) if resp.status_code == 200: return resp.json() logger.warning("LINE get_message_consumption failed: status=%s", resp.status_code) except Exception: logger.exception("LINE get_message_consumption error") return None async def get_message_delivery_count(self, delivery_type: str, date: str) -> dict | None: try: async with httpx.AsyncClient(timeout=httpx.Timeout(self.timeout)) as client: resp = await client.get( f"{LINE_API_BASE}/v2/bot/message/delivery/{delivery_type}", headers={"Authorization": f"Bearer {self.channel_access_token}"}, params={"date": date}, ) if resp.status_code == 200: return resp.json() logger.warning( "LINE get_message_delivery_count(%s) failed: status=%s", delivery_type, resp.status_code, ) except Exception: logger.exception("LINE get_message_delivery_count error") return None async def get_user_interaction_stats(self, date: str) -> dict | None: try: async with httpx.AsyncClient(timeout=httpx.Timeout(self.timeout)) as client: resp = await client.get( f"{LINE_API_BASE}/v2/bot/analytics/event", headers={"Authorization": f"Bearer {self.channel_access_token}"}, params={"date": date}, ) if resp.status_code == 200: return resp.json() logger.warning("LINE get_user_interaction_stats failed: status=%s", resp.status_code) except Exception: logger.exception("LINE get_user_interaction_stats error") return None async def create_audience_group(self, description: str, audiences: list[dict]) -> dict | None: try: async with httpx.AsyncClient(timeout=httpx.Timeout(self.timeout)) as client: resp = await client.post( f"{LINE_API_BASE}/v2/bot/audienceGroup/upload", headers={ "Authorization": f"Bearer {self.channel_access_token}", "Content-Type": "application/json", }, json={"description": description, "audiences": audiences}, ) if resp.status_code in (200, 202): return resp.json() logger.warning("LINE create_audience_group failed: status=%s", resp.status_code) except Exception: logger.exception("LINE create_audience_group error") return None async def create_audience_group_by_file(self, description: str, file_url: str) -> dict | None: try: async with httpx.AsyncClient(timeout=httpx.Timeout(self.timeout)) as client: resp = await client.post( f"{LINE_API_BASE}/v2/bot/audienceGroup/upload/byFile", headers={ "Authorization": f"Bearer {self.channel_access_token}", "Content-Type": "application/json", }, json={"description": description, "fileUrl": file_url}, ) if resp.status_code in (200, 202): return resp.json() logger.warning("LINE create_audience_group_by_file failed: status=%s", resp.status_code) except Exception: logger.exception("LINE create_audience_group_by_file error") return None async def add_users_to_audience(self, audience_group_id: str, audiences: list[dict]) -> bool: try: async with httpx.AsyncClient(timeout=httpx.Timeout(self.timeout)) as client: resp = await client.put( f"{LINE_API_BASE}/v2/bot/audienceGroup/upload", headers={ "Authorization": f"Bearer {self.channel_access_token}", "Content-Type": "application/json", }, json={"audienceGroupId": audience_group_id, "audiences": audiences}, ) return resp.status_code in (200, 202) except Exception: logger.exception("LINE add_users_to_audience error") return False async def get_audience_groups(self, page: int = 1, size: int = 40) -> dict | None: try: async with httpx.AsyncClient(timeout=httpx.Timeout(self.timeout)) as client: resp = await client.get( f"{LINE_API_BASE}/v2/bot/audienceGroup/list", headers={"Authorization": f"Bearer {self.channel_access_token}"}, params={"page": page, "size": size}, ) if resp.status_code == 200: return resp.json() logger.warning("LINE get_audience_groups failed: status=%s", resp.status_code) except Exception: logger.exception("LINE get_audience_groups error") return None async def delete_audience_group(self, audience_group_id: str) -> bool: try: async with httpx.AsyncClient(timeout=httpx.Timeout(self.timeout)) as client: resp = await client.delete( f"{LINE_API_BASE}/v2/bot/audienceGroup/{audience_group_id}", headers={"Authorization": f"Bearer {self.channel_access_token}"}, ) return resp.status_code in (200, 202) except Exception: logger.exception("LINE delete_audience_group error") return False async def get_statistics_per_unit( self, custom_aggregation_unit: str, from_date: str, to_date: str, ) -> dict | None: try: async with httpx.AsyncClient(timeout=httpx.Timeout(self.timeout)) as client: resp = await client.get( f"{LINE_API_BASE}/v2/bot/statistics/unit", headers={"Authorization": f"Bearer {self.channel_access_token}"}, params={ "customAggregationUnit": custom_aggregation_unit, "from": from_date, "to": to_date, }, ) if resp.status_code == 200: return resp.json() logger.warning("LINE get_statistics_per_unit failed: status=%s", resp.status_code) except Exception: logger.exception("LINE get_statistics_per_unit error") return None async def get_narrowcast_progress(self, request_id: str) -> dict | None: try: async with httpx.AsyncClient(timeout=httpx.Timeout(self.timeout)) as client: resp = await client.get( f"{LINE_API_BASE}/v2/bot/message/progress/narrowcast", headers={"Authorization": f"Bearer {self.channel_access_token}"}, params={"requestId": request_id}, ) if resp.status_code == 200: return resp.json() logger.warning("LINE get_narrowcast_progress failed: status=%s", resp.status_code) except Exception: logger.exception("LINE get_narrowcast_progress error") return None