新增 KakaoTalk 渠道扩展,支持在 Yuxi 平台中集成 KakaoTalk 即时通讯渠道。 包含以下功能模块: - bot: Bot 客户端封装 - config: 渠道配置管理 - gateway: SSE/WebSocket 网关接入 - webhook: Webhook 事件处理 - outbound: 外发消息管理 - streaming: 流式消息处理 - pairing: 用户配对与绑定 - security: 安全校验 - dedupe: 消息去重 - monitor: 渠道状态监控 - status: 会话状态管理 - card_builder: KakaoTalk 卡片消息构建 - quick_reply: 快捷回复处理 - types: 类型定义
148 lines
4.3 KiB
Python
148 lines
4.3 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
|
|
import httpx
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
KAKAO_API_BASE = "https://kapi.kakao.com"
|
|
|
|
|
|
class KakaoTalkBotClient:
|
|
|
|
def __init__(self, admin_key: str):
|
|
self._admin_key = admin_key
|
|
self._http = httpx.AsyncClient(
|
|
base_url=KAKAO_API_BASE,
|
|
headers={
|
|
"Authorization": f"KakaoAK {admin_key}",
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
},
|
|
timeout=httpx.Timeout(10.0),
|
|
)
|
|
|
|
async def probe(self) -> bool:
|
|
try:
|
|
resp = await self._http.get("/v2/user/me")
|
|
return resp.status_code == 200
|
|
except Exception:
|
|
return False
|
|
|
|
async def send_text(self, user_key: str, text: str) -> dict:
|
|
template = {
|
|
"object_type": "text",
|
|
"text": text,
|
|
}
|
|
resp = await self._http.post(
|
|
"/v1/api/talk/friends/message/send",
|
|
data={
|
|
"receiver_uuids": json.dumps([user_key]),
|
|
"template_object": json.dumps(template),
|
|
},
|
|
)
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
async def send_photo(self, user_key: str, image_url: str) -> dict:
|
|
template = {
|
|
"object_type": "feed",
|
|
"content": {
|
|
"title": "",
|
|
"image_url": image_url,
|
|
"image_width": 800,
|
|
"image_height": 800,
|
|
"link": {"web_url": image_url},
|
|
},
|
|
}
|
|
resp = await self._http.post(
|
|
"/v1/api/talk/friends/message/send",
|
|
data={
|
|
"receiver_uuids": json.dumps([user_key]),
|
|
"template_object": json.dumps(template),
|
|
},
|
|
)
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
async def send_list(
|
|
self, user_key: str,
|
|
header_title: str,
|
|
items: list[dict],
|
|
buttons: list[dict] | None = None,
|
|
) -> dict:
|
|
template = {
|
|
"object_type": "list",
|
|
"header_title": header_title,
|
|
"header_link": {"web_url": ""},
|
|
"contents": [
|
|
{
|
|
"title": item.get("title", ""),
|
|
"description": item.get("description", ""),
|
|
"image_url": item.get("image_url", ""),
|
|
"link": item.get("link", {"web_url": ""}),
|
|
}
|
|
for item in items[:5]
|
|
],
|
|
}
|
|
if buttons:
|
|
template["buttons"] = buttons[:2]
|
|
return await self.send_template(user_key, template)
|
|
|
|
async def send_commerce(
|
|
self, user_key: str,
|
|
title: str,
|
|
price: int,
|
|
currency: str = "won",
|
|
product_url: str = "",
|
|
image_url: str = "",
|
|
) -> dict:
|
|
template = {
|
|
"object_type": "commerce",
|
|
"content": {
|
|
"title": title,
|
|
"image_url": image_url,
|
|
"link": {"web_url": product_url},
|
|
"regular_price": price,
|
|
"currency_unit": currency,
|
|
"currency_unit_position": 0,
|
|
},
|
|
}
|
|
return await self.send_template(user_key, template)
|
|
|
|
async def send_location(
|
|
self, user_key: str,
|
|
address: str,
|
|
latitude: float,
|
|
longitude: float,
|
|
content_title: str = "",
|
|
) -> dict:
|
|
template = {
|
|
"object_type": "location",
|
|
"content": {
|
|
"title": content_title or address,
|
|
"address": address,
|
|
},
|
|
"address": address,
|
|
"address_title": address,
|
|
"social": {},
|
|
}
|
|
return await self.send_template(user_key, template)
|
|
|
|
async def send_template(self, user_key: str, template: dict) -> dict:
|
|
return await self.send_template_multi([user_key], template)
|
|
|
|
async def send_template_multi(self, user_keys: list[str], template: dict) -> dict:
|
|
resp = await self._http.post(
|
|
"/v1/api/talk/friends/message/send",
|
|
data={
|
|
"receiver_uuids": json.dumps(user_keys[:5]),
|
|
"template_object": json.dumps(template),
|
|
},
|
|
)
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
|
|
async def close(self) -> None:
|
|
await self._http.aclose() |