新增 KakaoTalk 渠道扩展,支持在 Yuxi 平台中集成 KakaoTalk 即时通讯渠道。 包含以下功能模块: - bot: Bot 客户端封装 - config: 渠道配置管理 - gateway: SSE/WebSocket 网关接入 - webhook: Webhook 事件处理 - outbound: 外发消息管理 - streaming: 流式消息处理 - pairing: 用户配对与绑定 - security: 安全校验 - dedupe: 消息去重 - monitor: 渠道状态监控 - status: 会话状态管理 - card_builder: KakaoTalk 卡片消息构建 - quick_reply: 快捷回复处理 - types: 类型定义
262 lines
8.7 KiB
Python
262 lines
8.7 KiB
Python
from __future__ import annotations
|
|
|
|
from yuxi.channel.extensions.kakaotalk.types import KakaoButton, KakaoQuickReply
|
|
|
|
|
|
class CardBuilder:
|
|
|
|
@staticmethod
|
|
def simple_text(text: str) -> dict:
|
|
return {"simpleText": {"text": text[:1000]}}
|
|
|
|
@staticmethod
|
|
def simple_image(image_url: str, alt_text: str) -> dict:
|
|
return {"simpleImage": {"imageUrl": image_url, "altText": alt_text}}
|
|
|
|
@staticmethod
|
|
def basic_card(
|
|
title: str,
|
|
description: str = "",
|
|
thumbnail_url: str | None = None,
|
|
buttons: list[KakaoButton] | None = None,
|
|
button_layout: str = "vertical",
|
|
profile_nickname: str | None = None,
|
|
profile_image_url: str | None = None,
|
|
forwardable: bool | None = None,
|
|
social_like: int = 0,
|
|
social_comment: int = 0,
|
|
social_share: int = 0,
|
|
) -> dict:
|
|
card: dict = {"title": title, "description": description}
|
|
if thumbnail_url:
|
|
card["thumbnail"] = {"imageUrl": thumbnail_url}
|
|
if profile_nickname:
|
|
card["profile"] = {"nickname": profile_nickname}
|
|
if profile_image_url:
|
|
card["profile"]["imageUrl"] = profile_image_url
|
|
if buttons:
|
|
card["buttons"] = [CardBuilder._build_button(b) for b in buttons]
|
|
card["buttonLayout"] = button_layout
|
|
if forwardable is not None:
|
|
card["forwardable"] = forwardable
|
|
if social_like or social_comment or social_share:
|
|
card["social"] = {
|
|
"like": social_like,
|
|
"comment": social_comment,
|
|
"share": social_share,
|
|
}
|
|
return {"basicCard": card}
|
|
|
|
@staticmethod
|
|
def commerce_card(
|
|
title: str,
|
|
description: str = "",
|
|
price: int = 0,
|
|
currency: str = "won",
|
|
discount: int = 0,
|
|
discount_rate: int = 0,
|
|
discounted_price: int = 0,
|
|
thumbnail_urls: list[str] | None = None,
|
|
profile_nickname: str | None = None,
|
|
profile_image_url: str | None = None,
|
|
buttons: list[KakaoButton] | None = None,
|
|
button_layout: str = "vertical",
|
|
) -> dict:
|
|
card: dict = {
|
|
"title": title,
|
|
"description": description,
|
|
"price": price,
|
|
"currency": currency,
|
|
}
|
|
if discount:
|
|
card["discount"] = discount
|
|
if discount_rate:
|
|
card["discountRate"] = discount_rate
|
|
if discounted_price:
|
|
card["discountedPrice"] = discounted_price
|
|
if thumbnail_urls:
|
|
card["thumbnails"] = [
|
|
{"imageUrl": url, "fixedRatio": True, "width": 800, "height": 800}
|
|
for url in thumbnail_urls
|
|
]
|
|
if profile_nickname:
|
|
card["profile"] = {"nickname": profile_nickname}
|
|
if profile_image_url:
|
|
card["profile"]["imageUrl"] = profile_image_url
|
|
if buttons:
|
|
card["buttons"] = [CardBuilder._build_button(b) for b in buttons]
|
|
card["buttonLayout"] = button_layout
|
|
return {"commerceCard": card}
|
|
|
|
@staticmethod
|
|
def list_card(
|
|
header_title: str,
|
|
items: list[dict],
|
|
buttons: list[KakaoButton] | None = None,
|
|
button_layout: str = "vertical",
|
|
) -> dict:
|
|
card: dict = {"header": {"title": header_title}, "items": items}
|
|
if buttons:
|
|
card["buttons"] = [CardBuilder._build_button(b) for b in buttons]
|
|
card["buttonLayout"] = button_layout
|
|
return {"listCard": card}
|
|
|
|
@staticmethod
|
|
def carousel(card_type: str, items: list[dict]) -> dict:
|
|
return {"carousel": {"type": card_type, "items": items}}
|
|
|
|
@staticmethod
|
|
def commerce_carousel(commerce_items: list[dict]) -> dict:
|
|
return {"carousel": {"type": "commerceCard", "items": commerce_items}}
|
|
|
|
@staticmethod
|
|
def text_card(
|
|
title: str,
|
|
description: str = "",
|
|
buttons: list[KakaoButton] | None = None,
|
|
button_layout: str = "vertical",
|
|
) -> dict:
|
|
card: dict = {"title": title[:50], "description": description[:400]}
|
|
if buttons:
|
|
card["buttons"] = [CardBuilder._build_button(b) for b in buttons]
|
|
card["buttonLayout"] = button_layout
|
|
return {"textCard": card}
|
|
|
|
@staticmethod
|
|
def item_card(
|
|
title: str = "",
|
|
description: str = "",
|
|
thumbnail_url: str | None = None,
|
|
item_list: list[dict] | None = None,
|
|
buttons: list[KakaoButton] | None = None,
|
|
button_layout: str = "vertical",
|
|
profile_nickname: str | None = None,
|
|
profile_image_url: str | None = None,
|
|
head_title: str = "",
|
|
image_title: str = "",
|
|
image_title_image_url: str | None = None,
|
|
) -> dict:
|
|
card: dict = {}
|
|
if title:
|
|
card["title"] = title
|
|
if description:
|
|
card["description"] = description
|
|
if thumbnail_url:
|
|
card["thumbnail"] = {"imageUrl": thumbnail_url}
|
|
if item_list:
|
|
card["itemList"] = item_list
|
|
if head_title:
|
|
card["head"] = {"title": head_title}
|
|
if image_title:
|
|
img_title: dict = {"title": image_title}
|
|
if image_title_image_url:
|
|
img_title["imageUrl"] = image_title_image_url
|
|
card["imageTitle"] = img_title
|
|
if profile_nickname:
|
|
card["profile"] = {"nickname": profile_nickname}
|
|
if profile_image_url:
|
|
card["profile"]["imageUrl"] = profile_image_url
|
|
if buttons:
|
|
card["buttons"] = [CardBuilder._build_button(b) for b in buttons]
|
|
card["buttonLayout"] = button_layout
|
|
return {"itemCard": card}
|
|
|
|
@staticmethod
|
|
def build_list_card_item(
|
|
title: str,
|
|
description: str = "",
|
|
image_url: str | None = None,
|
|
link_url: str | None = None,
|
|
) -> dict:
|
|
item: dict = {"title": title}
|
|
if description:
|
|
item["description"] = description
|
|
if image_url:
|
|
item["imageUrl"] = image_url
|
|
if link_url:
|
|
item["link"] = {"web": link_url}
|
|
return item
|
|
|
|
@staticmethod
|
|
def build_item_card_item(
|
|
title: str,
|
|
description: str = "",
|
|
image_url: str | None = None,
|
|
link_url: str | None = None,
|
|
) -> dict:
|
|
item: dict = {"title": title}
|
|
if description:
|
|
item["description"] = description
|
|
if image_url:
|
|
item["imageUrl"] = image_url
|
|
if link_url:
|
|
item["link"] = {"web": link_url}
|
|
return item
|
|
|
|
@staticmethod
|
|
def quick_reply(reply: KakaoQuickReply) -> dict:
|
|
qr: dict = {"label": reply.label, "action": reply.action}
|
|
if reply.message_text:
|
|
qr["messageText"] = reply.message_text
|
|
if reply.block_id:
|
|
qr["blockId"] = reply.block_id
|
|
if reply.web_link_url:
|
|
qr["webLinkUrl"] = reply.web_link_url
|
|
if reply.extra:
|
|
qr["extra"] = reply.extra
|
|
return qr
|
|
|
|
@staticmethod
|
|
def quick_replies(replies: list[dict]) -> list[dict]:
|
|
return replies
|
|
|
|
@staticmethod
|
|
def build_skill_response(
|
|
outputs: list[dict],
|
|
quick_replies: list[dict] | None = None,
|
|
context_values: list[dict] | None = None,
|
|
data: dict | None = None,
|
|
) -> dict:
|
|
template: dict = {"outputs": outputs}
|
|
if quick_replies:
|
|
template["quickReplies"] = quick_replies
|
|
|
|
skill_response: dict = {"version": "2.0", "template": template}
|
|
if context_values:
|
|
skill_response["context"] = {"values": context_values}
|
|
if data:
|
|
skill_response["data"] = data
|
|
return skill_response
|
|
|
|
@staticmethod
|
|
def build_context_value(
|
|
name: str,
|
|
life_span: int,
|
|
ttl: int = 0,
|
|
params: dict | None = None,
|
|
) -> dict:
|
|
cv: dict = {"name": name, "lifeSpan": life_span}
|
|
if ttl:
|
|
cv["ttl"] = ttl
|
|
if params:
|
|
cv["params"] = params
|
|
return cv
|
|
|
|
@staticmethod
|
|
def _build_button(button: KakaoButton) -> dict:
|
|
btn: dict = {"label": button.label, "action": button.action}
|
|
if button.message_text:
|
|
btn["messageText"] = button.message_text
|
|
if button.web_link_url:
|
|
btn["webLinkUrl"] = button.web_link_url
|
|
if button.phone_number:
|
|
btn["phoneNumber"] = button.phone_number
|
|
if button.block_id:
|
|
btn["blockId"] = button.block_id
|
|
if button.bot_command_id:
|
|
btn["botCommand"] = {"commandId": button.bot_command_id}
|
|
if button.operator_block_id:
|
|
btn["operatorBlock"] = {"blockId": button.operator_block_id}
|
|
if button.extra:
|
|
btn["extra"] = button.extra
|
|
return btn |