455 lines
13 KiB
Python
455 lines
13 KiB
Python
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()
|