mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-08 14:56:03 +00:00
276 lines
9.5 KiB
Python
276 lines
9.5 KiB
Python
from __future__ import annotations
|
|
|
|
import pathlib
|
|
from unittest.mock import AsyncMock
|
|
|
|
import pytest
|
|
import yaml
|
|
|
|
from langbot.pkg.platform.adapters.kook.adapter import KookAdapter
|
|
from langbot.pkg.platform.adapters.kook.event_converter import KookEventConverter
|
|
from langbot.pkg.platform.adapters.kook.message_converter import KookMessageConverter
|
|
from langbot.pkg.platform.adapters.kook.platform_api import PLATFORM_API_MAP, get_gateway
|
|
from langbot_plugin.api.definition.abstract.platform.event_logger import AbstractEventLogger
|
|
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
|
|
|
|
|
|
class DummyLogger(AbstractEventLogger):
|
|
async def info(self, text, images=None, message_session_id=None, no_throw=True):
|
|
pass
|
|
|
|
async def debug(self, text, images=None, message_session_id=None, no_throw=True):
|
|
pass
|
|
|
|
async def warning(self, text, images=None, message_session_id=None, no_throw=True):
|
|
pass
|
|
|
|
async def error(self, text, images=None, message_session_id=None, no_throw=True):
|
|
pass
|
|
|
|
|
|
def make_adapter() -> KookAdapter:
|
|
return KookAdapter({'token': 'fake', 'enable-stream-reply': False}, DummyLogger())
|
|
|
|
|
|
def manifest() -> dict:
|
|
manifest_path = (
|
|
pathlib.Path(__file__).parents[3]
|
|
/ 'src'
|
|
/ 'langbot'
|
|
/ 'pkg'
|
|
/ 'platform'
|
|
/ 'adapters'
|
|
/ 'kook'
|
|
/ 'manifest.yaml'
|
|
)
|
|
return yaml.safe_load(manifest_path.read_text())
|
|
|
|
|
|
def fake_kook_message(**overrides):
|
|
event = {
|
|
'channel_type': 'GROUP',
|
|
'type': 9,
|
|
'author_id': 'u1',
|
|
'target_id': 'c1',
|
|
'msg_id': 'm1',
|
|
'msg_timestamp': 1_775_000_000_000,
|
|
'content': 'hi (met)u2(met) and (met)all(met)',
|
|
'extra': {
|
|
'channel_name': 'general',
|
|
'guild_id': 'g1',
|
|
'guild_name': 'Guild',
|
|
'author': {
|
|
'id': 'u1',
|
|
'username': 'alice',
|
|
'nickname': 'Alice',
|
|
'avatar': 'https://example/avatar.png',
|
|
},
|
|
'mention': ['u2'],
|
|
'mention_all': True,
|
|
},
|
|
}
|
|
event.update(overrides)
|
|
return event
|
|
|
|
|
|
def test_kook_supported_events_match_manifest():
|
|
assert make_adapter().get_supported_events() == manifest()['spec']['supported_events']
|
|
|
|
|
|
def test_kook_supported_apis_match_manifest():
|
|
supported_apis = make_adapter().get_supported_apis()
|
|
manifest_apis = manifest()['spec']['supported_apis']
|
|
|
|
assert supported_apis == manifest_apis['required'] + manifest_apis['optional']
|
|
|
|
|
|
def test_kook_platform_api_map_matches_manifest():
|
|
manifest_actions = {item['action'] for item in manifest()['spec']['platform_specific_apis']}
|
|
|
|
assert set(PLATFORM_API_MAP) == manifest_actions
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_kook_adapter_dispatches_most_specific_eba_listener():
|
|
adapter = make_adapter()
|
|
calls: list[str] = []
|
|
|
|
async def event_listener(event, adapter):
|
|
calls.append('event')
|
|
|
|
async def eba_listener(event, adapter):
|
|
calls.append('eba')
|
|
|
|
async def message_listener(event, adapter):
|
|
calls.append('message')
|
|
|
|
adapter.register_listener(platform_events.Event, event_listener)
|
|
adapter.register_listener(platform_events.EBAEvent, eba_listener)
|
|
adapter.register_listener(platform_events.MessageReceivedEvent, message_listener)
|
|
|
|
event = platform_events.MessageReceivedEvent(
|
|
message_id='m1',
|
|
message_chain=platform_message.MessageChain([platform_message.Plain(text='hello')]),
|
|
sender=platform_entities.User(id='u1'),
|
|
chat_id='c1',
|
|
)
|
|
|
|
await adapter._dispatch_eba_event(event)
|
|
|
|
assert calls == ['message']
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_kook_message_converter_maps_target_text_mentions_and_source():
|
|
chain = await KookMessageConverter.target2yiri(fake_kook_message(), bot_account_id='bot')
|
|
|
|
assert isinstance(chain[0], platform_message.Source)
|
|
assert chain[0].id == 'm1'
|
|
assert isinstance(chain[1], platform_message.Plain)
|
|
assert chain[1].text == 'hi '
|
|
assert isinstance(chain[2], platform_message.At)
|
|
assert chain[2].target == 'u2'
|
|
assert isinstance(chain[4], platform_message.AtAll)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_kook_message_converter_maps_media_components():
|
|
image = await KookMessageConverter.target2yiri(fake_kook_message(type=2, content='https://example/image.png'))
|
|
assert isinstance(image[1], platform_message.Image)
|
|
assert image[1].url == 'https://example/image.png'
|
|
|
|
file_chain = await KookMessageConverter.target2yiri(
|
|
fake_kook_message(type=4, content='https://example/file.bin', extra={'attachments': {'name': 'file.bin'}})
|
|
)
|
|
assert isinstance(file_chain[1], platform_message.File)
|
|
assert file_chain[1].name == 'file.bin'
|
|
|
|
voice = await KookMessageConverter.target2yiri(fake_kook_message(type=8, content='https://example/voice.mp3'))
|
|
assert isinstance(voice[1], platform_message.Voice)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_kook_message_converter_maps_common_components_to_target():
|
|
content, msg_type = await KookMessageConverter.yiri2target(
|
|
platform_message.MessageChain(
|
|
[
|
|
platform_message.Plain(text='hi '),
|
|
platform_message.At(target='u1'),
|
|
platform_message.Plain(text=' all '),
|
|
platform_message.AtAll(),
|
|
]
|
|
)
|
|
)
|
|
|
|
assert content == 'hi (met)u1(met) all (met)all(met)'
|
|
assert msg_type == 1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_kook_event_converter_maps_group_private_and_platform_specific_events():
|
|
group_event = await KookEventConverter.target2yiri(fake_kook_message(), bot_account_id='bot')
|
|
assert isinstance(group_event, platform_events.MessageReceivedEvent)
|
|
assert group_event.type == 'message.received'
|
|
assert group_event.adapter_name == 'kook'
|
|
assert group_event.chat_type == platform_entities.ChatType.GROUP
|
|
assert group_event.chat_id == 'c1'
|
|
assert group_event.group.id == 'c1'
|
|
assert group_event.sender.id == 'u1'
|
|
|
|
private_event = await KookEventConverter.target2yiri(
|
|
fake_kook_message(channel_type='PERSON', target_id='u1', extra={'code': 'chat-code'}),
|
|
bot_account_id='bot',
|
|
)
|
|
assert private_event.chat_type == platform_entities.ChatType.PRIVATE
|
|
assert private_event.chat_id == 'chat-code'
|
|
assert private_event.group is None
|
|
|
|
specific = await KookEventConverter.target2yiri({'type': 255, 'target_id': 'raw'}, bot_account_id='bot')
|
|
assert isinstance(specific, platform_events.PlatformSpecificEvent)
|
|
assert specific.action == '255'
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_kook_event_converter_maps_legacy_events():
|
|
legacy_group = await KookEventConverter.target2legacy(fake_kook_message(), bot_account_id='bot')
|
|
assert isinstance(legacy_group, platform_events.GroupMessage)
|
|
assert legacy_group.sender.group.id == 'c1'
|
|
|
|
legacy_private = await KookEventConverter.target2legacy(
|
|
fake_kook_message(channel_type='PERSON', target_id='u1'),
|
|
bot_account_id='bot',
|
|
)
|
|
assert isinstance(legacy_private, platform_events.FriendMessage)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_kook_send_and_reply_pass_expected_payloads():
|
|
adapter = make_adapter()
|
|
adapter._request = AsyncMock(return_value={'code': 0, 'data': {'msg_id': 'sent'}})
|
|
|
|
result = await adapter.send_message(
|
|
'group',
|
|
'c1',
|
|
platform_message.MessageChain([platform_message.Plain(text='hello')]),
|
|
)
|
|
|
|
assert result.message_id == 'sent'
|
|
adapter._request.assert_awaited_with(
|
|
'POST',
|
|
'/message/create',
|
|
json={'target_id': 'c1', 'content': 'hello', 'type': 1},
|
|
)
|
|
|
|
source = await KookEventConverter.target2yiri(fake_kook_message(), bot_account_id='bot')
|
|
await adapter.reply_message(source, platform_message.MessageChain([platform_message.Plain(text='reply')]), True)
|
|
|
|
assert adapter._request.await_args_list[-1].args == ('POST', '/message/create')
|
|
payload = adapter._request.await_args_list[-1].kwargs['json']
|
|
assert payload['reply_msg_id'] == 'm1'
|
|
assert payload['quote'] == 'm1'
|
|
assert payload['content'] == 'reply'
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_kook_get_gateway_redacts_token_in_platform_api_result():
|
|
adapter = make_adapter()
|
|
adapter._request = AsyncMock(
|
|
return_value={
|
|
'code': 0,
|
|
'data': {
|
|
'url': 'wss://example.invalid/gateway?compress=1&token=secret-token',
|
|
},
|
|
}
|
|
)
|
|
|
|
result = await get_gateway(adapter, {'compress': 1})
|
|
|
|
assert result['data']['url'] == 'wss://example.invalid/gateway?compress=1&token=%3Credacted%3E'
|
|
assert 'secret-token' not in result['data']['url']
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_kook_handle_event_dispatches_eba_and_legacy_then_caches():
|
|
adapter = make_adapter()
|
|
adapter.bot_account_id = 'bot'
|
|
calls: list[str] = []
|
|
|
|
async def legacy_listener(event, adapter):
|
|
calls.append(type(event).__name__)
|
|
|
|
async def eba_listener(event, adapter):
|
|
calls.append(event.type)
|
|
|
|
adapter.register_listener(platform_events.GroupMessage, legacy_listener)
|
|
adapter.register_listener(platform_events.MessageReceivedEvent, eba_listener)
|
|
|
|
await adapter._handle_event(fake_kook_message(), 7)
|
|
|
|
assert calls == ['GroupMessage', 'message.received']
|
|
assert adapter.current_sn == 7
|
|
assert 'm1' in adapter._message_cache
|
|
assert 'u1' in adapter._user_cache
|
|
assert 'c1' in adapter._group_cache
|