mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-16 17:36:07 +00:00
681 lines
29 KiB
Python
681 lines
29 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import base64
|
|
import hashlib
|
|
import json
|
|
import time
|
|
import traceback
|
|
import typing
|
|
import uuid
|
|
|
|
from Crypto.Cipher import AES
|
|
import lark_oapi
|
|
from lark_oapi.api.auth.v3 import (
|
|
CreateAppAccessTokenRequest,
|
|
CreateAppAccessTokenRequestBody,
|
|
CreateAppAccessTokenResponse,
|
|
CreateTenantAccessTokenRequest,
|
|
CreateTenantAccessTokenRequestBody,
|
|
CreateTenantAccessTokenResponse,
|
|
ResendAppTicketRequest,
|
|
ResendAppTicketRequestBody,
|
|
ResendAppTicketResponse,
|
|
)
|
|
from lark_oapi.api.cardkit.v1 import (
|
|
ContentCardElementRequest,
|
|
ContentCardElementRequestBody,
|
|
ContentCardElementResponse,
|
|
CreateCardRequest,
|
|
CreateCardRequestBody,
|
|
CreateCardResponse,
|
|
)
|
|
from lark_oapi.api.im.v1 import (
|
|
CreateMessageRequest,
|
|
CreateMessageRequestBody,
|
|
CreateMessageResponse,
|
|
EventMessage,
|
|
EventSender,
|
|
P2ImMessageReceiveV1,
|
|
P2ImMessageReceiveV1Data,
|
|
ReplyMessageRequest,
|
|
ReplyMessageRequestBody,
|
|
ReplyMessageResponse,
|
|
)
|
|
import lark_oapi.ws.exception
|
|
import pydantic
|
|
import quart
|
|
|
|
import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter
|
|
import langbot_plugin.api.definition.abstract.platform.event_logger as abstract_platform_logger
|
|
from langbot.pkg.platform.adapters.lark.api_impl import LarkAPIMixin
|
|
from langbot.pkg.platform.adapters.lark.event_converter import LarkEventConverter
|
|
from langbot.pkg.platform.adapters.lark.message_converter import LarkMessageConverter
|
|
from langbot.pkg.platform.adapters.lark.platform_api import PLATFORM_API_MAP
|
|
from langbot_plugin.api.entities.builtin.platform import entities as platform_entities
|
|
from langbot_plugin.api.entities.builtin.platform import events as platform_events
|
|
from langbot_plugin.api.entities.builtin.platform import message as platform_message
|
|
from langbot_plugin.api.entities.builtin.platform.errors import NotSupportedError
|
|
|
|
|
|
class AESCipher:
|
|
def __init__(self, key: str):
|
|
self.key = hashlib.sha256(self.str_to_bytes(key)).digest()
|
|
|
|
@staticmethod
|
|
def str_to_bytes(data):
|
|
if isinstance(data, str):
|
|
return data.encode('utf8')
|
|
return data
|
|
|
|
@staticmethod
|
|
def _unpad(value: bytes) -> bytes:
|
|
return value[: -value[len(value) - 1]]
|
|
|
|
def decrypt_string(self, encrypted: str) -> str:
|
|
encrypted_bytes = base64.b64decode(encrypted)
|
|
iv = encrypted_bytes[: AES.block_size]
|
|
cipher = AES.new(self.key, AES.MODE_CBC, iv)
|
|
return self._unpad(cipher.decrypt(encrypted_bytes[AES.block_size :])).decode('utf8')
|
|
|
|
|
|
class LarkAdapter(LarkAPIMixin, abstract_platform_adapter.AbstractPlatformAdapter):
|
|
bot: lark_oapi.ws.Client = pydantic.Field(exclude=True)
|
|
api_client: lark_oapi.Client = pydantic.Field(exclude=True)
|
|
quart_app: quart.Quart = pydantic.Field(exclude=True)
|
|
cipher: AESCipher = pydantic.Field(exclude=True)
|
|
|
|
config: dict
|
|
lark_tenant_key: str = pydantic.Field(exclude=True, default='')
|
|
app_ticket: str | None = None
|
|
app_access_token: str | None = None
|
|
app_access_token_expire_at: int | None = None
|
|
tenant_access_tokens: dict[str, dict[str, typing.Any]] = pydantic.Field(default_factory=dict)
|
|
bot_uuid: str | None = None
|
|
event_loop: asyncio.AbstractEventLoop | None = pydantic.Field(exclude=True, default=None)
|
|
|
|
message_converter: LarkMessageConverter = LarkMessageConverter()
|
|
event_converter: LarkEventConverter = LarkEventConverter()
|
|
listeners: dict[
|
|
typing.Type[platform_events.Event],
|
|
typing.Callable[[platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None],
|
|
] = pydantic.Field(default_factory=dict)
|
|
card_id_dict: dict[str, str] = pydantic.Field(default_factory=dict)
|
|
pending_monitoring_msg: dict[str, str] = pydantic.Field(default_factory=dict)
|
|
reply_to_monitoring_msg: dict[str, tuple[str, float]] = pydantic.Field(default_factory=dict)
|
|
_message_cache: dict[str, platform_events.MessageReceivedEvent] = pydantic.PrivateAttr(default_factory=dict)
|
|
_user_cache: dict[str, platform_entities.User] = pydantic.PrivateAttr(default_factory=dict)
|
|
_group_cache: dict[str, platform_entities.UserGroup] = pydantic.PrivateAttr(default_factory=dict)
|
|
_monitoring_mapping_ttl: int = 600
|
|
|
|
class Config:
|
|
arbitrary_types_allowed = True
|
|
|
|
def __init__(self, config: dict, logger: abstract_platform_logger.AbstractEventLogger, **kwargs):
|
|
required_keys = ['app_id', 'app_secret', 'bot_name']
|
|
missing_keys = [key for key in required_keys if not config.get(key)]
|
|
if missing_keys:
|
|
raise ValueError(f'Lark missing required config: {", ".join(missing_keys)}')
|
|
|
|
api_client = self.build_api_client(config)
|
|
event_handler = self._build_event_handler()
|
|
bot = lark_oapi.ws.Client(config['app_id'], config['app_secret'], event_handler=event_handler)
|
|
cipher = AESCipher(config.get('encrypt-key', ''))
|
|
|
|
super().__init__(
|
|
config=config,
|
|
logger=logger,
|
|
lark_tenant_key=config.get('lark_tenant_key', ''),
|
|
bot_account_id=config['bot_name'],
|
|
bot=bot,
|
|
api_client=api_client,
|
|
quart_app=quart.Quart(__name__),
|
|
cipher=cipher,
|
|
listeners={},
|
|
card_id_dict={},
|
|
pending_monitoring_msg={},
|
|
reply_to_monitoring_msg={},
|
|
event_loop=None,
|
|
**kwargs,
|
|
)
|
|
self._message_cache = {}
|
|
self._user_cache = {}
|
|
self._group_cache = {}
|
|
self.request_app_ticket()
|
|
|
|
def _build_event_handler(self):
|
|
async def on_message(event: lark_oapi.im.v1.P2ImMessageReceiveV1):
|
|
await self._handle_message_event(event)
|
|
|
|
def sync_on_message(event: lark_oapi.im.v1.P2ImMessageReceiveV1):
|
|
self._submit_coro(on_message(event))
|
|
|
|
def sync_on_card_action(event):
|
|
return self._handle_card_action_sync(event)
|
|
|
|
return (
|
|
lark_oapi.EventDispatcherHandler.builder('', '')
|
|
.register_p2_im_message_receive_v1(sync_on_message)
|
|
.register_p2_card_action_trigger(sync_on_card_action)
|
|
.build()
|
|
)
|
|
|
|
def get_supported_events(self) -> list[str]:
|
|
return ['message.received', 'bot.invited_to_group', 'platform.specific']
|
|
|
|
def get_supported_apis(self) -> list[str]:
|
|
return [
|
|
'send_message',
|
|
'reply_message',
|
|
'get_message',
|
|
'get_group_info',
|
|
'get_group_member_info',
|
|
'get_user_info',
|
|
'get_file_url',
|
|
'call_platform_api',
|
|
]
|
|
|
|
def build_api_client(self, config: dict) -> lark_oapi.Client:
|
|
builder = lark_oapi.Client.builder().app_id(config['app_id']).app_secret(config['app_secret'])
|
|
if config.get('app_type', 'self') == 'isv':
|
|
builder = builder.app_type(lark_oapi.AppType.ISV)
|
|
return builder.build()
|
|
|
|
def request_app_ticket(self):
|
|
if self.config.get('app_type', 'self') != 'isv':
|
|
return
|
|
request = (
|
|
ResendAppTicketRequest.builder()
|
|
.request_body(
|
|
ResendAppTicketRequestBody.builder()
|
|
.app_id(self.config['app_id'])
|
|
.app_secret(self.config['app_secret'])
|
|
.build()
|
|
)
|
|
.build()
|
|
)
|
|
response: ResendAppTicketResponse = self.api_client.auth.v3.app_ticket.resend(request)
|
|
if not response.success():
|
|
raise RuntimeError(f'Lark app_ticket resend failed: {response.code} {response.msg}')
|
|
|
|
def request_app_access_token(self):
|
|
if self.config.get('app_type', 'self') != 'isv':
|
|
return
|
|
request = (
|
|
CreateAppAccessTokenRequest.builder()
|
|
.request_body(
|
|
CreateAppAccessTokenRequestBody.builder()
|
|
.app_id(self.config['app_id'])
|
|
.app_secret(self.config['app_secret'])
|
|
.app_ticket(self.app_ticket)
|
|
.build()
|
|
)
|
|
.build()
|
|
)
|
|
response: CreateAppAccessTokenResponse = self.api_client.auth.v3.app_access_token.create(request)
|
|
if not response.success():
|
|
raise RuntimeError(f'Lark app_access_token failed: {response.code} {response.msg}')
|
|
content = json.loads(response.raw.content)
|
|
self.app_access_token = content['app_access_token']
|
|
self.app_access_token_expire_at = int(time.time()) + content['expire'] - 300
|
|
|
|
def get_app_access_token(self):
|
|
if self.config.get('app_type', 'self') != 'isv':
|
|
return None
|
|
if (
|
|
self.app_access_token is None
|
|
or self.app_access_token_expire_at is None
|
|
or int(time.time()) >= self.app_access_token_expire_at
|
|
):
|
|
self.request_app_access_token()
|
|
return self.app_access_token
|
|
|
|
def request_tenant_access_token(self, tenant_key: str):
|
|
if self.config.get('app_type', 'self') != 'isv':
|
|
return
|
|
request = (
|
|
CreateTenantAccessTokenRequest.builder()
|
|
.request_body(
|
|
CreateTenantAccessTokenRequestBody.builder()
|
|
.app_access_token(self.get_app_access_token())
|
|
.tenant_key(tenant_key)
|
|
.build()
|
|
)
|
|
.build()
|
|
)
|
|
response: CreateTenantAccessTokenResponse = self.api_client.auth.v3.tenant_access_token.create(request)
|
|
if not response.success():
|
|
raise RuntimeError(f'Lark tenant_access_token failed: {response.code} {response.msg}')
|
|
content = json.loads(response.raw.content)
|
|
self.tenant_access_tokens[tenant_key] = {
|
|
'token': content['tenant_access_token'],
|
|
'expire_at': int(time.time()) + content['expire'] - 300,
|
|
}
|
|
|
|
def get_tenant_access_token(self, tenant_key: str | None):
|
|
if self.config.get('app_type', 'self') != 'isv' or not tenant_key:
|
|
return None
|
|
cached = self.tenant_access_tokens.get(tenant_key)
|
|
if cached is None or int(time.time()) >= cached['expire_at']:
|
|
self.request_tenant_access_token(tenant_key)
|
|
return self.tenant_access_tokens.get(tenant_key, {}).get('token')
|
|
|
|
async def send_message(
|
|
self,
|
|
target_type: str,
|
|
target_id: str,
|
|
message: platform_message.MessageChain,
|
|
) -> platform_events.MessageResult:
|
|
text_elements, media_items = await self.message_converter.yiri2target(message, self.api_client)
|
|
receive_id_type = 'chat_id' if target_type == 'group' else 'open_id'
|
|
message_ids: list[str] = []
|
|
|
|
for msg_type, content in self._outbound_payloads(text_elements, media_items):
|
|
request = (
|
|
CreateMessageRequest.builder()
|
|
.receive_id_type(receive_id_type)
|
|
.request_body(
|
|
CreateMessageRequestBody.builder()
|
|
.receive_id(str(target_id))
|
|
.content(json.dumps(content, ensure_ascii=False))
|
|
.msg_type(msg_type)
|
|
.uuid(str(uuid.uuid4()))
|
|
.build()
|
|
)
|
|
.build()
|
|
)
|
|
response: CreateMessageResponse = await self.api_client.im.v1.message.acreate(request)
|
|
if not response.success():
|
|
raise RuntimeError(f'Lark send_message failed: {response.code} {response.msg}')
|
|
message_ids.append(getattr(response.data, 'message_id', ''))
|
|
|
|
return platform_events.MessageResult(
|
|
message_id=message_ids[-1] if message_ids else '', raw={'message_ids': message_ids}
|
|
)
|
|
|
|
async def reply_message(
|
|
self,
|
|
message_source: platform_events.MessageEvent,
|
|
message: platform_message.MessageChain,
|
|
quote_origin: bool = False,
|
|
) -> platform_events.MessageResult:
|
|
text_elements, media_items = await self.message_converter.yiri2target(message, self.api_client)
|
|
tenant_key = self._tenant_key_from_source(message_source)
|
|
message_ids: list[str] = []
|
|
|
|
for msg_type, content in self._outbound_payloads(text_elements, media_items):
|
|
request = (
|
|
ReplyMessageRequest.builder()
|
|
.message_id(self._message_id_from_source(message_source))
|
|
.request_body(
|
|
ReplyMessageRequestBody.builder()
|
|
.content(json.dumps(content, ensure_ascii=False))
|
|
.msg_type(msg_type)
|
|
.reply_in_thread(False)
|
|
.uuid(str(uuid.uuid4()))
|
|
.build()
|
|
)
|
|
.build()
|
|
)
|
|
response: ReplyMessageResponse = await self.api_client.im.v1.message.areply(
|
|
request, self.request_option(tenant_key)
|
|
)
|
|
if not response.success():
|
|
raise RuntimeError(f'Lark reply_message failed: {response.code} {response.msg}')
|
|
message_ids.append(getattr(response.data, 'message_id', ''))
|
|
|
|
return platform_events.MessageResult(
|
|
message_id=message_ids[-1] if message_ids else '', raw={'message_ids': message_ids}
|
|
)
|
|
|
|
def _outbound_payloads(self, text_elements: list[list[dict]], media_items: list[dict]) -> list[tuple[str, dict]]:
|
|
payloads: list[tuple[str, dict]] = []
|
|
if text_elements:
|
|
needs_post = any(ele.get('tag') == 'at' for paragraph in text_elements for ele in paragraph)
|
|
if needs_post:
|
|
payloads.append(('post', {'zh_Hans': {'title': '', 'content': text_elements}}))
|
|
else:
|
|
parts = []
|
|
for paragraph in text_elements:
|
|
text = ''.join(ele.get('text', '') for ele in paragraph)
|
|
if text:
|
|
parts.append(text)
|
|
payloads.append(('text', {'text': '\n\n'.join(parts)}))
|
|
for media in media_items:
|
|
payloads.append((media['msg_type'], media['content']))
|
|
return payloads
|
|
|
|
async def is_stream_output_supported(self) -> bool:
|
|
return bool(self.config.get('enable-stream-reply', False))
|
|
|
|
async def on_monitoring_message_created(self, query, monitoring_message_id: str):
|
|
user_msg_id = getattr(query.message_event, 'message_id', None)
|
|
if user_msg_id:
|
|
self.pending_monitoring_msg[str(user_msg_id)] = monitoring_message_id
|
|
|
|
async def create_message_card(self, message_id, event) -> bool:
|
|
card_id = await self.create_card_id(message_id)
|
|
content = {'type': 'card', 'data': {'card_id': card_id, 'template_variable': {'content': 'Thinking...'}}}
|
|
request = (
|
|
ReplyMessageRequest.builder()
|
|
.message_id(self._message_id_from_source(event))
|
|
.request_body(
|
|
ReplyMessageRequestBody.builder().content(json.dumps(content)).msg_type('interactive').build()
|
|
)
|
|
.build()
|
|
)
|
|
response: ReplyMessageResponse = await self.api_client.im.v1.message.areply(
|
|
request, self.request_option(self._tenant_key_from_source(event))
|
|
)
|
|
if not response.success():
|
|
raise RuntimeError(f'Lark create_message_card failed: {response.code} {response.msg}')
|
|
return True
|
|
|
|
async def create_card_id(self, message_id) -> str:
|
|
card_data = {
|
|
'schema': '2.0',
|
|
'config': {'update_multi': True, 'streaming_mode': True},
|
|
'body': {
|
|
'direction': 'vertical',
|
|
'elements': [{'tag': 'markdown', 'content': '', 'element_id': 'streaming_txt'}],
|
|
},
|
|
}
|
|
request = (
|
|
CreateCardRequest.builder()
|
|
.request_body(CreateCardRequestBody.builder().type('card_json').data(json.dumps(card_data)).build())
|
|
.build()
|
|
)
|
|
response: CreateCardResponse = self.api_client.cardkit.v1.card.create(request)
|
|
if not response.success():
|
|
raise RuntimeError(f'Lark create_card failed: {response.code} {response.msg}')
|
|
self.card_id_dict[str(message_id)] = response.data.card_id
|
|
return response.data.card_id
|
|
|
|
async def reply_message_chunk(
|
|
self,
|
|
message_source: platform_events.MessageEvent,
|
|
bot_message,
|
|
message: platform_message.MessageChain,
|
|
quote_origin: bool = False,
|
|
is_final: bool = False,
|
|
):
|
|
if bot_message.msg_sequence % 8 != 0 and not is_final:
|
|
return
|
|
text_elements, _ = await self.message_converter.yiri2target(message, self.api_client)
|
|
content = '\n\n'.join(
|
|
''.join(ele.get('text', '') for ele in paragraph if ele.get('tag') in {'text', 'md'})
|
|
for paragraph in text_elements
|
|
)
|
|
request = (
|
|
ContentCardElementRequest.builder()
|
|
.card_id(self.card_id_dict[bot_message.resp_message_id])
|
|
.element_id('streaming_txt')
|
|
.request_body(
|
|
ContentCardElementRequestBody.builder().content(content).sequence(bot_message.msg_sequence).build()
|
|
)
|
|
.build()
|
|
)
|
|
response: ContentCardElementResponse = self.api_client.cardkit.v1.card_element.content(
|
|
request, self.request_option(self._tenant_key_from_source(message_source))
|
|
)
|
|
if not response.success():
|
|
raise RuntimeError(f'Lark card_element update failed: {response.code} {response.msg}')
|
|
if is_final and bot_message.tool_calls is None:
|
|
self.card_id_dict.pop(bot_message.resp_message_id, None)
|
|
|
|
async def call_platform_api(self, action: str, params: dict = {}) -> dict:
|
|
handler = PLATFORM_API_MAP.get(action)
|
|
if handler is None:
|
|
raise NotSupportedError(f'call_platform_api:{action}')
|
|
return await handler(self, params)
|
|
|
|
def register_listener(
|
|
self,
|
|
event_type: typing.Type[platform_events.Event],
|
|
callback: typing.Callable[
|
|
[platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None
|
|
],
|
|
):
|
|
self.listeners[event_type] = callback
|
|
|
|
def unregister_listener(
|
|
self,
|
|
event_type: typing.Type[platform_events.Event],
|
|
callback: typing.Callable[
|
|
[platform_events.Event, abstract_platform_adapter.AbstractMessagePlatformAdapter], None
|
|
],
|
|
):
|
|
if self.listeners.get(event_type) is callback:
|
|
self.listeners.pop(event_type, None)
|
|
|
|
def set_bot_uuid(self, bot_uuid: str):
|
|
self.bot_uuid = bot_uuid
|
|
|
|
def get_launcher_id(self, event: platform_events.MessageEvent) -> str | None:
|
|
source_event = getattr(event.source_platform_object, 'event', None)
|
|
message = getattr(source_event, 'message', None) if source_event else None
|
|
thread_id = getattr(message, 'thread_id', None)
|
|
if thread_id and isinstance(event, platform_events.MessageReceivedEvent) and event.group:
|
|
return f'{event.group.id}_{thread_id}'
|
|
return None
|
|
|
|
async def handle_unified_webhook(self, bot_uuid: str, path: str, request):
|
|
try:
|
|
data = await request.json
|
|
if 'encrypt' in data:
|
|
data = json.loads(self.cipher.decrypt_string(data['encrypt']))
|
|
event_type = self.get_event_type(data)
|
|
if event_type == 'url_verification':
|
|
return {'challenge': data.get('challenge')}
|
|
if event_type == 'app_ticket':
|
|
self.app_ticket = self._webhook_event(data).get('app_ticket')
|
|
return {'code': 200, 'message': 'ok'}
|
|
if event_type == 'im.message.receive_v1':
|
|
p2v1 = P2ImMessageReceiveV1()
|
|
p2v1.header = self._webhook_header(data)
|
|
event_data = P2ImMessageReceiveV1Data()
|
|
raw_event = self._webhook_event(data)
|
|
event_data.message = EventMessage(raw_event['message'])
|
|
event_data.sender = EventSender(raw_event['sender'])
|
|
p2v1.event = event_data
|
|
p2v1.schema = data.get('schema', '2.0')
|
|
await self._handle_message_event(p2v1)
|
|
return {'code': 200, 'message': 'ok'}
|
|
if event_type == 'im.chat.member.bot.added_v1':
|
|
raw_event = self._webhook_event(data)
|
|
header = self._webhook_header(data)
|
|
chat_id = raw_event.get('chat_id', '')
|
|
await self._send_bot_added_welcome(chat_id, getattr(header, 'tenant_key', None))
|
|
await self._dispatch_eba_event(LarkEventConverter.bot_invited_to_group(data, chat_id))
|
|
return {'code': 200, 'message': 'ok'}
|
|
if event_type == 'card.action.trigger':
|
|
feedback_event = self._feedback_event_from_webhook(data)
|
|
if feedback_event and platform_events.FeedbackEvent in self.listeners:
|
|
await self.listeners[platform_events.FeedbackEvent](feedback_event, self)
|
|
return {'toast': {'type': 'success', 'content': '感谢您的反馈'}}
|
|
await self._dispatch_eba_event(LarkEventConverter.platform_specific(data, event_type, data))
|
|
return {'code': 200, 'message': 'ok'}
|
|
except Exception:
|
|
await self.logger.error(f'Error in lark webhook: {traceback.format_exc()}')
|
|
return {'code': 500, 'message': 'error'}
|
|
|
|
def get_event_type(self, data: dict) -> str:
|
|
schema = data.get('schema', '1.0')
|
|
if schema == '2.0':
|
|
return data.get('header', {}).get('event_type', '')
|
|
if 'event' in data:
|
|
return data['event'].get('type', '')
|
|
return data.get('type', '')
|
|
|
|
def _webhook_event(self, data: dict) -> dict:
|
|
return data.get('event', {})
|
|
|
|
def _webhook_header(self, data: dict):
|
|
return type('LarkWebhookHeader', (), data.get('header', {}))()
|
|
|
|
async def run_async(self):
|
|
self.event_loop = asyncio.get_running_loop()
|
|
if not self.config.get('enable-webhook', False):
|
|
try:
|
|
await self.bot._connect()
|
|
except lark_oapi.ws.exception.ClientException:
|
|
raise
|
|
except Exception:
|
|
await self.bot._disconnect()
|
|
if self.bot._auto_reconnect:
|
|
await self.bot._reconnect()
|
|
else:
|
|
raise
|
|
else:
|
|
while True:
|
|
await asyncio.sleep(1)
|
|
|
|
async def kill(self) -> bool:
|
|
self.bot._auto_reconnect = False
|
|
await self.bot._disconnect()
|
|
return True
|
|
|
|
async def is_muted(self, group_id: int | None = None) -> bool:
|
|
return False
|
|
|
|
async def _handle_message_event(self, event: lark_oapi.im.v1.P2ImMessageReceiveV1):
|
|
try:
|
|
if platform_events.FriendMessage in self.listeners or platform_events.GroupMessage in self.listeners:
|
|
legacy_event = await self.event_converter.target2legacy(event, self.api_client)
|
|
if legacy_event and type(legacy_event) in self.listeners:
|
|
await self.listeners[type(legacy_event)](legacy_event, self)
|
|
eba_event = await self.event_converter.target2yiri(event, self.api_client)
|
|
if eba_event:
|
|
self._cache_event(eba_event)
|
|
await self._dispatch_eba_event(eba_event)
|
|
except Exception:
|
|
await self.logger.error(f'Error in lark message event: {traceback.format_exc()}')
|
|
|
|
async def _dispatch_eba_event(self, event: platform_events.Event):
|
|
for event_type in (type(event), platform_events.EBAEvent, platform_events.Event):
|
|
callback = self.listeners.get(event_type)
|
|
if callback:
|
|
await callback(event, self)
|
|
return
|
|
|
|
def _cache_event(self, event: platform_events.Event):
|
|
if not isinstance(event, platform_events.MessageReceivedEvent):
|
|
return
|
|
self._message_cache[str(event.message_id)] = event
|
|
self._user_cache[str(event.sender.id)] = event.sender
|
|
if event.group:
|
|
self._group_cache[str(event.group.id)] = event.group
|
|
|
|
def _handle_card_action_sync(self, event):
|
|
feedback_event = self._feedback_event_from_callback(event)
|
|
if feedback_event and platform_events.FeedbackEvent in self.listeners:
|
|
self._submit_coro(self.listeners[platform_events.FeedbackEvent](feedback_event, self))
|
|
from lark_oapi.event.callback.model.p2_card_action_trigger import P2CardActionTriggerResponse
|
|
|
|
return P2CardActionTriggerResponse({'toast': {'type': 'success', 'content': '感谢您的反馈'}})
|
|
|
|
def _submit_coro(self, coro):
|
|
try:
|
|
loop = asyncio.get_running_loop()
|
|
except RuntimeError:
|
|
loop = self.event_loop
|
|
if loop and loop.is_running():
|
|
asyncio.run_coroutine_threadsafe(coro, loop)
|
|
return
|
|
coro.close()
|
|
raise
|
|
else:
|
|
loop.create_task(coro)
|
|
|
|
def _feedback_event_from_callback(self, event) -> platform_events.FeedbackEvent | None:
|
|
value = getattr(getattr(event.event, 'action', None), 'value', {}) or {}
|
|
return self._feedback_event(
|
|
raw=event,
|
|
feedback_id=getattr(event.header, 'event_id', str(uuid.uuid4())),
|
|
feedback_value=value.get('feedback', ''),
|
|
user_id=getattr(getattr(event.event, 'operator', None), 'open_id', None),
|
|
chat_id=getattr(getattr(event.event, 'context', None), 'open_chat_id', None),
|
|
message_id=getattr(getattr(event.event, 'context', None), 'open_message_id', None),
|
|
)
|
|
|
|
def _feedback_event_from_webhook(self, data: dict) -> platform_events.FeedbackEvent | None:
|
|
event = data.get('event', {})
|
|
value = event.get('action', {}).get('value', {}) or {}
|
|
operator = event.get('operator', {})
|
|
context = event.get('context', {})
|
|
return self._feedback_event(
|
|
raw=data,
|
|
feedback_id=data.get('header', {}).get('event_id', str(uuid.uuid4())),
|
|
feedback_value=value.get('feedback', ''),
|
|
user_id=operator.get('open_id') or operator.get('user_id'),
|
|
chat_id=context.get('open_chat_id'),
|
|
message_id=context.get('open_message_id'),
|
|
)
|
|
|
|
def _feedback_event(
|
|
self,
|
|
raw,
|
|
feedback_id: str,
|
|
feedback_value: str,
|
|
user_id: str | None,
|
|
chat_id: str | None,
|
|
message_id: str | None,
|
|
) -> platform_events.FeedbackEvent | None:
|
|
if feedback_value == '有帮助':
|
|
feedback_type = 1
|
|
elif feedback_value == '无帮助':
|
|
feedback_type = 2
|
|
else:
|
|
return None
|
|
return platform_events.FeedbackEvent(
|
|
feedback_id=feedback_id,
|
|
feedback_type=feedback_type,
|
|
feedback_content=feedback_value,
|
|
user_id=user_id,
|
|
session_id=f'group_{chat_id}' if chat_id else (f'person_{user_id}' if user_id else None),
|
|
message_id=message_id,
|
|
stream_id=self.reply_to_monitoring_msg.get(message_id, (None, 0))[0] if message_id else None,
|
|
source_platform_object=raw,
|
|
)
|
|
|
|
async def _send_bot_added_welcome(self, chat_id: str, tenant_key: str | None):
|
|
welcome = self.config.get('bot_added_welcome', '')
|
|
if not welcome or not chat_id:
|
|
return
|
|
content = {'zh_Hans': {'title': '', 'content': [[{'tag': 'md', 'text': welcome}]]}}
|
|
request = (
|
|
CreateMessageRequest.builder()
|
|
.receive_id_type('chat_id')
|
|
.request_body(
|
|
CreateMessageRequestBody.builder()
|
|
.receive_id(chat_id)
|
|
.content(json.dumps(content, ensure_ascii=False))
|
|
.msg_type('post')
|
|
.uuid(str(uuid.uuid4()))
|
|
.build()
|
|
)
|
|
.build()
|
|
)
|
|
response: CreateMessageResponse = await self.api_client.im.v1.message.acreate(
|
|
request, self.request_option(tenant_key)
|
|
)
|
|
if not response.success():
|
|
await self.logger.warning(f'Lark bot_added_welcome failed: {response.code} {response.msg}')
|
|
|
|
def _tenant_key_from_source(self, event: platform_events.Event) -> str | None:
|
|
source = getattr(event, 'source_platform_object', None)
|
|
header = getattr(source, 'header', None)
|
|
return getattr(header, 'tenant_key', None)
|
|
|
|
def _message_id_from_source(self, event: platform_events.Event) -> str:
|
|
message_id = getattr(event, 'message_id', None)
|
|
if message_id:
|
|
return str(message_id)
|
|
source = getattr(event, 'source_platform_object', None)
|
|
source_event = getattr(source, 'event', None)
|
|
message = getattr(source_event, 'message', None) if source_event else None
|
|
message_id = getattr(message, 'message_id', None)
|
|
if message_id:
|
|
return str(message_id)
|
|
raise RuntimeError('Lark message source does not contain message_id')
|