mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-22 12:26:08 +00:00
fix(platform): align eba event declarations
This commit is contained in:
@@ -10,7 +10,7 @@ The DingTalk adapter now has an Event-Based Agents adapter package with:
|
|||||||
|
|
||||||
- `manifest.yaml` for adapter metadata, configuration, events, common APIs, and platform-specific APIs.
|
- `manifest.yaml` for adapter metadata, configuration, events, common APIs, and platform-specific APIs.
|
||||||
- `adapter.py` for DingTalk client startup, native callback handling, legacy compatibility, and EBA dispatch.
|
- `adapter.py` for DingTalk client startup, native callback handling, legacy compatibility, and EBA dispatch.
|
||||||
- `event_converter.py` for native DingTalk events to common EBA events.
|
- `event_converter.py` for native DingTalk events and card callbacks to common EBA events.
|
||||||
- `message_converter.py` for DingTalk message payloads to/from common `MessageChain` components.
|
- `message_converter.py` for DingTalk message payloads to/from common `MessageChain` components.
|
||||||
- `api_impl.py` for common EBA API implementations.
|
- `api_impl.py` for common EBA API implementations.
|
||||||
- `platform_api.py` for DingTalk-specific `call_platform_api` actions.
|
- `platform_api.py` for DingTalk-specific `call_platform_api` actions.
|
||||||
@@ -33,7 +33,8 @@ The legacy DingTalk HTTP client now returns successful JSON response bodies from
|
|||||||
| Event | Support | Evidence |
|
| Event | Support | Evidence |
|
||||||
|-------|---------|----------|
|
|-------|---------|----------|
|
||||||
| `message.received` | implemented | `plugin-e2e-ui` private text and emoji-as-text. |
|
| `message.received` | implemented | `plugin-e2e-ui` private text and emoji-as-text. |
|
||||||
| `platform.specific` | implemented | Not reproduced in the latest UI run. |
|
| `feedback.received` | unit covered | DingTalk card callback actions with feedback-like values (`like`, `dislike`, `cancel`, or `1`/`2`/`3`) map to `FeedbackReceivedEvent`. Other card actions remain `platform.specific`. |
|
||||||
|
| `platform.specific` | implemented | Non-feedback card callbacks and unmapped callback/message shapes are emitted as structured platform-specific events. |
|
||||||
|
|
||||||
## Receive Components
|
## Receive Components
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ Discord declares these EBA events:
|
|||||||
- `message.reaction`
|
- `message.reaction`
|
||||||
- `group.member_joined`
|
- `group.member_joined`
|
||||||
- `group.member_left`
|
- `group.member_left`
|
||||||
- `group.member_banned`
|
|
||||||
- `bot.invited_to_group`
|
- `bot.invited_to_group`
|
||||||
- `bot.removed_from_group`
|
- `bot.removed_from_group`
|
||||||
- `platform.specific`
|
- `platform.specific`
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ Status: partial migration. Unit/API-shape coverage is present, and private text
|
|||||||
| Event | Evidence | Notes |
|
| Event | Evidence | Notes |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `message.received` | plugin-e2e-ui, unit | Text UI message verified through WeChat Official Account on `dev.rockchin.top`; image and voice webhook payloads are covered by unit tests. |
|
| `message.received` | plugin-e2e-ui, unit | Text UI message verified through WeChat Official Account on `dev.rockchin.top`; image and voice webhook payloads are covered by unit tests. |
|
||||||
| `platform.specific` | unit | Subscribe/menu/etc. native events are emitted as structured `PlatformSpecificEvent`. |
|
| `platform.specific` | unit | Subscribe/unsubscribe/menu/etc. native events are emitted as structured `PlatformSpecificEvent`. |
|
||||||
|
|
||||||
## Common APIs
|
## Common APIs
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,12 @@ Status: partial migration. The EBA adapter structure, manifest, converters, cach
|
|||||||
| Event | Evidence | Notes |
|
| Event | Evidence | Notes |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `message.received` | adapter-live, unit | `C2C_MESSAGE_CREATE`, `DIRECT_MESSAGE_CREATE`, `GROUP_AT_MESSAGE_CREATE`, and `AT_MESSAGE_CREATE` map to common `MessageReceivedEvent`. A real WebSocket-mode QQ Official bot reached the LangBot pipeline on `dev.rockchin.top`; plugin JSONL evidence remains pending. |
|
| `message.received` | adapter-live, unit | `C2C_MESSAGE_CREATE`, `DIRECT_MESSAGE_CREATE`, `GROUP_AT_MESSAGE_CREATE`, and `AT_MESSAGE_CREATE` map to common `MessageReceivedEvent`. A real WebSocket-mode QQ Official bot reached the LangBot pipeline on `dev.rockchin.top`; plugin JSONL evidence remains pending. |
|
||||||
| `platform.specific` | unit, blocked | Unmapped gateway events are emitted as structured `PlatformSpecificEvent`; live evidence is pending. |
|
| `message.reaction` | unit | `MESSAGE_REACTION_ADD` and `MESSAGE_REACTION_REMOVE` map to common `MessageReactionEvent`. Live gateway evidence is pending. |
|
||||||
|
| `group.member_joined` | unit | `GUILD_MEMBER_ADD` and `GROUP_MEMBER_ADD` map to common `MemberJoinedEvent` when the gateway payload carries a group/guild/channel ID and member openid. |
|
||||||
|
| `group.member_left` | unit | `GUILD_MEMBER_REMOVE` and `GROUP_MEMBER_REMOVE` map to common `MemberLeftEvent`. Live gateway evidence is pending. |
|
||||||
|
| `bot.invited_to_group` | unit | `GUILD_CREATE` and `GROUP_ADD_ROBOT` map to common `BotInvitedToGroupEvent`. |
|
||||||
|
| `bot.removed_from_group` | unit | `GUILD_DELETE` and `GROUP_DEL_ROBOT` map to common `BotRemovedFromGroupEvent`. |
|
||||||
|
| `platform.specific` | unit | Unmapped gateway events are emitted as structured `PlatformSpecificEvent`; live evidence is pending. |
|
||||||
|
|
||||||
## Common APIs
|
## Common APIs
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|||||||
import traceback
|
import traceback
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
|
import dingtalk_stream
|
||||||
import pydantic
|
import pydantic
|
||||||
|
|
||||||
from langbot.libs.dingtalk_api.api import DingTalkClient
|
from langbot.libs.dingtalk_api.api import DingTalkClient
|
||||||
@@ -19,6 +20,32 @@ from langbot_plugin.api.entities.builtin.platform import message as platform_mes
|
|||||||
from langbot_plugin.api.entities.builtin.platform.errors import NotSupportedError
|
from langbot_plugin.api.entities.builtin.platform.errors import NotSupportedError
|
||||||
|
|
||||||
|
|
||||||
|
class DingTalkCardCallbackHandler(dingtalk_stream.CallbackHandler):
|
||||||
|
def __init__(self, adapter: 'DingTalkAdapter'):
|
||||||
|
super().__init__()
|
||||||
|
self.adapter = adapter
|
||||||
|
|
||||||
|
async def process(self, message: dingtalk_stream.CallbackMessage):
|
||||||
|
callback = dingtalk_stream.CardCallbackMessage.from_dict(message.data or {})
|
||||||
|
event = DingTalkEvent.from_payload(
|
||||||
|
{
|
||||||
|
'conversation_type': 'CardCallback',
|
||||||
|
'Type': 'card_callback',
|
||||||
|
'CardCallback': {
|
||||||
|
'extension': callback.extension,
|
||||||
|
'corp_id': callback.corp_id,
|
||||||
|
'user_id': callback.user_id,
|
||||||
|
'content': callback.content,
|
||||||
|
'space_id': callback.space_id,
|
||||||
|
'card_instance_id': callback.card_instance_id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if event is not None:
|
||||||
|
await self.adapter._handle_native_event(event)
|
||||||
|
return dingtalk_stream.AckMessage.STATUS_OK, 'OK'
|
||||||
|
|
||||||
|
|
||||||
class DingTalkAdapter(DingTalkAPIMixin, abstract_platform_adapter.AbstractPlatformAdapter):
|
class DingTalkAdapter(DingTalkAPIMixin, abstract_platform_adapter.AbstractPlatformAdapter):
|
||||||
bot: DingTalkClient = pydantic.Field(exclude=True)
|
bot: DingTalkClient = pydantic.Field(exclude=True)
|
||||||
|
|
||||||
@@ -68,6 +95,7 @@ class DingTalkAdapter(DingTalkAPIMixin, abstract_platform_adapter.AbstractPlatfo
|
|||||||
def get_supported_events(self) -> list[str]:
|
def get_supported_events(self) -> list[str]:
|
||||||
return [
|
return [
|
||||||
'message.received',
|
'message.received',
|
||||||
|
'feedback.received',
|
||||||
'platform.specific',
|
'platform.specific',
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -198,6 +226,10 @@ class DingTalkAdapter(DingTalkAPIMixin, abstract_platform_adapter.AbstractPlatfo
|
|||||||
|
|
||||||
self.bot.on_message('FriendMessage')(on_message)
|
self.bot.on_message('FriendMessage')(on_message)
|
||||||
self.bot.on_message('GroupMessage')(on_message)
|
self.bot.on_message('GroupMessage')(on_message)
|
||||||
|
self.bot.client.register_callback_handler(
|
||||||
|
dingtalk_stream.CallbackHandler.TOPIC_CARD_CALLBACK,
|
||||||
|
DingTalkCardCallbackHandler(self),
|
||||||
|
)
|
||||||
|
|
||||||
async def _handle_native_event(self, event: DingTalkEvent):
|
async def _handle_native_event(self, event: DingTalkEvent):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ class DingTalkEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
|||||||
async def target2yiri(event: DingTalkEvent, bot_name: str) -> platform_events.Event | None:
|
async def target2yiri(event: DingTalkEvent, bot_name: str) -> platform_events.Event | None:
|
||||||
if event.conversation in {'FriendMessage', 'GroupMessage'}:
|
if event.conversation in {'FriendMessage', 'GroupMessage'}:
|
||||||
return await DingTalkEventConverter.message_to_eba(event, bot_name)
|
return await DingTalkEventConverter.message_to_eba(event, bot_name)
|
||||||
|
if event.conversation == 'CardCallback':
|
||||||
|
feedback = DingTalkEventConverter.card_callback_to_feedback(event)
|
||||||
|
if feedback is not None:
|
||||||
|
return feedback
|
||||||
|
return DingTalkEventConverter.platform_specific(event, 'card.callback')
|
||||||
return DingTalkEventConverter.platform_specific(event, f'message.{event.conversation or "unknown"}')
|
return DingTalkEventConverter.platform_specific(event, f'message.{event.conversation or "unknown"}')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -57,6 +62,54 @@ class DingTalkEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
|||||||
source_platform_object=event,
|
source_platform_object=event,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def card_callback_to_feedback(event: DingTalkEvent) -> platform_events.FeedbackReceivedEvent | None:
|
||||||
|
callback = event.get('CardCallback') or {}
|
||||||
|
content = callback.get('content') or {}
|
||||||
|
extension = callback.get('extension') or {}
|
||||||
|
action = str(
|
||||||
|
content.get('action') or content.get('actionKey') or content.get('value') or extension.get('action') or ''
|
||||||
|
).lower()
|
||||||
|
feedback_type = DingTalkEventConverter._feedback_type(
|
||||||
|
action or content.get('feedback_type') or content.get('feedbackType')
|
||||||
|
)
|
||||||
|
if feedback_type is None:
|
||||||
|
return None
|
||||||
|
feedback_id = str(
|
||||||
|
content.get('feedback_id')
|
||||||
|
or content.get('feedbackId')
|
||||||
|
or callback.get('card_instance_id')
|
||||||
|
or callback.get('user_id')
|
||||||
|
or ''
|
||||||
|
)
|
||||||
|
return platform_events.FeedbackReceivedEvent(
|
||||||
|
type='feedback.received',
|
||||||
|
adapter_name=ADAPTER_NAME,
|
||||||
|
feedback_id=feedback_id,
|
||||||
|
feedback_type=feedback_type,
|
||||||
|
feedback_content=content.get('feedback_content') or content.get('feedbackContent') or content.get('reason'),
|
||||||
|
inaccurate_reasons=content.get('inaccurate_reasons') or content.get('inaccurateReasons'),
|
||||||
|
user_id=callback.get('user_id') or None,
|
||||||
|
session_id=callback.get('space_id') or None,
|
||||||
|
message_id=content.get('message_id') or content.get('messageId'),
|
||||||
|
stream_id=content.get('stream_id') or content.get('streamId'),
|
||||||
|
timestamp=0.0,
|
||||||
|
source_platform_object=event,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _feedback_type(value: typing.Any) -> int | None:
|
||||||
|
if isinstance(value, int) and value in {1, 2, 3}:
|
||||||
|
return value
|
||||||
|
normalized = str(value or '').lower()
|
||||||
|
if normalized in {'1', 'like', 'liked', 'thumb_up', 'thumbup', 'up', 'good'}:
|
||||||
|
return 1
|
||||||
|
if normalized in {'2', 'dislike', 'disliked', 'thumb_down', 'thumbdown', 'down', 'bad'}:
|
||||||
|
return 2
|
||||||
|
if normalized in {'3', 'cancel', 'remove', 'removed', 'clear'}:
|
||||||
|
return 3
|
||||||
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def user_from_event(event: DingTalkEvent) -> platform_entities.User:
|
def user_from_event(event: DingTalkEvent) -> platform_entities.User:
|
||||||
incoming_message = event.incoming_message
|
incoming_message = event.incoming_message
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ spec:
|
|||||||
|
|
||||||
supported_events:
|
supported_events:
|
||||||
- message.received
|
- message.received
|
||||||
|
- feedback.received
|
||||||
- platform.specific
|
- platform.specific
|
||||||
|
|
||||||
supported_apis:
|
supported_apis:
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
ADAPTER_NAME = 'dingtalk'
|
ADAPTER_NAME = 'dingtalk-eba'
|
||||||
|
|||||||
@@ -161,7 +161,11 @@ class LarkAdapter(LarkAPIMixin, abstract_platform_adapter.AbstractPlatformAdapte
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_supported_events(self) -> list[str]:
|
def get_supported_events(self) -> list[str]:
|
||||||
return ['message.received', 'bot.invited_to_group', 'platform.specific']
|
return [
|
||||||
|
'message.received',
|
||||||
|
'bot.invited_to_group',
|
||||||
|
'platform.specific',
|
||||||
|
]
|
||||||
|
|
||||||
def get_supported_apis(self) -> list[str]:
|
def get_supported_apis(self) -> list[str]:
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -11,7 +11,16 @@ from langbot.libs.qq_official_api.api import QQOfficialClient
|
|||||||
from langbot.libs.qq_official_api.qqofficialevent import QQOfficialEvent
|
from langbot.libs.qq_official_api.qqofficialevent import QQOfficialEvent
|
||||||
from langbot.pkg.platform.adapters.qqofficial.api_impl import QQOfficialAPIMixin
|
from langbot.pkg.platform.adapters.qqofficial.api_impl import QQOfficialAPIMixin
|
||||||
from langbot.pkg.platform.adapters.qqofficial.errors import NotSupportedError
|
from langbot.pkg.platform.adapters.qqofficial.errors import NotSupportedError
|
||||||
from langbot.pkg.platform.adapters.qqofficial.event_converter import QQOfficialEventConverter
|
from langbot.pkg.platform.adapters.qqofficial.event_converter import (
|
||||||
|
BOT_INVITED_EVENT_TYPES,
|
||||||
|
BOT_REMOVED_EVENT_TYPES,
|
||||||
|
MEMBER_JOINED_EVENT_TYPES,
|
||||||
|
MEMBER_LEFT_EVENT_TYPES,
|
||||||
|
MESSAGE_EVENT_TYPES,
|
||||||
|
REACTION_ADD_EVENT_TYPES,
|
||||||
|
REACTION_REMOVE_EVENT_TYPES,
|
||||||
|
QQOfficialEventConverter,
|
||||||
|
)
|
||||||
from langbot.pkg.platform.adapters.qqofficial.message_converter import QQOfficialMessageConverter
|
from langbot.pkg.platform.adapters.qqofficial.message_converter import QQOfficialMessageConverter
|
||||||
from langbot.pkg.platform.adapters.qqofficial.platform_api import PLATFORM_API_MAP
|
from langbot.pkg.platform.adapters.qqofficial.platform_api import PLATFORM_API_MAP
|
||||||
import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter
|
import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter
|
||||||
@@ -89,6 +98,11 @@ class QQOfficialAdapter(QQOfficialAPIMixin, abstract_platform_adapter.AbstractPl
|
|||||||
def get_supported_events(self) -> list[str]:
|
def get_supported_events(self) -> list[str]:
|
||||||
return [
|
return [
|
||||||
'message.received',
|
'message.received',
|
||||||
|
'message.reaction',
|
||||||
|
'group.member_joined',
|
||||||
|
'group.member_left',
|
||||||
|
'bot.invited_to_group',
|
||||||
|
'bot.removed_from_group',
|
||||||
'platform.specific',
|
'platform.specific',
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -111,7 +125,9 @@ class QQOfficialAdapter(QQOfficialAPIMixin, abstract_platform_adapter.AbstractPl
|
|||||||
target_id: str,
|
target_id: str,
|
||||||
message: platform_message.MessageChain,
|
message: platform_message.MessageChain,
|
||||||
) -> platform_events.MessageResult:
|
) -> platform_events.MessageResult:
|
||||||
raw = await self._send_content_list(str(target_type), str(target_id), await QQOfficialMessageConverter.yiri2target(message))
|
raw = await self._send_content_list(
|
||||||
|
str(target_type), str(target_id), await QQOfficialMessageConverter.yiri2target(message)
|
||||||
|
)
|
||||||
return platform_events.MessageResult(raw={'results': raw})
|
return platform_events.MessageResult(raw={'results': raw})
|
||||||
|
|
||||||
async def reply_message(
|
async def reply_message(
|
||||||
@@ -212,8 +228,14 @@ class QQOfficialAdapter(QQOfficialAPIMixin, abstract_platform_adapter.AbstractPl
|
|||||||
is_final: bool = False,
|
is_final: bool = False,
|
||||||
):
|
):
|
||||||
await self._cleanup_stale_streams()
|
await self._cleanup_stale_streams()
|
||||||
chunk_text = '\n\n'.join(component.text for component in message if isinstance(component, platform_message.Plain))
|
chunk_text = '\n\n'.join(
|
||||||
message_id = bot_message.get('resp_message_id') if isinstance(bot_message, dict) else getattr(bot_message, 'resp_message_id', None)
|
component.text for component in message if isinstance(component, platform_message.Plain)
|
||||||
|
)
|
||||||
|
message_id = (
|
||||||
|
bot_message.get('resp_message_id')
|
||||||
|
if isinstance(bot_message, dict)
|
||||||
|
else getattr(bot_message, 'resp_message_id', None)
|
||||||
|
)
|
||||||
if not message_id or message_id not in self._stream_ctx:
|
if not message_id or message_id not in self._stream_ctx:
|
||||||
if chunk_text:
|
if chunk_text:
|
||||||
self._fallback_text[message_id] = self._fallback_text.get(message_id, '') + chunk_text
|
self._fallback_text[message_id] = self._fallback_text.get(message_id, '') + chunk_text
|
||||||
@@ -221,7 +243,11 @@ class QQOfficialAdapter(QQOfficialAPIMixin, abstract_platform_adapter.AbstractPl
|
|||||||
if is_final:
|
if is_final:
|
||||||
full_text = self._fallback_text.pop(message_id, '')
|
full_text = self._fallback_text.pop(message_id, '')
|
||||||
if full_text:
|
if full_text:
|
||||||
await self.reply_message(message_source, platform_message.MessageChain([platform_message.Plain(text=full_text)]), quote_origin)
|
await self.reply_message(
|
||||||
|
message_source,
|
||||||
|
platform_message.MessageChain([platform_message.Plain(text=full_text)]),
|
||||||
|
quote_origin,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
ctx = self._stream_ctx[message_id]
|
ctx = self._stream_ctx[message_id]
|
||||||
@@ -259,7 +285,15 @@ class QQOfficialAdapter(QQOfficialAPIMixin, abstract_platform_adapter.AbstractPl
|
|||||||
self._stream_ctx_ts.pop(message_id, None)
|
self._stream_ctx_ts.pop(message_id, None)
|
||||||
|
|
||||||
def _register_native_handlers(self):
|
def _register_native_handlers(self):
|
||||||
for event_type in ('C2C_MESSAGE_CREATE', 'DIRECT_MESSAGE_CREATE', 'GROUP_AT_MESSAGE_CREATE', 'AT_MESSAGE_CREATE'):
|
for event_type in (
|
||||||
|
MESSAGE_EVENT_TYPES
|
||||||
|
| REACTION_ADD_EVENT_TYPES
|
||||||
|
| REACTION_REMOVE_EVENT_TYPES
|
||||||
|
| MEMBER_JOINED_EVENT_TYPES
|
||||||
|
| MEMBER_LEFT_EVENT_TYPES
|
||||||
|
| BOT_INVITED_EVENT_TYPES
|
||||||
|
| BOT_REMOVED_EVENT_TYPES
|
||||||
|
):
|
||||||
self.bot.on_message(event_type)(self._handle_native_event)
|
self.bot.on_message(event_type)(self._handle_native_event)
|
||||||
|
|
||||||
async def _handle_native_event(self, event: QQOfficialEvent):
|
async def _handle_native_event(self, event: QQOfficialEvent):
|
||||||
@@ -305,12 +339,12 @@ class QQOfficialAdapter(QQOfficialAPIMixin, abstract_platform_adapter.AbstractPl
|
|||||||
await self.logger.info('QQ Official WebSocket connected and ready')
|
await self.logger.info('QQ Official WebSocket connected and ready')
|
||||||
|
|
||||||
async def on_event(event_type: str, event_data: dict):
|
async def on_event(event_type: str, event_data: dict):
|
||||||
if event_type not in {'C2C_MESSAGE_CREATE', 'DIRECT_MESSAGE_CREATE', 'GROUP_AT_MESSAGE_CREATE', 'AT_MESSAGE_CREATE'}:
|
|
||||||
await self._dispatch_eba_event(QQOfficialEventConverter.platform_specific(QQOfficialEvent({'t': event_type, **(event_data or {})}), f'qqofficial.{event_type}'))
|
|
||||||
return
|
|
||||||
if not isinstance(event_data, dict):
|
if not isinstance(event_data, dict):
|
||||||
await self.logger.warning(f'Event data is not dict, skipping: {event_type} -> {type(event_data)}')
|
await self.logger.warning(f'Event data is not dict, skipping: {event_type} -> {type(event_data)}')
|
||||||
return
|
return
|
||||||
|
if event_type not in MESSAGE_EVENT_TYPES:
|
||||||
|
await self._handle_native_event(QQOfficialEvent({'t': event_type, **event_data}))
|
||||||
|
return
|
||||||
payload = {'t': event_type, 'd': event_data}
|
payload = {'t': event_type, 'd': event_data}
|
||||||
message_data = await self.bot.get_message(payload)
|
message_data = await self.bot.get_message(payload)
|
||||||
if message_data:
|
if message_data:
|
||||||
@@ -337,7 +371,9 @@ class QQOfficialAdapter(QQOfficialAPIMixin, abstract_platform_adapter.AbstractPl
|
|||||||
return 'channel_private', event.guild_id
|
return 'channel_private', event.guild_id
|
||||||
raise NotSupportedError(f'reply_message:{event.t or "unknown_event"}')
|
raise NotSupportedError(f'reply_message:{event.t or "unknown_event"}')
|
||||||
|
|
||||||
async def _send_content_list(self, target_type: str, target_id: str, content_list: list[dict], msg_id: str | None = None) -> list[dict]:
|
async def _send_content_list(
|
||||||
|
self, target_type: str, target_id: str, content_list: list[dict], msg_id: str | None = None
|
||||||
|
) -> list[dict]:
|
||||||
target_type = self._normalize_target_type(target_type)
|
target_type = self._normalize_target_type(target_type)
|
||||||
results: list[dict] = []
|
results: list[dict] = []
|
||||||
for content in content_list:
|
for content in content_list:
|
||||||
@@ -361,10 +397,14 @@ class QQOfficialAdapter(QQOfficialAPIMixin, abstract_platform_adapter.AbstractPl
|
|||||||
raise NotSupportedError(f'send_message:{target_type}')
|
raise NotSupportedError(f'send_message:{target_type}')
|
||||||
results.append({'type': content_type, 'raw': raw})
|
results.append({'type': content_type, 'raw': raw})
|
||||||
elif content_type == 'image':
|
elif content_type == 'image':
|
||||||
raw = await self.bot.send_image_msg(target_type, target_id, file_url=content.get('url'), file_data=content.get('base64'), msg_id=msg_id)
|
raw = await self.bot.send_image_msg(
|
||||||
|
target_type, target_id, file_url=content.get('url'), file_data=content.get('base64'), msg_id=msg_id
|
||||||
|
)
|
||||||
results.append({'type': content_type, 'raw': raw})
|
results.append({'type': content_type, 'raw': raw})
|
||||||
elif content_type == 'voice':
|
elif content_type == 'voice':
|
||||||
raw = await self.bot.send_voice_msg(target_type, target_id, file_url=content.get('url'), file_data=content.get('base64'), msg_id=msg_id)
|
raw = await self.bot.send_voice_msg(
|
||||||
|
target_type, target_id, file_url=content.get('url'), file_data=content.get('base64'), msg_id=msg_id
|
||||||
|
)
|
||||||
results.append({'type': content_type, 'raw': raw})
|
results.append({'type': content_type, 'raw': raw})
|
||||||
elif content_type == 'file':
|
elif content_type == 'file':
|
||||||
raw = await self.bot.send_file_msg(
|
raw = await self.bot.send_file_msg(
|
||||||
|
|||||||
@@ -12,12 +12,28 @@ from langbot_plugin.api.entities.builtin.platform import entities as platform_en
|
|||||||
from langbot_plugin.api.entities.builtin.platform import events as platform_events
|
from langbot_plugin.api.entities.builtin.platform import events as platform_events
|
||||||
|
|
||||||
|
|
||||||
|
MESSAGE_EVENT_TYPES = {
|
||||||
|
'C2C_MESSAGE_CREATE',
|
||||||
|
'DIRECT_MESSAGE_CREATE',
|
||||||
|
'GROUP_AT_MESSAGE_CREATE',
|
||||||
|
'AT_MESSAGE_CREATE',
|
||||||
|
}
|
||||||
|
REACTION_ADD_EVENT_TYPES = {'MESSAGE_REACTION_ADD'}
|
||||||
|
REACTION_REMOVE_EVENT_TYPES = {'MESSAGE_REACTION_REMOVE'}
|
||||||
|
MEMBER_JOINED_EVENT_TYPES = {'GUILD_MEMBER_ADD', 'GROUP_MEMBER_ADD'}
|
||||||
|
MEMBER_LEFT_EVENT_TYPES = {'GUILD_MEMBER_REMOVE', 'GROUP_MEMBER_REMOVE'}
|
||||||
|
BOT_INVITED_EVENT_TYPES = {'GUILD_CREATE', 'GROUP_ADD_ROBOT'}
|
||||||
|
BOT_REMOVED_EVENT_TYPES = {'GUILD_DELETE', 'GROUP_DEL_ROBOT'}
|
||||||
|
|
||||||
|
|
||||||
class QQOfficialEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
class QQOfficialEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def yiri2target(event: platform_events.Event) -> typing.Any:
|
async def yiri2target(event: platform_events.Event) -> typing.Any:
|
||||||
return getattr(event, 'source_platform_object', None)
|
return getattr(event, 'source_platform_object', None)
|
||||||
|
|
||||||
async def target2legacy(self, event: QQOfficialEvent) -> platform_events.FriendMessage | platform_events.GroupMessage | None:
|
async def target2legacy(
|
||||||
|
self, event: QQOfficialEvent
|
||||||
|
) -> platform_events.FriendMessage | platform_events.GroupMessage | None:
|
||||||
eba_event = await self.target2yiri(event)
|
eba_event = await self.target2yiri(event)
|
||||||
if not isinstance(eba_event, platform_events.MessageReceivedEvent):
|
if not isinstance(eba_event, platform_events.MessageReceivedEvent):
|
||||||
return None
|
return None
|
||||||
@@ -50,8 +66,18 @@ class QQOfficialEventConverter(abstract_platform_adapter.AbstractEventConverter)
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def target2yiri(self, event: QQOfficialEvent) -> platform_events.Event:
|
async def target2yiri(self, event: QQOfficialEvent) -> platform_events.Event:
|
||||||
if event.t in {'C2C_MESSAGE_CREATE', 'DIRECT_MESSAGE_CREATE', 'GROUP_AT_MESSAGE_CREATE', 'AT_MESSAGE_CREATE'}:
|
if event.t in MESSAGE_EVENT_TYPES:
|
||||||
return await self.message_to_eba(event)
|
return await self.message_to_eba(event)
|
||||||
|
if event.t in REACTION_ADD_EVENT_TYPES | REACTION_REMOVE_EVENT_TYPES:
|
||||||
|
return self.reaction_to_eba(event, event.t in REACTION_ADD_EVENT_TYPES)
|
||||||
|
if event.t in MEMBER_JOINED_EVENT_TYPES:
|
||||||
|
return self.member_joined_to_eba(event)
|
||||||
|
if event.t in MEMBER_LEFT_EVENT_TYPES:
|
||||||
|
return self.member_left_to_eba(event)
|
||||||
|
if event.t in BOT_INVITED_EVENT_TYPES:
|
||||||
|
return self.bot_invited_to_eba(event)
|
||||||
|
if event.t in BOT_REMOVED_EVENT_TYPES:
|
||||||
|
return self.bot_removed_to_eba(event)
|
||||||
return self.platform_specific(event, f'qqofficial.{event.t or "unknown"}')
|
return self.platform_specific(event, f'qqofficial.{event.t or "unknown"}')
|
||||||
|
|
||||||
async def message_to_eba(self, event: QQOfficialEvent) -> platform_events.MessageReceivedEvent:
|
async def message_to_eba(self, event: QQOfficialEvent) -> platform_events.MessageReceivedEvent:
|
||||||
@@ -82,6 +108,100 @@ class QQOfficialEventConverter(abstract_platform_adapter.AbstractEventConverter)
|
|||||||
source_platform_object=event,
|
source_platform_object=event,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def reaction_to_eba(self, event: QQOfficialEvent, is_add: bool) -> platform_events.MessageReactionEvent:
|
||||||
|
chat_type, chat_id, group = self._chat_from_event(event)
|
||||||
|
return platform_events.MessageReactionEvent(
|
||||||
|
type='message.reaction',
|
||||||
|
adapter_name=ADAPTER_NAME,
|
||||||
|
message_id=_event_value(event, 'message_id', 'msg_id', 'target_id', 'id'),
|
||||||
|
user=platform_entities.User(
|
||||||
|
id=_event_value(event, 'user_openid', 'member_openid', 'openid', 'user_id'),
|
||||||
|
nickname=_event_value(event, 'username', 'nick', 'nickname', 'user_openid', 'openid'),
|
||||||
|
),
|
||||||
|
reaction=_event_value(event, 'emoji', 'emoji_id', 'reaction', 'reaction_id', 'type'),
|
||||||
|
is_add=is_add,
|
||||||
|
chat_type=chat_type,
|
||||||
|
chat_id=chat_id,
|
||||||
|
group=group,
|
||||||
|
timestamp=_timestamp_value(event.timestamp),
|
||||||
|
source_platform_object=event,
|
||||||
|
)
|
||||||
|
|
||||||
|
def member_joined_to_eba(self, event: QQOfficialEvent) -> platform_events.MemberJoinedEvent:
|
||||||
|
group = self._group_from_event(event)
|
||||||
|
member = self._user_from_event(event, 'openid', 'member_openid', 'user_openid', 'user_id')
|
||||||
|
inviter_id = _event_value(event, 'inviter_openid', 'operator_openid', 'op_user_id')
|
||||||
|
return platform_events.MemberJoinedEvent(
|
||||||
|
type='group.member_joined',
|
||||||
|
adapter_name=ADAPTER_NAME,
|
||||||
|
group=group,
|
||||||
|
member=member,
|
||||||
|
inviter=platform_entities.User(id=inviter_id, nickname=inviter_id) if inviter_id else None,
|
||||||
|
timestamp=_timestamp_value(event.timestamp),
|
||||||
|
source_platform_object=event,
|
||||||
|
)
|
||||||
|
|
||||||
|
def member_left_to_eba(self, event: QQOfficialEvent) -> platform_events.MemberLeftEvent:
|
||||||
|
group = self._group_from_event(event)
|
||||||
|
member = self._user_from_event(event, 'openid', 'member_openid', 'user_openid', 'user_id')
|
||||||
|
operator_id = _event_value(event, 'operator_openid', 'op_user_id', 'user_openid')
|
||||||
|
return platform_events.MemberLeftEvent(
|
||||||
|
type='group.member_left',
|
||||||
|
adapter_name=ADAPTER_NAME,
|
||||||
|
group=group,
|
||||||
|
member=member,
|
||||||
|
operator=platform_entities.User(id=operator_id, nickname=operator_id) if operator_id else None,
|
||||||
|
is_kicked=event.t in {'GROUP_MEMBER_REMOVE', 'GUILD_MEMBER_REMOVE'},
|
||||||
|
timestamp=_timestamp_value(event.timestamp),
|
||||||
|
source_platform_object=event,
|
||||||
|
)
|
||||||
|
|
||||||
|
def bot_invited_to_eba(self, event: QQOfficialEvent) -> platform_events.BotInvitedToGroupEvent:
|
||||||
|
group = self._group_from_event(event)
|
||||||
|
inviter_id = _event_value(event, 'op_user_id', 'operator_openid', 'user_openid', 'member_openid', 'openid')
|
||||||
|
return platform_events.BotInvitedToGroupEvent(
|
||||||
|
type='bot.invited_to_group',
|
||||||
|
adapter_name=ADAPTER_NAME,
|
||||||
|
group=group,
|
||||||
|
inviter=platform_entities.User(id=inviter_id, nickname=inviter_id) if inviter_id else None,
|
||||||
|
request_id=_event_value(event, 'event_id', 'id'),
|
||||||
|
timestamp=_timestamp_value(event.timestamp),
|
||||||
|
source_platform_object=event,
|
||||||
|
)
|
||||||
|
|
||||||
|
def bot_removed_to_eba(self, event: QQOfficialEvent) -> platform_events.BotRemovedFromGroupEvent:
|
||||||
|
group = self._group_from_event(event)
|
||||||
|
operator_id = _event_value(event, 'op_user_id', 'operator_openid', 'user_openid', 'member_openid', 'openid')
|
||||||
|
return platform_events.BotRemovedFromGroupEvent(
|
||||||
|
type='bot.removed_from_group',
|
||||||
|
adapter_name=ADAPTER_NAME,
|
||||||
|
group=group,
|
||||||
|
operator=platform_entities.User(id=operator_id, nickname=operator_id) if operator_id else None,
|
||||||
|
timestamp=_timestamp_value(event.timestamp),
|
||||||
|
source_platform_object=event,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _chat_from_event(
|
||||||
|
self, event: QQOfficialEvent
|
||||||
|
) -> tuple[platform_entities.ChatType, str, platform_entities.UserGroup | None]:
|
||||||
|
group_id = _event_value(event, 'group_openid', 'guild_id', 'channel_id', 'group_id')
|
||||||
|
if group_id:
|
||||||
|
return platform_entities.ChatType.GROUP, group_id, self._group_from_event(event)
|
||||||
|
private_id = _event_value(event, 'user_openid', 'member_openid', 'openid', 'user_id')
|
||||||
|
return platform_entities.ChatType.PRIVATE, private_id, None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _group_from_event(event: QQOfficialEvent) -> platform_entities.UserGroup:
|
||||||
|
group_id = _event_value(event, 'group_openid', 'guild_id', 'channel_id', 'group_id')
|
||||||
|
group_name = _event_value(event, 'group_name', 'guild_name', 'channel_name', 'name') or group_id
|
||||||
|
return platform_entities.UserGroup(id=group_id, name=group_name)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _user_from_event(event: QQOfficialEvent, *keys: str) -> platform_entities.User:
|
||||||
|
user_id = _event_value(event, *keys)
|
||||||
|
nickname = _event_value(event, 'username', 'nick', 'nickname') or user_id
|
||||||
|
return platform_entities.User(id=user_id, nickname=nickname)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _sender_id(event: QQOfficialEvent) -> str:
|
def _sender_id(event: QQOfficialEvent) -> str:
|
||||||
member_openid = event.member_openid or event.get('member_openid', '')
|
member_openid = event.member_openid or event.get('member_openid', '')
|
||||||
@@ -107,6 +227,17 @@ class QQOfficialEventConverter(abstract_platform_adapter.AbstractEventConverter)
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _event_value(event: QQOfficialEvent, *keys: str) -> str:
|
||||||
|
nested = event.get('d') if isinstance(event.get('d'), dict) else {}
|
||||||
|
for key in keys:
|
||||||
|
value = event.get(key)
|
||||||
|
if value in (None, '', {}):
|
||||||
|
value = nested.get(key)
|
||||||
|
if value not in (None, '', {}):
|
||||||
|
return str(value)
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
def _timestamp_value(value: str) -> float:
|
def _timestamp_value(value: str) -> float:
|
||||||
if not value:
|
if not value:
|
||||||
return time.time()
|
return time.time()
|
||||||
|
|||||||
@@ -88,6 +88,11 @@ spec:
|
|||||||
|
|
||||||
supported_events:
|
supported_events:
|
||||||
- message.received
|
- message.received
|
||||||
|
- message.reaction
|
||||||
|
- group.member_joined
|
||||||
|
- group.member_left
|
||||||
|
- bot.invited_to_group
|
||||||
|
- bot.removed_from_group
|
||||||
- platform.specific
|
- platform.specific
|
||||||
|
|
||||||
supported_apis:
|
supported_apis:
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ class DummyLogger(AbstractEventLogger):
|
|||||||
class DummyDingTalkClient(DingTalkClient):
|
class DummyDingTalkClient(DingTalkClient):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self._message_handlers = {}
|
self._message_handlers = {}
|
||||||
|
self.client = SimpleNamespace(register_callback_handler=AsyncMock())
|
||||||
self.markdown_card = kwargs.get('markdown_card', True)
|
self.markdown_card = kwargs.get('markdown_card', True)
|
||||||
self.access_token = ''
|
self.access_token = ''
|
||||||
self.send_message = AsyncMock()
|
self.send_message = AsyncMock()
|
||||||
@@ -115,6 +116,30 @@ def dingtalk_event(conversation='GroupMessage', **overrides) -> DingTalkEvent:
|
|||||||
return DingTalkEvent.from_payload(payload)
|
return DingTalkEvent.from_payload(payload)
|
||||||
|
|
||||||
|
|
||||||
|
def dingtalk_card_callback_event(**overrides) -> DingTalkEvent:
|
||||||
|
return DingTalkEvent.from_payload(
|
||||||
|
{
|
||||||
|
'conversation_type': 'CardCallback',
|
||||||
|
'Type': 'card_callback',
|
||||||
|
'CardCallback': {
|
||||||
|
'extension': overrides.get('extension', {}),
|
||||||
|
'corp_id': overrides.get('corp_id', 'corp-1'),
|
||||||
|
'user_id': overrides.get('user_id', 'user-1'),
|
||||||
|
'content': overrides.get(
|
||||||
|
'content',
|
||||||
|
{
|
||||||
|
'action': 'like',
|
||||||
|
'feedback_id': 'feedback-1',
|
||||||
|
'message_id': 'bot-msg-1',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
'space_id': overrides.get('space_id', 'space-1'),
|
||||||
|
'card_instance_id': overrides.get('card_instance_id', 'card-1'),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_dingtalk_supported_events_match_manifest():
|
def test_dingtalk_supported_events_match_manifest():
|
||||||
assert make_adapter().get_supported_events() == manifest()['spec']['supported_events']
|
assert make_adapter().get_supported_events() == manifest()['spec']['supported_events']
|
||||||
|
|
||||||
@@ -195,7 +220,7 @@ async def test_dingtalk_event_converter_maps_group_and_private_message():
|
|||||||
group_event = await DingTalkEventConverter.target2yiri(dingtalk_event(), 'LangBot')
|
group_event = await DingTalkEventConverter.target2yiri(dingtalk_event(), 'LangBot')
|
||||||
|
|
||||||
assert isinstance(group_event, platform_events.MessageReceivedEvent)
|
assert isinstance(group_event, platform_events.MessageReceivedEvent)
|
||||||
assert group_event.adapter_name == 'dingtalk'
|
assert group_event.adapter_name == 'dingtalk-eba'
|
||||||
assert group_event.chat_type == platform_entities.ChatType.GROUP
|
assert group_event.chat_type == platform_entities.ChatType.GROUP
|
||||||
assert group_event.chat_id == 'group-1'
|
assert group_event.chat_id == 'group-1'
|
||||||
assert group_event.group.name == 'LangBot Team'
|
assert group_event.group.name == 'LangBot Team'
|
||||||
@@ -212,6 +237,42 @@ async def test_dingtalk_event_converter_maps_group_and_private_message():
|
|||||||
assert private_event.group is None
|
assert private_event.group is None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_dingtalk_event_converter_maps_card_feedback():
|
||||||
|
feedback = await DingTalkEventConverter.target2yiri(dingtalk_card_callback_event(), 'LangBot')
|
||||||
|
callback = await DingTalkEventConverter.target2yiri(
|
||||||
|
dingtalk_card_callback_event(content={'action': 'open_details'}),
|
||||||
|
'LangBot',
|
||||||
|
)
|
||||||
|
|
||||||
|
assert isinstance(feedback, platform_events.FeedbackReceivedEvent)
|
||||||
|
assert feedback.adapter_name == 'dingtalk-eba'
|
||||||
|
assert feedback.feedback_id == 'feedback-1'
|
||||||
|
assert feedback.feedback_type == 1
|
||||||
|
assert feedback.user_id == 'user-1'
|
||||||
|
assert feedback.session_id == 'space-1'
|
||||||
|
assert feedback.message_id == 'bot-msg-1'
|
||||||
|
|
||||||
|
assert isinstance(callback, platform_events.PlatformSpecificEvent)
|
||||||
|
assert callback.action == 'card.callback'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_dingtalk_adapter_dispatches_card_feedback_event():
|
||||||
|
adapter = make_adapter()
|
||||||
|
calls: list[platform_events.Event] = []
|
||||||
|
|
||||||
|
async def listener(event, adapter):
|
||||||
|
calls.append(event)
|
||||||
|
|
||||||
|
adapter.register_listener(platform_events.FeedbackReceivedEvent, listener)
|
||||||
|
await adapter._handle_native_event(dingtalk_card_callback_event())
|
||||||
|
|
||||||
|
assert len(calls) == 1
|
||||||
|
assert isinstance(calls[0], platform_events.FeedbackReceivedEvent)
|
||||||
|
assert calls[0].feedback_type == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_dingtalk_adapter_dispatches_and_caches_message_event():
|
async def test_dingtalk_adapter_dispatches_and_caches_message_event():
|
||||||
adapter = make_adapter()
|
adapter = make_adapter()
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ def qq_event(event_type: str = 'C2C_MESSAGE_CREATE', **overrides) -> QQOfficialE
|
|||||||
'image_attachments': overrides.get('image_attachments'),
|
'image_attachments': overrides.get('image_attachments'),
|
||||||
'content_type': overrides.get('content_type', 'image/png'),
|
'content_type': overrides.get('content_type', 'image/png'),
|
||||||
}
|
}
|
||||||
|
payload.update(overrides.get('extra', {}))
|
||||||
return QQOfficialEvent(payload)
|
return QQOfficialEvent(payload)
|
||||||
|
|
||||||
|
|
||||||
@@ -194,7 +195,7 @@ async def test_qqofficial_event_converter_maps_private_group_and_platform_specif
|
|||||||
private_event = await QQOfficialEventConverter().target2yiri(qq_event('C2C_MESSAGE_CREATE'))
|
private_event = await QQOfficialEventConverter().target2yiri(qq_event('C2C_MESSAGE_CREATE'))
|
||||||
group_event = await QQOfficialEventConverter().target2yiri(qq_event('GROUP_AT_MESSAGE_CREATE'))
|
group_event = await QQOfficialEventConverter().target2yiri(qq_event('GROUP_AT_MESSAGE_CREATE'))
|
||||||
channel_event = await QQOfficialEventConverter().target2yiri(qq_event('AT_MESSAGE_CREATE'))
|
channel_event = await QQOfficialEventConverter().target2yiri(qq_event('AT_MESSAGE_CREATE'))
|
||||||
platform_event = await QQOfficialEventConverter().target2yiri(qq_event('GUILD_CREATE'))
|
platform_event = await QQOfficialEventConverter().target2yiri(qq_event('UNKNOWN_EVENT'))
|
||||||
|
|
||||||
assert isinstance(private_event, platform_events.MessageReceivedEvent)
|
assert isinstance(private_event, platform_events.MessageReceivedEvent)
|
||||||
assert private_event.adapter_name == 'qqofficial-eba'
|
assert private_event.adapter_name == 'qqofficial-eba'
|
||||||
@@ -209,7 +210,67 @@ async def test_qqofficial_event_converter_maps_private_group_and_platform_specif
|
|||||||
|
|
||||||
assert channel_event.chat_id == 'channel-1'
|
assert channel_event.chat_id == 'channel-1'
|
||||||
assert isinstance(platform_event, platform_events.PlatformSpecificEvent)
|
assert isinstance(platform_event, platform_events.PlatformSpecificEvent)
|
||||||
assert platform_event.action == 'qqofficial.GUILD_CREATE'
|
assert platform_event.action == 'qqofficial.UNKNOWN_EVENT'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_qqofficial_event_converter_maps_declared_lifecycle_events():
|
||||||
|
converter = QQOfficialEventConverter()
|
||||||
|
|
||||||
|
reaction_add = await converter.target2yiri(
|
||||||
|
qq_event(
|
||||||
|
'MESSAGE_REACTION_ADD',
|
||||||
|
extra={
|
||||||
|
'message_id': 'reacted-msg',
|
||||||
|
'emoji': 'thumbs_up',
|
||||||
|
'group_openid': 'group-openid',
|
||||||
|
'openid': 'reactor-openid',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
reaction_remove = await converter.target2yiri(
|
||||||
|
qq_event('MESSAGE_REACTION_REMOVE', extra={'message_id': 'reacted-msg', 'emoji_id': '1'})
|
||||||
|
)
|
||||||
|
member_joined = await converter.target2yiri(
|
||||||
|
qq_event('GROUP_MEMBER_ADD', extra={'openid': 'new-member', 'group_openid': 'group-openid'})
|
||||||
|
)
|
||||||
|
member_left = await converter.target2yiri(
|
||||||
|
qq_event('GROUP_MEMBER_REMOVE', extra={'openid': 'old-member', 'operator_openid': 'operator-openid'})
|
||||||
|
)
|
||||||
|
bot_invited = await converter.target2yiri(
|
||||||
|
qq_event('GROUP_ADD_ROBOT', extra={'group_openid': 'group-openid', 'op_user_id': 'inviter-openid'})
|
||||||
|
)
|
||||||
|
bot_removed = await converter.target2yiri(
|
||||||
|
qq_event('GROUP_DEL_ROBOT', extra={'group_openid': 'group-openid', 'op_user_id': 'operator-openid'})
|
||||||
|
)
|
||||||
|
|
||||||
|
assert isinstance(reaction_add, platform_events.MessageReactionEvent)
|
||||||
|
assert reaction_add.type == 'message.reaction'
|
||||||
|
assert reaction_add.is_add is True
|
||||||
|
assert reaction_add.message_id == 'reacted-msg'
|
||||||
|
assert reaction_add.reaction == 'thumbs_up'
|
||||||
|
assert reaction_add.chat_type == platform_entities.ChatType.GROUP
|
||||||
|
|
||||||
|
assert isinstance(reaction_remove, platform_events.MessageReactionEvent)
|
||||||
|
assert reaction_remove.is_add is False
|
||||||
|
|
||||||
|
assert isinstance(member_joined, platform_events.MemberJoinedEvent)
|
||||||
|
assert member_joined.type == 'group.member_joined'
|
||||||
|
assert member_joined.group.id == 'group-openid'
|
||||||
|
assert member_joined.member.id == 'new-member'
|
||||||
|
|
||||||
|
assert isinstance(member_left, platform_events.MemberLeftEvent)
|
||||||
|
assert member_left.type == 'group.member_left'
|
||||||
|
assert member_left.member.id == 'old-member'
|
||||||
|
assert member_left.operator.id == 'operator-openid'
|
||||||
|
|
||||||
|
assert isinstance(bot_invited, platform_events.BotInvitedToGroupEvent)
|
||||||
|
assert bot_invited.type == 'bot.invited_to_group'
|
||||||
|
assert bot_invited.inviter.id == 'inviter-openid'
|
||||||
|
|
||||||
|
assert isinstance(bot_removed, platform_events.BotRemovedFromGroupEvent)
|
||||||
|
assert bot_removed.type == 'bot.removed_from_group'
|
||||||
|
assert bot_removed.operator.id == 'operator-openid'
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -236,6 +297,24 @@ async def test_qqofficial_adapter_dispatches_eba_and_legacy_and_caches_group_eve
|
|||||||
assert (await adapter.get_group_member_info('group-openid', 'member-openid')).user.id == 'member-openid'
|
assert (await adapter.get_group_member_info('group-openid', 'member-openid')).user.id == 'member-openid'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_qqofficial_adapter_dispatches_non_message_eba_events():
|
||||||
|
adapter = make_adapter()
|
||||||
|
calls: list[platform_events.Event] = []
|
||||||
|
|
||||||
|
async def member_listener(event, adapter):
|
||||||
|
calls.append(event)
|
||||||
|
|
||||||
|
adapter.register_listener(platform_events.MemberJoinedEvent, member_listener)
|
||||||
|
await adapter._handle_native_event(
|
||||||
|
qq_event('GROUP_MEMBER_ADD', extra={'openid': 'new-member', 'group_openid': 'group-openid'})
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(calls) == 1
|
||||||
|
assert isinstance(calls[0], platform_events.MemberJoinedEvent)
|
||||||
|
assert calls[0].member.id == 'new-member'
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_qqofficial_send_reply_stream_platform_api_and_unsupported():
|
async def test_qqofficial_send_reply_stream_platform_api_and_unsupported():
|
||||||
adapter = make_adapter()
|
adapter = make_adapter()
|
||||||
@@ -252,7 +331,9 @@ async def test_qqofficial_send_reply_stream_platform_api_and_unsupported():
|
|||||||
assert ('private_text', 'user-openid', 'reply', 'msg-1') in adapter.bot.sent
|
assert ('private_text', 'user-openid', 'reply', 'msg-1') in adapter.bot.sent
|
||||||
assert any(call[0] == 'image' and call[1] == 'c2c' for call in adapter.bot.sent)
|
assert any(call[0] == 'image' and call[1] == 'c2c' for call in adapter.bot.sent)
|
||||||
|
|
||||||
await adapter.send_message('group', 'group-openid', platform_message.MessageChain([platform_message.Plain(text='hello group')]))
|
await adapter.send_message(
|
||||||
|
'group', 'group-openid', platform_message.MessageChain([platform_message.Plain(text='hello group')])
|
||||||
|
)
|
||||||
assert ('group_text', 'group-openid', 'hello group', None) in adapter.bot.sent
|
assert ('group_text', 'group-openid', 'hello group', None) in adapter.bot.sent
|
||||||
|
|
||||||
assert await adapter.call_platform_api('get_mode', {}) == {
|
assert await adapter.call_platform_api('get_mode', {}) == {
|
||||||
|
|||||||
Reference in New Issue
Block a user