from __future__ import annotations import logging import httpx from yuxi.channel.extensions.googlechat.auth import get_access_token from yuxi.channel.extensions.googlechat.types import ResolvedGoogleChatAccount logger = logging.getLogger(__name__) CHAT_API_BASE = "https://chat.googleapis.com/v1" CHAT_UPLOAD_BASE = "https://chat.googleapis.com/upload/v1" CHAT_MEDIA_BASE = "https://chat.googleapis.com/v1/media" async def _get_http_for_account( account: ResolvedGoogleChatAccount, ) -> tuple[httpx.AsyncClient, str]: token = await get_access_token(account) if not token: raise RuntimeError("Failed to obtain Google Chat access token") client = httpx.AsyncClient( base_url=CHAT_API_BASE, timeout=httpx.Timeout(30.0), headers={"Authorization": f"Bearer {token}"}, ) return client, token async def send_message( account: ResolvedGoogleChatAccount, space_name: str, text: str, *, thread_name: str | None = None, quoted_message_name: str | None = None, private_message_viewer: str | None = None, accessory_widgets: list[dict] | None = None, client_message_id: str | None = None, message_reply_option: str = "REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD", ) -> dict: url = f"{CHAT_API_BASE}/{space_name}/messages" token = await get_access_token(account) if not token: raise RuntimeError("Failed to obtain access token") body: dict = {"text": text} if thread_name: body["messageReplyOption"] = message_reply_option body["thread"] = {"name": thread_name} if quoted_message_name: body["quotedMessageMetadata"] = {"name": quoted_message_name} if private_message_viewer: body["privateMessageViewer"] = {"name": private_message_viewer} if accessory_widgets: body["accessoryWidgets"] = accessory_widgets params: dict = {} if client_message_id: params["messageId"] = client_message_id async with httpx.AsyncClient(timeout=httpx.Timeout(30.0)) as client: resp = await client.post( url, json=body, params=params, headers={ "Authorization": f"Bearer {token}", "Content-Type": "application/json", }, ) resp.raise_for_status() return resp.json() async def send_message_with_attachments( account: ResolvedGoogleChatAccount, space_name: str, text: str, attachments: list[dict], *, thread_name: str | None = None, quoted_message_name: str | None = None, private_message_viewer: str | None = None, client_message_id: str | None = None, message_reply_option: str = "REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD", ) -> dict: url = f"{CHAT_API_BASE}/{space_name}/messages" token = await get_access_token(account) if not token: raise RuntimeError("Failed to obtain access token") body: dict = {"text": text, "attachment": attachments} if thread_name: body["messageReplyOption"] = message_reply_option body["thread"] = {"name": thread_name} if quoted_message_name: body["quotedMessageMetadata"] = {"name": quoted_message_name} if private_message_viewer: body["privateMessageViewer"] = {"name": private_message_viewer} params: dict = {} if client_message_id: params["messageId"] = client_message_id async with httpx.AsyncClient(timeout=httpx.Timeout(30.0)) as client: resp = await client.post( url, json=body, params=params, headers={ "Authorization": f"Bearer {token}", "Content-Type": "application/json", }, ) resp.raise_for_status() return resp.json() async def send_card_message( account: ResolvedGoogleChatAccount, space_name: str, cards_v2: list[dict], *, thread_name: str | None = None, fallback_text: str = "", message_reply_option: str = "REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD", ) -> dict: url = f"{CHAT_API_BASE}/{space_name}/messages" token = await get_access_token(account) if not token: raise RuntimeError("Failed to obtain access token") body: dict = { "cardsV2": cards_v2, "fallbackText": fallback_text, } if thread_name: body["messageReplyOption"] = message_reply_option body["thread"] = {"name": thread_name} async with httpx.AsyncClient(timeout=httpx.Timeout(30.0)) as client: resp = await client.post( url, json=body, headers={ "Authorization": f"Bearer {token}", "Content-Type": "application/json", }, ) resp.raise_for_status() return resp.json() async def update_message( account: ResolvedGoogleChatAccount, message_name: str, text: str, ) -> dict: url = f"{CHAT_API_BASE}/{message_name}?updateMask=text" token = await get_access_token(account) if not token: raise RuntimeError("Failed to obtain access token") body = {"text": text} async with httpx.AsyncClient(timeout=httpx.Timeout(30.0)) as client: resp = await client.patch( url, json=body, headers={ "Authorization": f"Bearer {token}", "Content-Type": "application/json", }, ) resp.raise_for_status() return resp.json() async def delete_message(account: ResolvedGoogleChatAccount, message_name: str) -> None: url = f"{CHAT_API_BASE}/{message_name}" token = await get_access_token(account) if not token: raise RuntimeError("Failed to obtain access token") async with httpx.AsyncClient(timeout=httpx.Timeout(30.0)) as client: resp = await client.delete( url, headers={"Authorization": f"Bearer {token}"}, ) resp.raise_for_status() async def get_message( account: ResolvedGoogleChatAccount, message_name: str, ) -> dict | None: url = f"{CHAT_API_BASE}/{message_name}" token = await get_access_token(account) if not token: raise RuntimeError("Failed to obtain access token") async with httpx.AsyncClient(timeout=httpx.Timeout(15.0)) as client: resp = await client.get( url, headers={"Authorization": f"Bearer {token}"}, ) if resp.status_code == 404: return None resp.raise_for_status() return resp.json() async def upload_attachment( account: ResolvedGoogleChatAccount, space_name: str, filename: str, content: bytes, content_type: str, ) -> dict: url = f"{CHAT_UPLOAD_BASE}/{space_name}/attachments:upload" token = await get_access_token(account) if not token: raise RuntimeError("Failed to obtain access token") async with httpx.AsyncClient(timeout=httpx.Timeout(60.0)) as client: resp = await client.post( url, data={"filename": filename}, files={"data": (filename, content, content_type)}, headers={"Authorization": f"Bearer {token}"}, ) resp.raise_for_status() return resp.json() async def download_media_content( account: ResolvedGoogleChatAccount, resource_name: str, max_bytes: int = 20 * 1024 * 1024, ) -> bytes: url = f"{CHAT_MEDIA_BASE}/{resource_name}" token = await get_access_token(account) if not token: raise RuntimeError("Failed to obtain access token") async with httpx.AsyncClient(timeout=httpx.Timeout(60.0)) as client: resp = await client.get( url, headers={"Authorization": f"Bearer {token}"}, params={"alt": "media"}, ) resp.raise_for_status() content = resp.content if len(content) > max_bytes: raise ValueError(f"Media size {len(content)} exceeds limit {max_bytes}") return content async def create_reaction( account: ResolvedGoogleChatAccount, message_name: str, emoji: str, custom_emoji_url: str | None = None, ) -> dict: url = f"{CHAT_API_BASE}/{message_name}/reactions" token = await get_access_token(account) if not token: raise RuntimeError("Failed to obtain access token") body: dict = {"emoji": {"unicode": emoji}} if custom_emoji_url: body["emoji"]["customEmojiUrl"] = custom_emoji_url async with httpx.AsyncClient(timeout=httpx.Timeout(30.0)) as client: resp = await client.post( url, json=body, headers={ "Authorization": f"Bearer {token}", "Content-Type": "application/json", }, ) resp.raise_for_status() return resp.json() async def list_reactions( account: ResolvedGoogleChatAccount, message_name: str, ) -> list[dict]: url = f"{CHAT_API_BASE}/{message_name}/reactions" token = await get_access_token(account) if not token: raise RuntimeError("Failed to obtain access token") async with httpx.AsyncClient(timeout=httpx.Timeout(30.0)) as client: resp = await client.get( url, headers={"Authorization": f"Bearer {token}"}, ) resp.raise_for_status() data = resp.json() return data.get("reactions", []) async def delete_reaction( account: ResolvedGoogleChatAccount, reaction_name: str, ) -> None: url = f"{CHAT_API_BASE}/{reaction_name}" token = await get_access_token(account) if not token: raise RuntimeError("Failed to obtain access token") async with httpx.AsyncClient(timeout=httpx.Timeout(30.0)) as client: resp = await client.delete( url, headers={"Authorization": f"Bearer {token}"}, ) resp.raise_for_status() async def list_thread_messages( account: ResolvedGoogleChatAccount, space_name: str, thread_name: str, *, page_size: int = 20, order_by: str = "createTime desc", ) -> list[dict]: url = f"{CHAT_API_BASE}/{space_name}/messages" token = await get_access_token(account) if not token: raise RuntimeError("Failed to obtain access token") params = { "thread.name": thread_name, "pageSize": page_size, "orderBy": order_by, } async with httpx.AsyncClient(timeout=httpx.Timeout(10.0)) as client: resp = await client.get( url, params=params, headers={"Authorization": f"Bearer {token}"}, ) resp.raise_for_status() data = resp.json() return data.get("messages", []) async def list_messages( account: ResolvedGoogleChatAccount, space_name: str, *, page_size: int = 50, page_token: str | None = None, filter_str: str | None = None, order_by: str = "createTime desc", ) -> dict: url = f"{CHAT_API_BASE}/{space_name}/messages" token = await get_access_token(account) if not token: raise RuntimeError("Failed to obtain access token") params: dict = {"pageSize": page_size, "orderBy": order_by} if page_token: params["pageToken"] = page_token if filter_str: params["filter"] = filter_str async with httpx.AsyncClient(timeout=httpx.Timeout(10.0)) as client: resp = await client.get( url, params=params, headers={"Authorization": f"Bearer {token}"}, ) resp.raise_for_status() return resp.json() async def list_spaces( account: ResolvedGoogleChatAccount, *, page_size: int = 100, filter_str: str | None = None, ) -> list[dict]: url = f"{CHAT_API_BASE}/spaces" token = await get_access_token(account) if not token: raise RuntimeError("Failed to obtain access token") params: dict = {"pageSize": page_size} if filter_str: params["filter"] = filter_str async with httpx.AsyncClient(timeout=httpx.Timeout(15.0)) as client: resp = await client.get( url, params=params, headers={"Authorization": f"Bearer {token}"}, ) resp.raise_for_status() data = resp.json() return data.get("spaces", []) async def find_direct_message( account: ResolvedGoogleChatAccount, user_name: str, ) -> str | None: url = f"{CHAT_API_BASE}/spaces:findDirectMessage" token = await get_access_token(account) if not token: raise RuntimeError("Failed to obtain access token") async with httpx.AsyncClient(timeout=httpx.Timeout(30.0)) as client: resp = await client.get( url, params={"name": user_name}, headers={"Authorization": f"Bearer {token}"}, ) if resp.status_code == 404: return None resp.raise_for_status() data = resp.json() return data.get("name") async def list_members( account: ResolvedGoogleChatAccount, space_name: str, *, page_size: int = 100, page_token: str | None = None, ) -> dict: url = f"{CHAT_API_BASE}/{space_name}/members" token = await get_access_token(account) if not token: raise RuntimeError("Failed to obtain access token") params: dict = {"pageSize": page_size} if page_token: params["pageToken"] = page_token async with httpx.AsyncClient(timeout=httpx.Timeout(10.0)) as client: resp = await client.get( url, params=params, headers={"Authorization": f"Bearer {token}"}, ) resp.raise_for_status() return resp.json()