mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 12:05:54 +00:00
feat: route telegram eba events to plugins
This commit is contained in:
@@ -469,7 +469,9 @@ class PlatformSpecificEvent(Event):
|
||||
| `friend.added` | N | Y | Y | N | N | N | Y | Y | N |
|
||||
| `bot.invited_to_group` | Y | Y | Y | Y | Y | Y | Y | N | Y |
|
||||
| `bot.removed_from_group` | Y | Y | Y | Y | N | N | Y | N | Y |
|
||||
| `bot.muted` | N | N | Y | N | N | N | N | N | N |
|
||||
| `bot.muted` | Y | N | Y | N | N | N | N | N | N |
|
||||
| `bot.unmuted` | Y | N | Y | N | N | N | N | N | N |
|
||||
| `platform.specific` | Y | Y | Y | Y | Y | Y | Y | Y | Y |
|
||||
|
||||
> 注:此表为初步评估,具体以各平台 SDK/API 文档为准,实施时逐个确认。
|
||||
|
||||
@@ -545,6 +547,9 @@ spec:
|
||||
- group.info_updated
|
||||
- bot.invited_to_group
|
||||
- bot.removed_from_group
|
||||
- bot.muted
|
||||
- bot.unmuted
|
||||
- platform.specific
|
||||
platform_specific_events:
|
||||
- chat_member_updated
|
||||
- chat_join_request
|
||||
|
||||
@@ -323,6 +323,9 @@ spec:
|
||||
- group.info_updated
|
||||
- bot.invited_to_group
|
||||
- bot.removed_from_group
|
||||
- bot.muted
|
||||
- bot.unmuted
|
||||
- platform.specific
|
||||
|
||||
supported_apis:
|
||||
required:
|
||||
|
||||
@@ -721,6 +721,9 @@ interface BotConfig {
|
||||
- friend.added
|
||||
- bot.invited_to_group
|
||||
- bot.removed_from_group
|
||||
- bot.muted
|
||||
- bot.unmuted
|
||||
- platform.specific
|
||||
─────────────────
|
||||
- message.* (所有消息事件)
|
||||
- feedback.* (所有反馈事件)
|
||||
|
||||
@@ -154,6 +154,9 @@ class TelegramAdapter(TelegramAPIMixin, abstract_platform_adapter.AbstractPlatfo
|
||||
'group.member_banned',
|
||||
'bot.invited_to_group',
|
||||
'bot.removed_from_group',
|
||||
'bot.muted',
|
||||
'bot.unmuted',
|
||||
'platform.specific',
|
||||
]
|
||||
|
||||
def get_supported_apis(self) -> list[str]:
|
||||
@@ -221,27 +224,41 @@ class TelegramAdapter(TelegramAPIMixin, abstract_platform_adapter.AbstractPlatfo
|
||||
components = await TelegramMessageConverter.yiri2target(message, self.bot)
|
||||
|
||||
for component in components:
|
||||
if component['type'] == 'text':
|
||||
component_type = component.get('type')
|
||||
args = {
|
||||
'chat_id': message_source.source_platform_object.effective_chat.id,
|
||||
}
|
||||
|
||||
if message_source.source_platform_object.message.message_thread_id:
|
||||
args['message_thread_id'] = message_source.source_platform_object.message.message_thread_id
|
||||
|
||||
if quote_origin:
|
||||
args['reply_to_message_id'] = message_source.source_platform_object.message.id
|
||||
|
||||
if component_type == 'text':
|
||||
if self.config['markdown_card'] is True:
|
||||
content = telegramify_markdown.markdownify(
|
||||
content=component['text'],
|
||||
)
|
||||
else:
|
||||
content = component['text']
|
||||
args = {
|
||||
'chat_id': message_source.source_platform_object.effective_chat.id,
|
||||
'text': content,
|
||||
}
|
||||
if self.config['markdown_card'] is True:
|
||||
args['parse_mode'] = 'MarkdownV2'
|
||||
|
||||
if message_source.source_platform_object.message.message_thread_id:
|
||||
args['message_thread_id'] = message_source.source_platform_object.message.message_thread_id
|
||||
|
||||
if quote_origin:
|
||||
args['reply_to_message_id'] = message_source.source_platform_object.message.id
|
||||
|
||||
await self.bot.send_message(**args)
|
||||
args['text'] = content
|
||||
await self.bot.send_message(**args)
|
||||
elif component_type == 'photo':
|
||||
photo = component.get('photo')
|
||||
if photo is None:
|
||||
continue
|
||||
args['photo'] = telegram.InputFile(photo)
|
||||
await self.bot.send_photo(**args)
|
||||
elif component_type == 'document':
|
||||
doc = component.get('document')
|
||||
if doc is None:
|
||||
continue
|
||||
filename = component.get('filename', 'file')
|
||||
args['document'] = telegram.InputFile(doc, filename=filename)
|
||||
await self.bot.send_document(**args)
|
||||
|
||||
# ---- Streaming Output (preserving original logic) ----
|
||||
|
||||
|
||||
@@ -310,6 +310,16 @@ class TelegramEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
||||
source_platform_object=update,
|
||||
)
|
||||
|
||||
if old_status == 'restricted' and new_status in ('member', 'administrator') and mcm.new_chat_member:
|
||||
return platform_events.BotUnmutedEvent(
|
||||
type='bot.unmuted',
|
||||
timestamp=mcm.date.timestamp() if mcm.date else time.time(),
|
||||
adapter_name='telegram',
|
||||
group=group,
|
||||
operator=inviter,
|
||||
source_platform_object=update,
|
||||
)
|
||||
|
||||
return platform_events.PlatformSpecificEvent(
|
||||
type='platform.specific',
|
||||
timestamp=mcm.date.timestamp() if mcm.date else time.time(),
|
||||
|
||||
@@ -47,6 +47,9 @@ spec:
|
||||
- group.member_banned
|
||||
- bot.invited_to_group
|
||||
- bot.removed_from_group
|
||||
- bot.muted
|
||||
- bot.unmuted
|
||||
- platform.specific
|
||||
|
||||
supported_apis:
|
||||
required:
|
||||
|
||||
@@ -18,6 +18,7 @@ from ..entity.errors import platform as platform_errors
|
||||
from .logger import EventLogger
|
||||
|
||||
import langbot_plugin.api.entities.builtin.provider.session as provider_session
|
||||
import langbot_plugin.api.entities.events as plugin_events
|
||||
import langbot_plugin.api.entities.builtin.platform.events as platform_events
|
||||
import langbot_plugin.api.entities.builtin.platform.message as platform_message
|
||||
import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter
|
||||
@@ -77,6 +78,31 @@ class RuntimeBot:
|
||||
PIPELINE_DISCARD = '__discard__'
|
||||
PIPELINE_DISCARD_DISPLAY_NAME = 'Discarded'
|
||||
|
||||
@staticmethod
|
||||
def _eba_event_to_plugin_event(event: platform_events.EBAEvent) -> plugin_events.BaseEventModel | None:
|
||||
"""Map a platform EBA event to a plugin EventListener event model."""
|
||||
event_mapping: list[tuple[type[platform_events.EBAEvent], type[plugin_events.BaseEventModel]]] = [
|
||||
(platform_events.MessageReceivedEvent, plugin_events.MessageReceived),
|
||||
(platform_events.MessageEditedEvent, plugin_events.MessageEdited),
|
||||
(platform_events.MessageDeletedEvent, plugin_events.MessageDeleted),
|
||||
(platform_events.MessageReactionEvent, plugin_events.MessageReactionReceived),
|
||||
(platform_events.FeedbackReceivedEvent, plugin_events.FeedbackReceived),
|
||||
(platform_events.MemberJoinedEvent, plugin_events.GroupMemberJoined),
|
||||
(platform_events.MemberLeftEvent, plugin_events.GroupMemberLeft),
|
||||
(platform_events.MemberBannedEvent, plugin_events.GroupMemberBanned),
|
||||
(platform_events.BotInvitedToGroupEvent, plugin_events.BotInvitedToGroup),
|
||||
(platform_events.BotRemovedFromGroupEvent, plugin_events.BotRemovedFromGroup),
|
||||
(platform_events.BotMutedEvent, plugin_events.BotMuted),
|
||||
(platform_events.BotUnmutedEvent, plugin_events.BotUnmuted),
|
||||
(platform_events.PlatformSpecificEvent, plugin_events.PlatformSpecificEventReceived),
|
||||
]
|
||||
|
||||
for platform_event_type, plugin_event_type in event_mapping:
|
||||
if isinstance(event, platform_event_type):
|
||||
return plugin_event_type.from_platform_event(event)
|
||||
|
||||
return None
|
||||
|
||||
def resolve_pipeline_uuid(
|
||||
self,
|
||||
launcher_type: str,
|
||||
@@ -366,6 +392,21 @@ class RuntimeBot:
|
||||
|
||||
self.adapter.register_listener(platform_events.FeedbackEvent, on_feedback)
|
||||
|
||||
async def on_eba_event(
|
||||
event: platform_events.EBAEvent,
|
||||
adapter: abstract_platform_adapter.AbstractMessagePlatformAdapter,
|
||||
):
|
||||
plugin_event = self._eba_event_to_plugin_event(event)
|
||||
if plugin_event is None:
|
||||
return
|
||||
|
||||
try:
|
||||
await self.ap.plugin_connector.emit_event(plugin_event)
|
||||
except Exception:
|
||||
await self.logger.error(f'Failed to dispatch EBA event to plugins: {traceback.format_exc()}')
|
||||
|
||||
self.adapter.register_listener(platform_events.EBAEvent, on_eba_event)
|
||||
|
||||
async def run(self):
|
||||
async def exception_wrapper():
|
||||
try:
|
||||
|
||||
164
tests/e2e/live_telegram_eba_probe.py
Normal file
164
tests/e2e/live_telegram_eba_probe.py
Normal file
@@ -0,0 +1,164 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import telegram
|
||||
|
||||
from langbot.pkg.platform.adapters.telegram.adapter import TelegramAdapter
|
||||
from langbot_plugin.api.definition.abstract.platform.event_logger import AbstractEventLogger
|
||||
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 ProbeLogger(AbstractEventLogger):
|
||||
async def info(self, text, images=None, message_session_id=None, no_throw=True):
|
||||
print(f'[info] {text}')
|
||||
|
||||
async def debug(self, text, images=None, message_session_id=None, no_throw=True):
|
||||
print(f'[debug] {text}')
|
||||
|
||||
async def warning(self, text, images=None, message_session_id=None, no_throw=True):
|
||||
print(f'[warning] {text}')
|
||||
|
||||
async def error(self, text, images=None, message_session_id=None, no_throw=True):
|
||||
print(f'[error] {text}')
|
||||
|
||||
|
||||
PNG_1X1 = base64.b64decode(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII='
|
||||
)
|
||||
|
||||
|
||||
def summarize_event(event: platform_events.EBAEvent) -> dict:
|
||||
data = {
|
||||
'type': event.type,
|
||||
'adapter_name': event.adapter_name,
|
||||
'timestamp': event.timestamp,
|
||||
}
|
||||
for field in (
|
||||
'message_id',
|
||||
'chat_id',
|
||||
'chat_type',
|
||||
'reaction',
|
||||
'is_add',
|
||||
'action',
|
||||
'data',
|
||||
):
|
||||
if hasattr(event, field):
|
||||
value = getattr(event, field)
|
||||
if hasattr(value, 'value'):
|
||||
value = value.value
|
||||
data[field] = value
|
||||
return data
|
||||
|
||||
|
||||
async def run_probe(token: str, log_path: Path, timeout: int):
|
||||
adapter = TelegramAdapter(
|
||||
{
|
||||
'token': token,
|
||||
'markdown_card': False,
|
||||
'enable-stream-reply': False,
|
||||
},
|
||||
ProbeLogger(),
|
||||
)
|
||||
events: list[platform_events.EBAEvent] = []
|
||||
first_message = asyncio.Event()
|
||||
|
||||
async def listener(event, adapter):
|
||||
events.append(event)
|
||||
log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with log_path.open('a', encoding='utf-8') as fp:
|
||||
fp.write(json.dumps(summarize_event(event), ensure_ascii=False) + '\n')
|
||||
print('TELEGRAM_EBA_EVENT', json.dumps(summarize_event(event), ensure_ascii=False))
|
||||
if isinstance(event, platform_events.MessageReceivedEvent):
|
||||
first_message.set()
|
||||
|
||||
adapter.register_listener(platform_events.EBAEvent, listener)
|
||||
await adapter.run_async()
|
||||
|
||||
try:
|
||||
print('READY: send a private or group message to the Telegram test bot now.')
|
||||
await asyncio.wait_for(first_message.wait(), timeout=timeout)
|
||||
source = next(event for event in events if isinstance(event, platform_events.MessageReceivedEvent))
|
||||
|
||||
await adapter.reply_message(
|
||||
source,
|
||||
platform_message.MessageChain(
|
||||
[
|
||||
platform_message.Plain(text='EBA live reply: text'),
|
||||
platform_message.Image(base64=base64.b64encode(PNG_1X1).decode()),
|
||||
platform_message.File(
|
||||
name='eba-live.txt',
|
||||
size=8,
|
||||
base64='data:text/plain;base64,' + base64.b64encode(b'eba-live').decode(),
|
||||
),
|
||||
]
|
||||
),
|
||||
quote_origin=True,
|
||||
)
|
||||
await adapter.send_message(
|
||||
source.chat_type.value if hasattr(source.chat_type, 'value') else str(source.chat_type),
|
||||
source.chat_id,
|
||||
platform_message.MessageChain([platform_message.Plain(text='EBA live send_message OK')]),
|
||||
)
|
||||
|
||||
edit_probe = await adapter.bot.send_message(chat_id=source.chat_id, text='EBA edit/delete probe')
|
||||
await adapter.edit_message(
|
||||
str(source.chat_type),
|
||||
source.chat_id,
|
||||
edit_probe.message_id,
|
||||
platform_message.MessageChain([platform_message.Plain(text='EBA edit probe edited')]),
|
||||
)
|
||||
await adapter.delete_message(str(source.chat_type), source.chat_id, edit_probe.message_id)
|
||||
|
||||
await adapter.bot.send_message(
|
||||
chat_id=source.chat_id,
|
||||
text='EBA callback probe',
|
||||
reply_markup=telegram.InlineKeyboardMarkup(
|
||||
[[telegram.InlineKeyboardButton('callback probe', callback_data='eba-callback-probe')]]
|
||||
),
|
||||
)
|
||||
|
||||
if str(source.chat_type) == 'ChatType.GROUP' or getattr(source.chat_type, 'value', '') == 'group':
|
||||
group_info = await adapter.get_group_info(source.chat_id)
|
||||
print('GROUP_INFO', group_info.model_dump())
|
||||
members = await adapter.get_group_member_list(source.chat_id)
|
||||
print('GROUP_MEMBER_LIST_COUNT', len(members))
|
||||
await adapter.call_platform_api('send_chat_action', {'chat_id': source.chat_id, 'action': 'typing'})
|
||||
count = await adapter.call_platform_api('get_chat_member_count', {'chat_id': source.chat_id})
|
||||
print('GROUP_MEMBER_COUNT', count)
|
||||
|
||||
print('READY: click the callback button or react to a bot message if you want live callback/reaction events.')
|
||||
await asyncio.sleep(max(5, timeout // 3))
|
||||
finally:
|
||||
await adapter.kill()
|
||||
summary = {
|
||||
'events': [summarize_event(event) for event in events],
|
||||
'event_types': [event.type for event in events],
|
||||
}
|
||||
print('SUMMARY', json.dumps(summary, ensure_ascii=False))
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--token', default=os.getenv('TELEGRAM_BOT_TOKEN', ''))
|
||||
parser.add_argument('--log', default='data/temp/live_telegram_eba_probe.jsonl')
|
||||
parser.add_argument('--timeout', type=int, default=90)
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.token:
|
||||
raise SystemExit('Set TELEGRAM_BOT_TOKEN or pass --token')
|
||||
|
||||
log_path = Path(args.log)
|
||||
if log_path.exists():
|
||||
log_path.unlink()
|
||||
asyncio.run(run_probe(args.token, log_path, args.timeout))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,16 +1,25 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import datetime
|
||||
import pathlib
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
import telegram
|
||||
from telegram.ext import CallbackQueryHandler, ChatMemberHandler, MessageHandler, MessageReactionHandler
|
||||
|
||||
from langbot.pkg.platform.adapters.telegram.event_converter import TelegramEventConverter
|
||||
from langbot.pkg.platform.adapters.telegram.platform_api import PLATFORM_API_MAP
|
||||
from langbot.pkg.platform.adapters.telegram.adapter import TelegramAdapter
|
||||
from langbot.pkg.platform.botmgr import RuntimeBot
|
||||
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
|
||||
from langbot_plugin.api.entities import events as plugin_events
|
||||
|
||||
|
||||
class DummyLogger(AbstractEventLogger):
|
||||
@@ -38,6 +47,23 @@ def make_adapter() -> TelegramAdapter:
|
||||
)
|
||||
|
||||
|
||||
def make_update(data: dict) -> telegram.Update:
|
||||
payload = {'update_id': 1000, **data}
|
||||
return telegram.Update.de_json(payload, make_adapter().bot)
|
||||
|
||||
|
||||
def base_message_payload(**overrides):
|
||||
payload = {
|
||||
'message_id': 10,
|
||||
'date': int(datetime.datetime.now(datetime.UTC).timestamp()),
|
||||
'chat': {'id': 123, 'type': 'private', 'first_name': 'Chat User'},
|
||||
'from': {'id': 456, 'is_bot': False, 'first_name': 'Sender', 'username': 'sender'},
|
||||
'text': 'hello',
|
||||
}
|
||||
payload.update(overrides)
|
||||
return payload
|
||||
|
||||
|
||||
def test_telegram_adapter_registers_all_declared_update_handlers():
|
||||
adapter = make_adapter()
|
||||
|
||||
@@ -125,3 +151,290 @@ def test_telegram_supported_events_match_manifest():
|
||||
assert adapter_events == manifest_events
|
||||
assert 'message.deleted' not in adapter_events
|
||||
assert 'group.info_updated' not in adapter_events
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_telegram_converter_maps_message_and_edited_message_events():
|
||||
update = make_update({'message': base_message_payload(text='hello @test_bot')})
|
||||
event = await TelegramEventConverter.target2yiri(update, make_adapter().bot, 'test_bot')
|
||||
|
||||
assert isinstance(event, platform_events.MessageReceivedEvent)
|
||||
assert event.type == 'message.received'
|
||||
assert event.chat_type == platform_entities.ChatType.PRIVATE
|
||||
assert event.chat_id == 123
|
||||
assert event.sender.id == 456
|
||||
assert platform_message.At in event.message_chain
|
||||
assert isinstance(event.message_chain[0], platform_message.At)
|
||||
assert isinstance(event.message_chain[1], platform_message.Plain)
|
||||
assert event.message_chain[1].text == 'hello '
|
||||
|
||||
group_chat = {'id': -100123, 'type': 'supergroup', 'title': 'Test Group'}
|
||||
edited_payload = base_message_payload(chat=group_chat, text='edited')
|
||||
edited_payload['edit_date'] = edited_payload['date'] + 1
|
||||
edited = make_update({'edited_message': edited_payload})
|
||||
edited_event = await TelegramEventConverter.target2yiri(edited, make_adapter().bot, 'test_bot')
|
||||
|
||||
assert isinstance(edited_event, platform_events.MessageEditedEvent)
|
||||
assert edited_event.type == 'message.edited'
|
||||
assert edited_event.chat_type == platform_entities.ChatType.GROUP
|
||||
assert edited_event.group.name == 'Test Group'
|
||||
assert str(edited_event.new_content) == 'edited'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_telegram_converter_maps_non_message_updates():
|
||||
chat_member = make_update(
|
||||
{
|
||||
'chat_member': {
|
||||
'chat': {'id': -1001, 'type': 'supergroup', 'title': 'Group'},
|
||||
'from': {'id': 1, 'is_bot': False, 'first_name': 'Admin'},
|
||||
'date': int(datetime.datetime.now(datetime.UTC).timestamp()),
|
||||
'old_chat_member': {
|
||||
'user': {'id': 2, 'is_bot': False, 'first_name': 'Member'},
|
||||
'status': 'left',
|
||||
},
|
||||
'new_chat_member': {
|
||||
'user': {'id': 2, 'is_bot': False, 'first_name': 'Member'},
|
||||
'status': 'member',
|
||||
},
|
||||
}
|
||||
}
|
||||
)
|
||||
joined = await TelegramEventConverter.target2yiri(chat_member, make_adapter().bot, 'test_bot')
|
||||
assert isinstance(joined, platform_events.MemberJoinedEvent)
|
||||
assert joined.type == 'group.member_joined'
|
||||
|
||||
callback = make_update(
|
||||
{
|
||||
'callback_query': {
|
||||
'id': 'cb-1',
|
||||
'from': {'id': 3, 'is_bot': False, 'first_name': 'Clicker'},
|
||||
'chat_instance': 'ci',
|
||||
'data': 'button-data',
|
||||
'message': base_message_payload(message_id=77),
|
||||
}
|
||||
}
|
||||
)
|
||||
callback_event = await TelegramEventConverter.target2yiri(callback, make_adapter().bot, 'test_bot')
|
||||
assert isinstance(callback_event, platform_events.PlatformSpecificEvent)
|
||||
assert callback_event.action == 'callback_query'
|
||||
assert callback_event.data['callback_query_id'] == 'cb-1'
|
||||
assert callback_event.data['data'] == 'button-data'
|
||||
|
||||
reaction = make_update(
|
||||
{
|
||||
'message_reaction': {
|
||||
'chat': {'id': -1001, 'type': 'supergroup', 'title': 'Group'},
|
||||
'message_id': 77,
|
||||
'date': int(datetime.datetime.now(datetime.UTC).timestamp()),
|
||||
'user': {'id': 3, 'is_bot': False, 'first_name': 'Reactor'},
|
||||
'old_reaction': [],
|
||||
'new_reaction': [{'type': 'emoji', 'emoji': '👍'}],
|
||||
}
|
||||
}
|
||||
)
|
||||
reaction_event = await TelegramEventConverter.target2yiri(reaction, make_adapter().bot, 'test_bot')
|
||||
assert isinstance(reaction_event, platform_events.MessageReactionEvent)
|
||||
assert reaction_event.reaction == '👍'
|
||||
assert reaction_event.is_add is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_telegram_converter_maps_bot_status_events():
|
||||
base_member = {
|
||||
'chat': {'id': -1001, 'type': 'supergroup', 'title': 'Group'},
|
||||
'from': {'id': 1, 'is_bot': False, 'first_name': 'Admin'},
|
||||
'date': int(datetime.datetime.now(datetime.UTC).timestamp()),
|
||||
}
|
||||
restricted_member = {
|
||||
'user': {'id': 999, 'is_bot': True, 'first_name': 'Bot'},
|
||||
'status': 'restricted',
|
||||
'is_member': True,
|
||||
'can_send_messages': False,
|
||||
'can_send_audios': False,
|
||||
'can_send_documents': False,
|
||||
'can_send_photos': False,
|
||||
'can_send_videos': False,
|
||||
'can_send_video_notes': False,
|
||||
'can_send_voice_notes': False,
|
||||
'can_send_polls': False,
|
||||
'can_send_other_messages': False,
|
||||
'can_add_web_page_previews': False,
|
||||
'can_change_info': False,
|
||||
'can_invite_users': False,
|
||||
'can_pin_messages': False,
|
||||
'can_manage_topics': False,
|
||||
'until_date': 0,
|
||||
}
|
||||
invited = make_update(
|
||||
{
|
||||
'my_chat_member': {
|
||||
**base_member,
|
||||
'old_chat_member': {
|
||||
'user': {'id': 999, 'is_bot': True, 'first_name': 'Bot'},
|
||||
'status': 'left',
|
||||
},
|
||||
'new_chat_member': {
|
||||
'user': {'id': 999, 'is_bot': True, 'first_name': 'Bot'},
|
||||
'status': 'member',
|
||||
},
|
||||
}
|
||||
}
|
||||
)
|
||||
invited_event = await TelegramEventConverter.target2yiri(invited, make_adapter().bot, 'test_bot')
|
||||
assert isinstance(invited_event, platform_events.BotInvitedToGroupEvent)
|
||||
|
||||
muted = make_update(
|
||||
{
|
||||
'my_chat_member': {
|
||||
**base_member,
|
||||
'old_chat_member': {
|
||||
'user': {'id': 999, 'is_bot': True, 'first_name': 'Bot'},
|
||||
'status': 'member',
|
||||
},
|
||||
'new_chat_member': {
|
||||
**restricted_member,
|
||||
},
|
||||
}
|
||||
}
|
||||
)
|
||||
muted_event = await TelegramEventConverter.target2yiri(muted, make_adapter().bot, 'test_bot')
|
||||
assert isinstance(muted_event, platform_events.BotMutedEvent)
|
||||
|
||||
unmuted = make_update(
|
||||
{
|
||||
'my_chat_member': {
|
||||
**base_member,
|
||||
'old_chat_member': {
|
||||
**restricted_member,
|
||||
},
|
||||
'new_chat_member': {
|
||||
'user': {'id': 999, 'is_bot': True, 'first_name': 'Bot'},
|
||||
'status': 'member',
|
||||
},
|
||||
}
|
||||
}
|
||||
)
|
||||
unmuted_event = await TelegramEventConverter.target2yiri(unmuted, make_adapter().bot, 'test_bot')
|
||||
assert isinstance(unmuted_event, platform_events.BotUnmutedEvent)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_telegram_reply_message_sends_text_image_and_file_components():
|
||||
adapter = make_adapter()
|
||||
bot = SimpleNamespace(
|
||||
send_message=AsyncMock(),
|
||||
send_photo=AsyncMock(),
|
||||
send_document=AsyncMock(),
|
||||
)
|
||||
object.__setattr__(adapter, 'bot', bot)
|
||||
update = make_update({'message': base_message_payload(message_id=88)})
|
||||
|
||||
message_source = platform_events.MessageReceivedEvent(
|
||||
message_id=88,
|
||||
source_platform_object=update,
|
||||
)
|
||||
await adapter.reply_message(
|
||||
message_source,
|
||||
platform_message.MessageChain(
|
||||
[
|
||||
platform_message.Plain(text='reply text'),
|
||||
platform_message.Image(base64=base64.b64encode(b'image-bytes').decode('utf-8')),
|
||||
platform_message.File(
|
||||
name='test.txt',
|
||||
size=4,
|
||||
base64='data:text/plain;base64,' + base64.b64encode(b'test').decode('utf-8'),
|
||||
),
|
||||
]
|
||||
),
|
||||
quote_origin=True,
|
||||
)
|
||||
|
||||
bot.send_message.assert_awaited_once()
|
||||
bot.send_photo.assert_awaited_once()
|
||||
bot.send_document.assert_awaited_once()
|
||||
assert bot.send_message.await_args.kwargs['reply_to_message_id'] == 88
|
||||
assert bot.send_photo.await_args.kwargs['reply_to_message_id'] == 88
|
||||
assert bot.send_document.await_args.kwargs['document'].filename == 'test.txt'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_telegram_platform_apis_call_underlying_bot_methods():
|
||||
bot = SimpleNamespace(
|
||||
pin_chat_message=AsyncMock(),
|
||||
unpin_chat_message=AsyncMock(),
|
||||
unpin_all_chat_messages=AsyncMock(),
|
||||
get_chat_administrators=AsyncMock(
|
||||
return_value=[
|
||||
SimpleNamespace(
|
||||
user=SimpleNamespace(id=1, username='admin', first_name='Admin'),
|
||||
status='administrator',
|
||||
custom_title='Boss',
|
||||
)
|
||||
]
|
||||
),
|
||||
set_chat_title=AsyncMock(),
|
||||
set_chat_description=AsyncMock(),
|
||||
get_chat_member_count=AsyncMock(return_value=3),
|
||||
send_chat_action=AsyncMock(),
|
||||
create_chat_invite_link=AsyncMock(
|
||||
return_value=SimpleNamespace(
|
||||
invite_link='https://t.me/+abc',
|
||||
name='invite',
|
||||
is_primary=False,
|
||||
is_revoked=False,
|
||||
)
|
||||
),
|
||||
answer_callback_query=AsyncMock(),
|
||||
)
|
||||
|
||||
assert await PLATFORM_API_MAP['pin_message'](bot, {'chat_id': 1, 'message_id': 2}) == {'ok': True}
|
||||
assert await PLATFORM_API_MAP['unpin_message'](bot, {'chat_id': 1, 'message_id': 2}) == {'ok': True}
|
||||
assert await PLATFORM_API_MAP['unpin_all_messages'](bot, {'chat_id': 1}) == {'ok': True}
|
||||
admins = await PLATFORM_API_MAP['get_chat_administrators'](bot, {'chat_id': 1})
|
||||
assert admins['administrators'][0]['user_id'] == 1
|
||||
assert await PLATFORM_API_MAP['set_chat_title'](bot, {'chat_id': 1, 'title': 'New'}) == {'ok': True}
|
||||
assert await PLATFORM_API_MAP['set_chat_description'](bot, {'chat_id': 1, 'description': 'Desc'}) == {'ok': True}
|
||||
assert await PLATFORM_API_MAP['get_chat_member_count'](bot, {'chat_id': 1}) == {'count': 3}
|
||||
assert await PLATFORM_API_MAP['send_chat_action'](bot, {'chat_id': 1, 'action': 'typing'}) == {'ok': True}
|
||||
invite = await PLATFORM_API_MAP['create_chat_invite_link'](bot, {'chat_id': 1, 'name': 'invite'})
|
||||
assert invite['invite_link'] == 'https://t.me/+abc'
|
||||
assert await PLATFORM_API_MAP['answer_callback_query'](bot, {'callback_query_id': 'cb'}) == {'ok': True}
|
||||
|
||||
|
||||
def test_runtime_bot_maps_telegram_eba_events_to_plugin_events():
|
||||
group = platform_entities.UserGroup(id='group-1', name='Group')
|
||||
user = platform_entities.User(id='user-1', nickname='User')
|
||||
|
||||
cases = [
|
||||
(
|
||||
platform_events.MessageReceivedEvent(
|
||||
message_id='msg',
|
||||
message_chain=platform_message.MessageChain([platform_message.Plain(text='hi')]),
|
||||
sender=user,
|
||||
chat_id='user-1',
|
||||
),
|
||||
plugin_events.MessageReceived,
|
||||
),
|
||||
(
|
||||
platform_events.MessageReactionEvent(message_id='msg', user=user, reaction='👍'),
|
||||
plugin_events.MessageReactionReceived,
|
||||
),
|
||||
(
|
||||
platform_events.MemberJoinedEvent(group=group, member=user),
|
||||
plugin_events.GroupMemberJoined,
|
||||
),
|
||||
(
|
||||
platform_events.BotUnmutedEvent(group=group, operator=user),
|
||||
plugin_events.BotUnmuted,
|
||||
),
|
||||
(
|
||||
platform_events.PlatformSpecificEvent(adapter_name='telegram', action='callback_query', data={'data': 'x'}),
|
||||
plugin_events.PlatformSpecificEventReceived,
|
||||
),
|
||||
]
|
||||
|
||||
for platform_event, plugin_event_type in cases:
|
||||
plugin_event = RuntimeBot._eba_event_to_plugin_event(platform_event)
|
||||
assert isinstance(plugin_event, plugin_event_type)
|
||||
assert plugin_event.model_dump()['event_name'] == plugin_event_type.__name__
|
||||
|
||||
Reference in New Issue
Block a user