新增 Zalo OA、Zoom Chat、Zulip 三个渠道扩展。 Zalo OA 渠道扩展主要模块:sidecar_client, config, gateway, outbound, streaming, pairing, security, auth, dedupe, directory, monitor, status, session, reactions, tools Zoom Chat 渠道扩展主要模块:config, gateway, webhook, outbound, streaming, pairing, security, crypto, dedupe, actions, media, mentions, monitor, status, session, reactions, threading Zulip 渠道扩展主要模块:client, config, gateway, outbound, streaming, pairing, security, monitor, status
261 lines
8.1 KiB
Python
261 lines
8.1 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
from yuxi.channel.extensions.zalouser.format import chunk_text_for_outbound, format_for_zalouser_markdown
|
|
from yuxi.channel.extensions.zalouser.sidecar_client import ZcaSidecarClient
|
|
from yuxi.channel.protocols import OutboundDeliveryMode
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
DEFAULT_CHUNK_LIMIT = 2000
|
|
|
|
|
|
class ZaloUserOutboundAdapter:
|
|
delivery_mode = OutboundDeliveryMode.DIRECT
|
|
chunker_mode = "markdown"
|
|
text_chunk_limit = DEFAULT_CHUNK_LIMIT
|
|
poll_max_options = 10
|
|
supports_poll_duration_seconds = False
|
|
supports_anonymous_polls = False
|
|
extract_markdown_images = True
|
|
|
|
def __init__(self):
|
|
self._client: ZcaSidecarClient | None = None
|
|
|
|
def set_client(self, client: ZcaSidecarClient) -> None:
|
|
self._client = client
|
|
|
|
def chunker(self, text: str, limit: int, ctx: object | None = None) -> list[str]:
|
|
return chunk_text_for_outbound(text, limit)
|
|
|
|
def sanitize_text(self, text: str, payload: object) -> str:
|
|
return format_for_zalouser_markdown(text)
|
|
|
|
def should_skip_plain_text_sanitization(self, payload: object) -> bool:
|
|
return True
|
|
|
|
def normalize_payload(self, payload: object, config: dict, account_id: str | None = None) -> object | None:
|
|
return payload
|
|
|
|
def resolve_effective_text_chunk_limit(
|
|
self, config: dict, account_id: str | None = None, fallback_limit: int | None = None
|
|
) -> int | None:
|
|
return config.get("text_chunk_limit", DEFAULT_CHUNK_LIMIT)
|
|
|
|
async def send_text(
|
|
self,
|
|
target_id: str,
|
|
content: str,
|
|
*,
|
|
reply_to_id: str | None = None,
|
|
thread_id: str | None = None,
|
|
account_id: str | None = None,
|
|
) -> None:
|
|
if not self._client:
|
|
raise RuntimeError("ZcaSidecarClient not set on ZaloUserOutboundAdapter")
|
|
|
|
is_group = thread_id == "Group"
|
|
quote = None
|
|
if reply_to_id:
|
|
parts = reply_to_id.split(":", 1)
|
|
quote = {"msgId": parts[0], "cliMsgId": parts[1] if len(parts) > 1 else parts[0]}
|
|
|
|
await self._client.send_message(target_id, content, is_group=is_group, quote=quote)
|
|
|
|
async def send_media(
|
|
self,
|
|
target_id: str,
|
|
media_url: str,
|
|
media_type: str,
|
|
reply_to_id: str | None = None,
|
|
thread_id: str | None = None,
|
|
account_id: str | None = None,
|
|
) -> None:
|
|
if not self._client:
|
|
raise RuntimeError("ZcaSidecarClient not set on ZaloUserOutboundAdapter")
|
|
|
|
is_group = thread_id == "Group"
|
|
|
|
if media_type == "image":
|
|
await self._client.send_image(target_id, media_url, is_group=is_group)
|
|
elif media_type in ("voice", "audio"):
|
|
await self._client.send_voice(target_id, media_url, is_group=is_group)
|
|
else:
|
|
await self._client.send_attachment(target_id, media_url, media_type, is_group=is_group)
|
|
|
|
async def send_link(
|
|
self,
|
|
target_id: str,
|
|
url: str,
|
|
caption: str = "",
|
|
account_id: str | None = None,
|
|
) -> None:
|
|
if not self._client:
|
|
raise RuntimeError("ZcaSidecarClient not set on ZaloUserOutboundAdapter")
|
|
|
|
await self._client.send_link(target_id, url, caption)
|
|
|
|
async def send_reaction(
|
|
self,
|
|
target_id: str,
|
|
message_id: str,
|
|
icon: str,
|
|
account_id: str | None = None,
|
|
) -> None:
|
|
if not self._client:
|
|
raise RuntimeError("ZcaSidecarClient not set on ZaloUserOutboundAdapter")
|
|
|
|
parts = message_id.split(":", 1)
|
|
msg_id = parts[0]
|
|
cli_msg_id = parts[1] if len(parts) > 1 else msg_id
|
|
|
|
await self._client.send_reaction(target_id, msg_id, cli_msg_id, icon)
|
|
|
|
async def remove_reaction(
|
|
self,
|
|
target_id: str,
|
|
message_id: str,
|
|
account_id: str | None = None,
|
|
) -> None:
|
|
if not self._client:
|
|
raise RuntimeError("ZcaSidecarClient not set on ZaloUserOutboundAdapter")
|
|
|
|
parts = message_id.split(":", 1)
|
|
msg_id = parts[0]
|
|
cli_msg_id = parts[1] if len(parts) > 1 else msg_id
|
|
|
|
await self._client.remove_reaction(target_id, msg_id, cli_msg_id)
|
|
|
|
async def delete_message(
|
|
self,
|
|
target_id: str,
|
|
message_id: str,
|
|
account_id: str | None = None,
|
|
) -> None:
|
|
if not self._client:
|
|
raise RuntimeError("ZcaSidecarClient not set on ZaloUserOutboundAdapter")
|
|
|
|
parts = message_id.split(":", 1)
|
|
msg_id = parts[0]
|
|
cli_msg_id = parts[1] if len(parts) > 1 else msg_id
|
|
|
|
await self._client.delete_message(target_id, msg_id, cli_msg_id)
|
|
|
|
async def send_delivered(
|
|
self,
|
|
target_id: str,
|
|
message_id: str,
|
|
) -> None:
|
|
if not self._client:
|
|
return
|
|
|
|
msg_id = message_id.split(":", 1)[0]
|
|
await self._client.send_delivered(target_id, msg_id)
|
|
|
|
async def send_seen(
|
|
self,
|
|
target_id: str,
|
|
message_id: str,
|
|
) -> None:
|
|
if not self._client:
|
|
return
|
|
|
|
msg_id = message_id.split(":", 1)[0]
|
|
await self._client.send_seen(target_id, msg_id)
|
|
|
|
async def send_typing(self, target_id: str, account_id: str | None = None) -> None:
|
|
if not self._client:
|
|
return
|
|
|
|
await self._client.send_typing(target_id)
|
|
|
|
async def clear_typing(self, target_id: str, account_id: str | None = None) -> None:
|
|
pass
|
|
|
|
async def send_poll(
|
|
self,
|
|
target_id: str,
|
|
question: str,
|
|
options: list[str],
|
|
account_id: str | None = None,
|
|
) -> dict:
|
|
if not self._client:
|
|
raise RuntimeError("ZcaSidecarClient not set on ZaloUserOutboundAdapter")
|
|
return await self._client.create_poll(target_id, question, options)
|
|
|
|
async def end_poll(
|
|
self,
|
|
target_id: str,
|
|
poll_id: str,
|
|
account_id: str | None = None,
|
|
) -> dict:
|
|
if not self._client:
|
|
raise RuntimeError("ZcaSidecarClient not set on ZaloUserOutboundAdapter")
|
|
return await self._client.lock_poll(poll_id, target_id)
|
|
|
|
async def send_sticker(
|
|
self,
|
|
target_id: str,
|
|
sticker_obj: dict,
|
|
account_id: str | None = None,
|
|
) -> dict:
|
|
if not self._client:
|
|
raise RuntimeError("ZcaSidecarClient not set on ZaloUserOutboundAdapter")
|
|
return await self._client.send_sticker(target_id, sticker_obj)
|
|
|
|
async def send_card(
|
|
self,
|
|
target_id: str,
|
|
user_id: str,
|
|
phone: str,
|
|
name: str,
|
|
account_id: str | None = None,
|
|
) -> dict:
|
|
if not self._client:
|
|
raise RuntimeError("ZcaSidecarClient not set on ZaloUserOutboundAdapter")
|
|
return await self._client.send_card(target_id, user_id, phone, name)
|
|
|
|
async def create_note(
|
|
self,
|
|
target_id: str,
|
|
title: str,
|
|
content: str,
|
|
color: int = 0,
|
|
account_id: str | None = None,
|
|
) -> dict:
|
|
if not self._client:
|
|
raise RuntimeError("ZcaSidecarClient not set on ZaloUserOutboundAdapter")
|
|
return await self._client.create_note(target_id, title, content, color)
|
|
|
|
async def edit_note(
|
|
self,
|
|
target_id: str,
|
|
note_id: str,
|
|
title: str,
|
|
content: str,
|
|
color: int = 0,
|
|
account_id: str | None = None,
|
|
) -> dict:
|
|
if not self._client:
|
|
raise RuntimeError("ZcaSidecarClient not set on ZaloUserOutboundAdapter")
|
|
return await self._client.edit_note(target_id, note_id, title, content, color)
|
|
|
|
def resolve_target(
|
|
self,
|
|
to: str | None = None,
|
|
*,
|
|
config: dict | None = None,
|
|
allow_from: list[str] | None = None,
|
|
account_id: str | None = None,
|
|
mode: str | None = None,
|
|
) -> tuple[bool, str]:
|
|
if not to:
|
|
return False, "Target is required"
|
|
from yuxi.channel.extensions.zalouser.session import ZaloUserSessionAdapter
|
|
|
|
return True, ZaloUserSessionAdapter.normalize_target(to)
|
|
|
|
def should_treat_delivered_text_as_visible(self, kind: str, text: str | None = None) -> bool:
|
|
return True |