diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 6589fc47..3970b8d5 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -149,8 +149,21 @@ CONFIG_METADATA_2 = { "app_secret": "", "domain": "https://open.feishu.cn" }, + "telegram": { + "id": "telegram", + "type": "telegram", + "enable": False, + "telegram_token": "your_bot_token", + "start_message": "Hello, I'm AstrBot!", + "telegram_api_base_url": "https://api.telegram.org/bot", + } }, "items": { + "telegram_token": { + "description": "Bot Token", + "type": "string", + "hint": "如果你的网络环境为中国大陆,请在 `其他配置` 处设置代理或更改 api_base。", + }, "id": { "description": "ID", "type": "string", @@ -250,7 +263,7 @@ CONFIG_METADATA_2 = { "description": "间隔时间计算方法", "type": "string", "options": ["random", "log"], - "hint": "分段回复的间隔时间计算方法。random 为随机时间,log 为根据消息长度计算,$y=log_{log\_base}(x)$,x为字数,y的单位为秒。", + "hint": "分段回复的间隔时间计算方法。random 为随机时间,log 为根据消息长度计算,$y=log_(x)$,x为字数,y的单位为秒。", }, "interval": { "description": "随机间隔时间(秒)", diff --git a/astrbot/core/platform/manager.py b/astrbot/core/platform/manager.py index a99fb553..a21b0672 100644 --- a/astrbot/core/platform/manager.py +++ b/astrbot/core/platform/manager.py @@ -49,6 +49,8 @@ class PlatformManager(): from .sources.gewechat.gewechat_platform_adapter import GewechatPlatformAdapter # noqa: F401 case "lark": from .sources.lark.lark_adapter import LarkPlatformAdapter # noqa: F401 + case "telegram": + from .sources.telegram.tg_adapter import TelegramPlatformAdapter # noqa: F401 except (ImportError, ModuleNotFoundError) as e: logger.error(f"加载平台适配器 {platform_config['type']} 失败,原因:{e}。请检查依赖库是否安装。提示:可以在 管理面板->控制台->安装Pip库 中安装依赖库。") except Exception as e: @@ -56,7 +58,7 @@ class PlatformManager(): if platform_config['type'] not in platform_cls_map: - logger.error(f"未找到适用于 {platform_config['type']}({platform_config['id']}) 平台适配器,请检查是否已经安装或者名称填写错误。") + logger.error(f"未找到适用于 {platform_config['type']}({platform_config['id']}) 平台适配器,请检查是否已经安装或者名称填写错误") return cls_type = platform_cls_map[platform_config['type']] inst = cls_type(platform_config, self.settings, self.event_queue) diff --git a/astrbot/core/platform/sources/telegram/tg_adapter.py b/astrbot/core/platform/sources/telegram/tg_adapter.py new file mode 100644 index 00000000..0a931fa6 --- /dev/null +++ b/astrbot/core/platform/sources/telegram/tg_adapter.py @@ -0,0 +1,117 @@ +import sys +import uuid +import asyncio + +from astrbot.api.platform import Platform, AstrBotMessage, MessageMember, PlatformMetadata, MessageType +from astrbot.api.event import MessageChain +from astrbot.api.message_components import Plain, Image, Record, File as AstrBotFile, Video +from astrbot.core.platform.astr_message_event import MessageSesion +from astrbot.api.platform import register_platform_adapter + +from telegram import Update +from telegram.ext import ApplicationBuilder, ContextTypes, filters +from telegram.constants import ChatType +from telegram.ext import MessageHandler as TelegramMessageHandler +from .tg_event import TelegramPlatformEvent +from astrbot.api import logger + +if sys.version_info >= (3, 12): + from typing import override +else: + from typing_extensions import override + +@register_platform_adapter("telegram", "telegram 适配器") +class TelegramPlatformAdapter(Platform): + + def __init__(self, platform_config: dict, platform_settings: dict, event_queue: asyncio.Queue) -> None: + super().__init__(event_queue) + self.config = platform_config + self.settings = platform_settings + self.client_self_id = uuid.uuid4().hex[:8] + + @override + async def send_by_session(self, session: MessageSesion, message_chain: MessageChain): + from_username = session.session_id + await TelegramPlatformEvent.send_with_client(self.client, message_chain, from_username) + await super().send_by_session(session, message_chain) + + @override + def meta(self) -> PlatformMetadata: + return PlatformMetadata( + "telegram", + "telegram 适配器", + ) + + @override + async def run(self): + base_url = self.config.get("telegram_api_base_url", "https://api.telegram.org/bot") + if not base_url: + base_url = "https://api.telegram.org/bot" + + self.application = ApplicationBuilder().token(self.config['telegram_token']).base_url(base_url).build() + message_handler = TelegramMessageHandler( + filters=filters.ALL, # receive all messages + callback=self.convert_message + ) + self.application.add_handler(message_handler) + await self.application.initialize() + await self.application.start() + queue = self.application.updater.start_polling() + self.client = self.application.bot + logger.info("Telegram Platform Adapter is running.") + + await queue + + async def start(self, update: Update, context: ContextTypes.DEFAULT_TYPE): + await context.bot.send_message(chat_id=update.effective_chat.id, text=self.config["start_message"]) + + async def convert_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> AstrBotMessage: + message = AstrBotMessage() + # 获得是群聊还是私聊 + if update.effective_chat.type == ChatType.PRIVATE: + message.type = MessageType.FRIEND_MESSAGE + else: + message.type = MessageType.GROUP_MESSAGE + message.group_id = update.effective_chat.id + message.message_id = str(update.message.message_id) + message.session_id = str(update.effective_chat.id) + message.sender = MessageMember(str(update.effective_user.id), update.effective_user.username) + message.self_id = str(context.bot.id) + message.raw_message = update + message.message_str = "" + message.message = [] + + if update.message.text: + plain_text = update.message.text + message.message = [Plain(plain_text),] + message.message_str = plain_text + + elif update.message.voice: + file = await update.message.voice.get_file() + message.message = [Record(file=file.file_path, url=file.file_path),] + + elif update.message.photo: + for photo in update.message.photo: + file = await photo.get_file() + message.message.append(Image(file=file.file_path, url=file.file_path)) + + elif update.message.document: + file = await update.message.document.get_file() + message.message = [AstrBotFile(file=file.file_path, name="file"),] + + elif update.message.video: + file = await update.message.video.get_file() + message.message = [Video(file=file.file_path, path=file.file_path),] + + + await self.handle_msg(message) + + async def handle_msg(self, message: AstrBotMessage): + message_event = TelegramPlatformEvent( + message_str=message.message_str, + message_obj=message, + platform_meta=self.meta(), + session_id=message.session_id, + client=self.client + ) + self.commit_event(message_event) \ No newline at end of file diff --git a/astrbot/core/platform/sources/telegram/tg_event.py b/astrbot/core/platform/sources/telegram/tg_event.py new file mode 100644 index 00000000..04b82bde --- /dev/null +++ b/astrbot/core/platform/sources/telegram/tg_event.py @@ -0,0 +1,50 @@ +from astrbot.api.event import AstrMessageEvent, MessageChain +from astrbot.api.platform import AstrBotMessage, PlatformMetadata, MessageType +from astrbot.api.message_components import Plain, Image, Reply, At +from telegram.ext import ExtBot + +class TelegramPlatformEvent(AstrMessageEvent): + def __init__(self, message_str: str, message_obj: AstrBotMessage, platform_meta: PlatformMetadata, session_id: str, client: ExtBot): + super().__init__(message_str, message_obj, platform_meta, session_id) + self.client = client + + @staticmethod + async def send_with_client(client: ExtBot, message: MessageChain, user_name: str): + image_path = None + + has_reply = False + reply_message_id = None + at_user_id = None + for i in message.chain: + if isinstance(i, Reply): + has_reply = True + reply_message_id = i.id + if isinstance(i, At): + at_user_id = i.name + + at_flag = False + for i in message.chain: + payload = { + "chat_id": user_name, + } + if has_reply: + payload["reply_to_message_id"] = reply_message_id + + if isinstance(i, Plain): + if at_user_id and not at_flag: + i.text = f"@{at_user_id} " + i.text + at_flag = True + await client.send_message(text=i.text, **payload) + elif isinstance(i, Image): + if i.path: + image_path = i.path + else: + image_path = i.file + await client.send_photo(photo=image_path, **payload) + + async def send(self, message: MessageChain): + if self.get_message_type() == MessageType.GROUP_MESSAGE: + await self.send_with_client(self.client, message, self.message_obj.group_id) + else: + await self.send_with_client(self.client, message, self.get_sender_id()) + await super().send(message) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 441d73d4..7290f629 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,6 @@ beautifulsoup4 googlesearch-python readability-lxml quart -psutil lxml_html_clean colorlog aiocqhttp @@ -23,5 +22,5 @@ psutil>=5.8.0 lark-oapi ormsgpack cryptography - -dashscope \ No newline at end of file +dashscope +python-telegram-bot \ No newline at end of file