From b041aa0de827f27d748feffc2e0f9e1cd4076e0c Mon Sep 17 00:00:00 2001 From: grassblock Date: Sun, 24 Aug 2025 19:13:25 +0800 Subject: [PATCH] feat: init matrix bot code --- adapters/matrix.py | 154 ++++++++++++++++++++++++++++++++++++++++ config.example.yaml | 8 +++ config.py | 22 ++++++ helpers/matrix_login.py | 65 +++++++++++++++++ main.py | 15 +++- 5 files changed, 262 insertions(+), 2 deletions(-) create mode 100644 helpers/matrix_login.py diff --git a/adapters/matrix.py b/adapters/matrix.py index e69de29..f4c260e 100644 --- a/adapters/matrix.py +++ b/adapters/matrix.py @@ -0,0 +1,154 @@ +import asyncio +import json +import logging +import os + +from nio import AsyncClient, MatrixRoom, RoomMessageText +from nio.events.room_events import RoomMessageText +from typing import Dict, Callable + +import config + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +class MatrixAdapter: + def __init__(self, homeserver: str, user_id: str, token: str, device_name: str = "MatrixBot"): + """ + Initialize the Matrix bot. + + Args: + homeserver: The Matrix homeserver URL + user_id: The bot's Matrix user ID + token: The bot's token + device_name: Device name for the bot + """ + self.homeserver = homeserver + self.user_id = user_id + self.token = token + self.device_name = device_name + self.client = AsyncClient(homeserver, user_id) + self.commands: Dict[str, Callable] = {} + + # Register event handlers + self.client.add_event_callback(self.message_callback, RoomMessageText) + + def add_command(self, command: str, handler: Callable): + """Add a command handler.""" + self.commands[command.lower()] = handler + + async def message_callback(self, room: MatrixRoom, event: RoomMessageText): + """Handle incoming messages.""" + # Ignore messages from the bot itself + if event.sender == self.user_id: + return + + # Check if message starts with command prefix + message = event.body.strip() + if not message.startswith('!'): + return + + # Parse command and arguments + parts = message[1:].split(' ', 1) + command = parts[0].lower() + args = parts[1] if len(parts) > 1 else "" + + # Execute command if it exists + if command in self.commands: + try: + response = await self.commands[command](room, event, args) + if response: + await self.send_message(room.room_id, response) + except Exception as e: + logger.error(f"Error executing command {command}: {e}") + await self.send_message(room.room_id, f"Error executing command: {str(e)}") + else: + await self.send_message(room.room_id, f"Unknown command: {command}") + + async def send_message(self, room_id: str, message: str): + """Send a message to a room.""" + await self.client.room_send( + room_id=room_id, + message_type="m.room.message", + content={ + "msgtype": "m.text", + "body": message + } + ) + + async def start(self): + """Start the bot.""" + logger.info("Starting Matrix bot...") + + # Login + #response = await self.client.login(token=self.token, device_name=self.device_name) + self.client.access_token = self.token + self.client.device_id = "REALBOT" + + logger.info(f"Logged in as {self.user_id}") + + # Sync and listen for events + await self.client.sync_forever(timeout=30000) + + async def stop(self): + """Stop the bot.""" + logger.info("Stopping Matrix bot...") + await self.client.logout() + await self.client.close() + + +# Example command handlers +async def hello_command(room: MatrixRoom, event: RoomMessageText, args: str) -> str: + """Handle !hello command.""" + return f"Hello {event.sender}!" + + +async def echo_command(room: MatrixRoom, event: RoomMessageText, args: str) -> str: + """Handle !echo command.""" + if not args: + return "Usage: !echo " + return f"Echo: {args}" + + +async def help_command(room: MatrixRoom, event: RoomMessageText, args: str) -> str: + """Handle !help command.""" + help_text = """ +Available commands: +- !hello - Say hello +- !echo - Echo a message +- !help - Show this help message + """ + return help_text.strip() + + +async def main(): + """Main function to run the bot.""" + # 从环境变量或配置文件中获取值 + matrix_config = config.Config().get_config_value('matrix', {}) + homeserver = matrix_config.get('homeserver', "https://matrix.org") + user_id = matrix_config.get('user_id') + token = os.getenv("MATRIX_BOT_TOKEN") + + # Create bot instance + bot = MatrixAdapter(homeserver, user_id, token) + + # Register commands + bot.add_command("hello", hello_command) + bot.add_command("echo", echo_command) + bot.add_command("help", help_command) + + try: + print("Starting Matrix bot...") + await bot.start() + except KeyboardInterrupt: + logger.info("Bot interrupted by user") + except Exception as e: + logger.error(f"Bot error: {e}") + finally: + await bot.stop() + + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/config.example.yaml b/config.example.yaml index e6d17a5..17242bd 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -6,6 +6,14 @@ admin: 123456789 # 这部分仅限于汇报链接跟踪参数去除的问题 dev: 616760897 +start_telegram_bot: true +also_start_matrix_bot: false + +matrix: + homeserver: "https://matrix.org" + user_id: "@your_bot:matrix.org" + # Token 请使用登录后生成的 Access Token,一般可以在客户端的设置页面找到,然后设置到 MATRIX_BOT_TOKEN 环境变量中 + # 如果你想使用一个新的 token,可以使用 helpers/matrix_login.py 脚本生成一个新的 token # global features settings diff --git a/config.py b/config.py index 1b4b184..a4d312c 100644 --- a/config.py +++ b/config.py @@ -27,6 +27,28 @@ class Config: """Get developer user ID""" return self.config_data.get('dev') + def get_config_value(self, key: str, default: Any = None) -> Any: + """ + Get a configuration value by key + + Args: + key: Configuration key to retrieve + default: Default value if key is not found + + Returns: + The configuration value or default if not found + """ + keys = key.split('.') + value = self.config_data + + for k in keys: + if isinstance(value, dict) and k in value: + value = value[k] + else: + return default + + return value + def is_feature_enabled(self, feature_name: str, chat_id: Optional[int] = None) -> bool: """ Check if a feature is enabled for a specific chat or globally diff --git a/helpers/matrix_login.py b/helpers/matrix_login.py new file mode 100644 index 0000000..5241645 --- /dev/null +++ b/helpers/matrix_login.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +import asyncio +import getpass +import json +import os +import sys +import aiofiles +from nio import AsyncClient, LoginResponse + +CONFIG_FILE = "credentials.json" +# Check out main() below to see how it's done. +def write_details_to_disk(resp: LoginResponse, homeserver) -> None: + """Writes the required login details to disk so we can log in later without + using a password. + + Arguments: + resp {LoginResponse} -- the successful client login response. + homeserver -- URL of homeserver, e.g. "https://matrix.example.org" + """ + # open the config file in write-mode + with open(CONFIG_FILE, "w") as f: + # write the login details to disk + json.dump( + { + "homeserver": homeserver, # e.g. "https://matrix.example.org" + "user_id": resp.user_id, # e.g. "@user:example.org" + "device_id": resp.device_id, # device ID, 10 uppercase letters + "access_token": resp.access_token, # cryptogr. access token + }, + f, + ) + +async def main() -> None: + # If there are no previously-saved credentials, we'll use the password + if not os.path.exists(CONFIG_FILE): + print( + "First time use. Did not find credential file. Asking for " + "homeserver, user, and password to create credential file." + ) + homeserver = "https://matrix.example.org" + homeserver = input(f"Enter your homeserver URL: [{homeserver}] ") + if not (homeserver.startswith("https://") or homeserver.startswith("http://")): + homeserver = "https://" + homeserver + user_id = "@user:example.org" + user_id = input(f"Enter your full user ID: [{user_id}] ") + device_name = "matrix-nio" + device_name = input(f"Choose a name for this device: [{device_name}] ") + client = AsyncClient(homeserver, user_id) + pw = getpass.getpass() + resp = await client.login(pw, device_name=device_name) + # check that we logged in successfully + if isinstance(resp, LoginResponse): + write_details_to_disk(resp, homeserver) + else: + print(f'homeserver = "{homeserver}"; user = "{user_id}"') + print(f"Failed to log in: {resp}") + sys.exit(1) + print( + "登录成功,凭据已经保存到 " + CONFIG_FILE + " 文件中" + ) + # Otherwise the config file exists, so we'll use the stored credentials + else: + print("你已经登录过了") + +asyncio.run(main()) diff --git a/main.py b/main.py index ad5bfa1..3531f7c 100644 --- a/main.py +++ b/main.py @@ -2,6 +2,7 @@ import asyncio import logging import sys +import config from adapters.tg import TelegramAdapter @@ -9,9 +10,19 @@ async def main(): """Main entry point""" logging.basicConfig(level=logging.DEBUG, stream=sys.stdout) + tasks = [] # Initialize and start Telegram adapter - tg_adapter = TelegramAdapter() - await tg_adapter.start() + if config.Config().get_config_value('start_telegram_bot', True): + tg_adapter = TelegramAdapter() + tasks.append(tg_adapter.start()) + if config.Config().get_config_value('also_start_matrix_bot', False): + import adapters.matrix as matrix_bot + # Initialize and start Matrix bot if configured + tasks.append(matrix_bot.main()) + if tasks: + await asyncio.gather(*tasks, return_exceptions=True) + else: + logging.error("No bot is configured to start. Please check your configuration.") if __name__ == "__main__":