Различия
Показаны различия между двумя версиями страницы.
| Следующая версия | Предыдущая версия | ||
| vm:matrix-bot:01-install [2025/11/29 17:55] – создано admin | vm:matrix-bot:01-install [2025/12/01 12:34] (текущий) – [Полный код бота] admin | ||
|---|---|---|---|
| Строка 213: | Строка 213: | ||
| Ты помощник Администратора (Николай). | Ты помощник Администратора (Николай). | ||
| Отвечай кратко, | Отвечай кратко, | ||
| - | Обязательно используй контекст предыдущих сообщений для лучшего ответа. | + | Обязательно используй контекст предыдущих сообщений для лучшего ответа. |
| + | |||
| + | ПРАВИЛА ЧАТА (сообщай их ТОЛЬКО если тебя | ||
| + | 1. Сообщения | ||
| + | 2. Все сообщения | ||
| + | 3. В чате есть AI ассистент - может помочь с вопросами. | ||
| + | 4. Соблюдай законодательство РФ. | ||
| </ | </ | ||
| Строка 659: | Строка 665: | ||
| 2. Проверьте что у вас есть баланс в OpenAI | 2. Проверьте что у вас есть баланс в OpenAI | ||
| 3. Проверьте что используется правильная модель (gpt-4o-mini) | 3. Проверьте что используется правильная модель (gpt-4o-mini) | ||
| + | |||
| + | ===== Matrix Bot сервис (Python) ====== | ||
| + | |||
| + | ==== Описание ==== | ||
| + | |||
| + | Matrix Bot - это Python сервис, | ||
| + | * Получает новые сообщения из чатов | ||
| + | * Отправляет их в n8n webhook для обработки AI | ||
| + | * Получает ответы через HTTP API и отправляет в Matrix | ||
| + | * Поддерживает шифрованные комнаты (Megolm) | ||
| + | * Приветствует новых пользователей в комнате " | ||
| + | |||
| + | ==== Требования ==== | ||
| + | |||
| + | * Python 3.9+ | ||
| + | * python-nio (Matrix client library) | ||
| + | * aiohttp (async HTTP client) | ||
| + | * python-dotenv (для конфигурации) | ||
| + | |||
| + | ==== Установка ==== | ||
| + | |||
| + | <code bash> | ||
| + | pip install python-nio aiohttp python-dotenv | ||
| + | </ | ||
| + | |||
| + | ==== Конфигурация (.env файл) ==== | ||
| + | |||
| + | < | ||
| + | MATRIX_HOMESERVER=https:// | ||
| + | MATRIX_USER=@bot: | ||
| + | MATRIX_PASSWORD=your_password_here | ||
| + | PICKLE_KEY=your_encryption_key_here | ||
| + | N8N_WEBHOOK_URL=http:// | ||
| + | N8N_TIMEOUT=30 | ||
| + | HTTP_API_PORT=5000 | ||
| + | </ | ||
| + | |||
| + | ==== Полный код бота ==== | ||
| + | |||
| + | **Файл: matrix_bot.py** | ||
| + | |||
| + | <code python> | ||
| + | import asyncio | ||
| + | import os | ||
| + | import logging | ||
| + | from logging.handlers import RotatingFileHandler | ||
| + | import json | ||
| + | import aiohttp | ||
| + | from pathlib import Path | ||
| + | from aiohttp import web | ||
| + | from nio import ( | ||
| + | AsyncClient, | ||
| + | AsyncClientConfig, | ||
| + | RoomMessageText, | ||
| + | MegolmEvent, | ||
| + | InviteMemberEvent, | ||
| + | RoomMemberEvent, | ||
| + | LoginResponse, | ||
| + | KeyVerificationStart, | ||
| + | KeyVerificationCancel, | ||
| + | KeyVerificationKey, | ||
| + | KeyVerificationMac, | ||
| + | LocalProtocolError, | ||
| + | ) | ||
| + | |||
| + | # ===== Настройка логирования ===== | ||
| + | os.makedirs('/ | ||
| + | |||
| + | formatter = logging.Formatter( | ||
| + | ' | ||
| + | ) | ||
| + | |||
| + | file_handler = RotatingFileHandler( | ||
| + | '/ | ||
| + | maxBytes=50*1024*1024, | ||
| + | backupCount=10, | ||
| + | encoding=' | ||
| + | ) | ||
| + | file_handler.setFormatter(formatter) | ||
| + | file_handler.setLevel(logging.INFO) | ||
| + | |||
| + | console_handler = logging.StreamHandler() | ||
| + | console_handler.setFormatter(formatter) | ||
| + | console_handler.setLevel(logging.INFO) | ||
| + | |||
| + | root_logger = logging.getLogger() | ||
| + | root_logger.setLevel(logging.INFO) | ||
| + | root_logger.addHandler(file_handler) | ||
| + | root_logger.addHandler(console_handler) | ||
| + | |||
| + | logger = logging.getLogger(" | ||
| + | |||
| + | # ===== Конфигурация ===== | ||
| + | HOMESERVER = os.getenv(" | ||
| + | USER = os.getenv(" | ||
| + | PASSWORD = os.getenv(" | ||
| + | STORE_PATH = "/ | ||
| + | PICKLE_KEY = os.getenv(" | ||
| + | BOT_NICKNAME = " | ||
| + | CREDENTIALS_FILE = os.path.join(STORE_PATH, | ||
| + | |||
| + | # n8n интеграция | ||
| + | N8N_WEBHOOK_URL = os.getenv(" | ||
| + | N8N_TIMEOUT = int(os.getenv(" | ||
| + | |||
| + | # HTTP API для ответов от n8n | ||
| + | HTTP_API_PORT = int(os.getenv(" | ||
| + | |||
| + | # Настройка клиента с шифрованием | ||
| + | config = AsyncClientConfig( | ||
| + | encryption_enabled=True, | ||
| + | pickle_key=PICKLE_KEY, | ||
| + | store_sync_tokens=True | ||
| + | ) | ||
| + | |||
| + | # Глобальные переменные | ||
| + | client = None | ||
| + | app = None | ||
| + | http_runner = None | ||
| + | sync_error_count = 0 | ||
| + | |||
| + | # ===== НОВОЕ: Функция очистки sync токенов ===== | ||
| + | def clear_sync_tokens(): | ||
| + | """ | ||
| + | try: | ||
| + | store_path = Path(STORE_PATH) | ||
| + | | ||
| + | # Список файлов токенов которые нужно удалить | ||
| + | token_files = [ | ||
| + | store_path / " | ||
| + | store_path / " | ||
| + | ] | ||
| + | | ||
| + | for token_file in token_files: | ||
| + | if token_file.exists(): | ||
| + | token_file.unlink() | ||
| + | logger.info(f" | ||
| + | | ||
| + | # Удаляем базу комнат для полной пересинхронизации | ||
| + | db_file = store_path / " | ||
| + | if db_file.exists(): | ||
| + | db_file.unlink() | ||
| + | logger.info(f" | ||
| + | | ||
| + | logger.info(" | ||
| + | | ||
| + | except Exception as e: | ||
| + | logger.warning(f" | ||
| + | |||
| + | # ===== HTTP API обработчики ===== | ||
| + | async def send_message_handler(request): | ||
| + | """ | ||
| + | try: | ||
| + | data = await request.json() | ||
| + | room_id = data.get(" | ||
| + | message = data.get(" | ||
| + | reply_to_event_id = data.get(" | ||
| + | sender_id = data.get(" | ||
| + | sender_name = data.get(" | ||
| + | | ||
| + | if not room_id or not message: | ||
| + | return web.json_response( | ||
| + | {" | ||
| + | status=400 | ||
| + | ) | ||
| + | | ||
| + | logger.info(f" | ||
| + | logger.info(f" | ||
| + | if reply_to_event_id: | ||
| + | logger.info(f" | ||
| + | if sender_id: | ||
| + | logger.info(f" | ||
| + | | ||
| + | # Формируем content сообщения | ||
| + | content = { | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | | ||
| + | # Если есть sender_id, добавляем HTML mention | ||
| + | if sender_id and sender_name: | ||
| + | content[" | ||
| + | content[" | ||
| + | | ||
| + | # Добавляем reply-to если указан event_id | ||
| + | if reply_to_event_id: | ||
| + | content[" | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | } | ||
| + | if " | ||
| + | content[" | ||
| + | content[" | ||
| + | content[" | ||
| + | | ||
| + | # Отправляем сообщение в Matrix | ||
| + | try: | ||
| + | await client.room_send( | ||
| + | room_id=room_id, | ||
| + | message_type=" | ||
| + | content=content | ||
| + | ) | ||
| + | logger.info(f" | ||
| + | return web.json_response({" | ||
| + | except LocalProtocolError as e: | ||
| + | logger.error(f" | ||
| + | return web.json_response( | ||
| + | {" | ||
| + | status=500 | ||
| + | ) | ||
| + | except Exception as e: | ||
| + | logger.error(f" | ||
| + | return web.json_response( | ||
| + | {" | ||
| + | status=500 | ||
| + | ) | ||
| + | | ||
| + | except json.JSONDecodeError: | ||
| + | return web.json_response( | ||
| + | {" | ||
| + | status=400 | ||
| + | ) | ||
| + | except Exception as e: | ||
| + | logger.error(f" | ||
| + | return web.json_response( | ||
| + | {" | ||
| + | status=500 | ||
| + | ) | ||
| + | |||
| + | async def health_handler(request): | ||
| + | """ | ||
| + | is_connected = client is not None and client.logged_in if client else False | ||
| + | return web.json_response({ | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | }) | ||
| + | |||
| + | # ===== Функции ===== | ||
| + | def save_credentials(device_id, | ||
| + | """ | ||
| + | try: | ||
| + | Path(STORE_PATH).mkdir(parents=True, | ||
| + | with open(CREDENTIALS_FILE, | ||
| + | json.dump({ | ||
| + | " | ||
| + | " | ||
| + | }, f) | ||
| + | logger.info(f" | ||
| + | except Exception as e: | ||
| + | logger.error(f" | ||
| + | |||
| + | def load_credentials(): | ||
| + | """ | ||
| + | try: | ||
| + | if os.path.exists(CREDENTIALS_FILE): | ||
| + | with open(CREDENTIALS_FILE, | ||
| + | creds = json.load(f) | ||
| + | logger.info(f" | ||
| + | return creds.get(" | ||
| + | except Exception as e: | ||
| + | logger.warning(f" | ||
| + | return None, None | ||
| + | |||
| + | async def send_to_n8n(text, | ||
| + | """ | ||
| + | payload = { | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | | ||
| + | try: | ||
| + | async with aiohttp.ClientSession() as session: | ||
| + | async with session.post( | ||
| + | N8N_WEBHOOK_URL, | ||
| + | json=payload, | ||
| + | timeout=aiohttp.ClientTimeout(total=N8N_TIMEOUT) | ||
| + | ) as resp: | ||
| + | if resp.status == 200: | ||
| + | result = await resp.json() | ||
| + | logger.info(f" | ||
| + | return result | ||
| + | else: | ||
| + | text_resp = await resp.text() | ||
| + | logger.error(f" | ||
| + | return None | ||
| + | except asyncio.TimeoutError: | ||
| + | logger.error(f" | ||
| + | return None | ||
| + | except Exception as e: | ||
| + | logger.error(f" | ||
| + | return None | ||
| + | |||
| + | async def message_callback(room, | ||
| + | """ | ||
| + | logger.info(f" | ||
| + | | ||
| + | # Расшифровка Megolm-сообщений | ||
| + | if isinstance(event, | ||
| + | logger.info(f" | ||
| + | try: | ||
| + | event = await client.decrypt_event(event) | ||
| + | if not event: | ||
| + | logger.warning(f" | ||
| + | return | ||
| + | logger.info(f" | ||
| + | except Exception as e: | ||
| + | logger.error(f" | ||
| + | return | ||
| + | | ||
| + | if not isinstance(event, | ||
| + | logger.debug(f" | ||
| + | return | ||
| + | | ||
| + | if event.sender == client.user_id: | ||
| + | logger.debug(f" | ||
| + | return | ||
| + | | ||
| + | text = (event.body or "" | ||
| + | if not text: | ||
| + | return | ||
| + | | ||
| + | logger.info(f" | ||
| + | logger.info(f" | ||
| + | | ||
| + | # ===== ИСПРАВЛЕНО: | ||
| + | member_count = getattr(room, | ||
| + | | ||
| + | # Если member_count None, пробуем другие способы | ||
| + | if member_count is None or member_count == 0: | ||
| + | member_count = len(room.users) if hasattr(room, | ||
| + | | ||
| + | if member_count == 0 and hasattr(room, | ||
| + | member_count = getattr(room, | ||
| + | | ||
| + | if member_count == 0 and hasattr(room, | ||
| + | member_count = getattr(room.summary, | ||
| + | | ||
| + | # ===== КРИТИЧНО: | ||
| + | # Если всё ещё None (после перезагрузки/ | ||
| + | if member_count is None: | ||
| + | logger.warning(f" | ||
| + | member_count = 2 | ||
| + | | ||
| + | logger.info(f" | ||
| + | | ||
| + | # Определяем тип комнаты | ||
| + | if member_count == 2: | ||
| + | is_dm = True | ||
| + | logger.info(f" | ||
| + | elif member_count <= 1: # ← Теперь member_count никогда не будет None! | ||
| + | is_dm = True | ||
| + | logger.info(f" | ||
| + | else: | ||
| + | is_dm = False | ||
| + | logger.info(f" | ||
| + | | ||
| + | # Проверяем нужно ли отвечать | ||
| + | if is_dm: | ||
| + | cleaned = text | ||
| + | logger.info(f" | ||
| + | else: | ||
| + | # В группе только на обращение | ||
| + | if not text.lower().startswith(f" | ||
| + | logger.info(f" | ||
| + | return | ||
| + | cleaned = text[len(f" | ||
| + | if not cleaned: | ||
| + | return | ||
| + | logger.info(f" | ||
| + | | ||
| + | # Отправляем сообщение в n8n для обработки AI | ||
| + | logger.info(f" | ||
| + | | ||
| + | # Получаем имя отправителя из комнаты | ||
| + | sender_name = None | ||
| + | if event.sender in room.users: | ||
| + | user = room.users[event.sender] | ||
| + | sender_name = user.name or event.sender | ||
| + | | ||
| + | result = await send_to_n8n(cleaned, | ||
| + | | ||
| + | if not result: | ||
| + | fallback_reply = " | ||
| + | try: | ||
| + | await client.room_send( | ||
| + | room_id=room.room_id, | ||
| + | message_type=" | ||
| + | content={" | ||
| + | ) | ||
| + | logger.warning(f" | ||
| + | except Exception as e: | ||
| + | logger.error(f" | ||
| + | |||
| + | async def invite_callback(room, | ||
| + | """ | ||
| + | try: | ||
| + | await client.join(room.room_id) | ||
| + | logger.info(f" | ||
| + | except Exception as e: | ||
| + | logger.exception(f" | ||
| + | |||
| + | async def member_callback(room, | ||
| + | """ | ||
| + | try: | ||
| + | if event.membership != " | ||
| + | return | ||
| + | | ||
| + | if event.state_key == client.user_id: | ||
| + | logger.debug(f" | ||
| + | return | ||
| + | | ||
| + | room_name = room.display_name or room.name or "" | ||
| + | logger.info(f" | ||
| + | | ||
| + | if room_name.lower() != " | ||
| + | logger.debug(f" | ||
| + | return | ||
| + | | ||
| + | new_user_id = event.state_key | ||
| + | new_user_name = new_user_id | ||
| + | | ||
| + | if new_user_id in room.users: | ||
| + | user = room.users[new_user_id] | ||
| + | new_user_name = user.name or new_user_id | ||
| + | | ||
| + | logger.info(f" | ||
| + | | ||
| + | plain_message = f" | ||
| + | | ||
| + | html_message = f'< | ||
| + | | ||
| + | await client.room_send( | ||
| + | room_id=room.room_id, | ||
| + | message_type=" | ||
| + | content={ | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | ) | ||
| + | | ||
| + | logger.info(f" | ||
| + | | ||
| + | except Exception as e: | ||
| + | logger.error(f" | ||
| + | logger.exception(e) | ||
| + | |||
| + | async def to_device_callback(event): | ||
| + | """ | ||
| + | logger.debug(f" | ||
| + | |||
| + | # ===== HTTP сервер ===== | ||
| + | async def start_http_server(): | ||
| + | """ | ||
| + | global app, http_runner | ||
| + | | ||
| + | app = web.Application() | ||
| + | app.router.add_post('/ | ||
| + | app.router.add_get('/ | ||
| + | | ||
| + | http_runner = web.AppRunner(app) | ||
| + | await http_runner.setup() | ||
| + | site = web.TCPSite(http_runner, | ||
| + | await site.start() | ||
| + | logger.info(f" | ||
| + | logger.info(f" | ||
| + | |||
| + | # ===== Основной цикл ===== | ||
| + | async def main(): | ||
| + | global client, sync_error_count | ||
| + | | ||
| + | logger.info(f" | ||
| + | logger.info(f" | ||
| + | logger.info(f" | ||
| + | logger.info(f" | ||
| + | | ||
| + | # Загружаем сохранённые credentials | ||
| + | device_id, access_token = load_credentials() | ||
| + | | ||
| + | # Создаём клиента с сохранёнными данными | ||
| + | if device_id and access_token: | ||
| + | client = AsyncClient( | ||
| + | HOMESERVER, | ||
| + | USER, | ||
| + | config=config, | ||
| + | store_path=STORE_PATH, | ||
| + | device_id=device_id | ||
| + | ) | ||
| + | client.access_token = access_token | ||
| + | client.user_id = USER | ||
| + | logger.info(f" | ||
| + | else: | ||
| + | client = AsyncClient(HOMESERVER, | ||
| + | client.user_id = USER | ||
| + | logger.info(" | ||
| + | | ||
| + | if not PASSWORD: | ||
| + | logger.error(" | ||
| + | return | ||
| + | | ||
| + | login_resp = await client.login(PASSWORD, | ||
| + | if not isinstance(login_resp, | ||
| + | logger.error(f" | ||
| + | return | ||
| + | logger.info(f" | ||
| + | | ||
| + | save_credentials(client.device_id, | ||
| + | | ||
| + | # Загрузка crypto store | ||
| + | try: | ||
| + | client.load_store() | ||
| + | logger.info(" | ||
| + | except Exception as e: | ||
| + | logger.warning(f" | ||
| + | | ||
| + | # ===== НОВОЕ: Первичная синхронизация с обработкой next_batch ошибок ===== | ||
| + | logger.info(" | ||
| + | max_sync_retries = 3 | ||
| + | for sync_attempt in range(max_sync_retries): | ||
| + | try: | ||
| + | sync_response = await client.sync(timeout=30000, | ||
| + | logger.info(f" | ||
| + | sync_error_count = 0 | ||
| + | break | ||
| + | | ||
| + | except Exception as e: | ||
| + | error_str = str(e) | ||
| + | sync_attempt_num = sync_attempt + 1 | ||
| + | | ||
| + | # Проверяем на ошибку next_batch | ||
| + | if " | ||
| + | logger.error(f" | ||
| + | logger.warning(f" | ||
| + | | ||
| + | if sync_attempt_num < max_sync_retries: | ||
| + | logger.info(f" | ||
| + | clear_sync_tokens() | ||
| + | await asyncio.sleep(5) | ||
| + | else: | ||
| + | logger.error(f" | ||
| + | logger.info(" | ||
| + | break | ||
| + | else: | ||
| + | logger.error(f" | ||
| + | if sync_attempt_num < max_sync_retries: | ||
| + | await asyncio.sleep(5) | ||
| + | else: | ||
| + | logger.error(f" | ||
| + | logger.info(" | ||
| + | break | ||
| + | | ||
| + | # Загрузка ключей шифрования при необходимости | ||
| + | if client.should_upload_keys: | ||
| + | logger.info(" | ||
| + | try: | ||
| + | await client.keys_upload() | ||
| + | logger.info(" | ||
| + | except Exception as e: | ||
| + | logger.warning(f" | ||
| + | | ||
| + | # Запрос ключей для шифрованных комнат | ||
| + | logger.info(" | ||
| + | try: | ||
| + | await client.keys_query() | ||
| + | shared_count = 0 | ||
| + | for room_id, room in client.rooms.items(): | ||
| + | if room.encrypted: | ||
| + | try: | ||
| + | await client.share_group_session(room_id, | ||
| + | shared_count += 1 | ||
| + | except Exception as e: | ||
| + | logger.warning(f" | ||
| + | if shared_count > 0: | ||
| + | logger.info(f" | ||
| + | except Exception as e: | ||
| + | if "No key query required" | ||
| + | logger.warning(f" | ||
| + | | ||
| + | # Регистрируем callbacks для новых событий | ||
| + | client.add_event_callback(message_callback, | ||
| + | client.add_event_callback(message_callback, | ||
| + | client.add_event_callback(invite_callback, | ||
| + | client.add_event_callback(member_callback, | ||
| + | client.add_to_device_callback(to_device_callback, | ||
| + | (KeyVerificationStart, | ||
| + | | ||
| + | | ||
| + | | ||
| + | | ||
| + | # Запускаем HTTP сервер | ||
| + | await start_http_server() | ||
| + | | ||
| + | logger.info(f" | ||
| + | logger.info(f" | ||
| + | | ||
| + | try: | ||
| + | # ===== НОВОЕ: Основной цикл с обработкой ошибок next_batch ===== | ||
| + | consecutive_errors = 0 | ||
| + | while True: | ||
| + | try: | ||
| + | await client.sync(timeout=30000) | ||
| + | consecutive_errors = 0 # Reset при успехе | ||
| + | | ||
| + | except Exception as e: | ||
| + | error_str = str(e) | ||
| + | consecutive_errors += 1 | ||
| + | | ||
| + | # Проверяем на ошибку next_batch | ||
| + | if " | ||
| + | logger.warning(f" | ||
| + | | ||
| + | if consecutive_errors == 1: | ||
| + | logger.info(" | ||
| + | clear_sync_tokens() | ||
| + | | ||
| + | # Экспоненциальная задержка: | ||
| + | delay = min(5 * (2 ** (consecutive_errors - 1)), 120) | ||
| + | logger.info(f" | ||
| + | await asyncio.sleep(delay) | ||
| + | | ||
| + | if consecutive_errors > 5: | ||
| + | logger.error(" | ||
| + | break | ||
| + | else: | ||
| + | # Другие ошибки | ||
| + | logger.error(f" | ||
| + | | ||
| + | if consecutive_errors > 3: | ||
| + | logger.warning(f" | ||
| + | await asyncio.sleep(30) | ||
| + | else: | ||
| + | await asyncio.sleep(10) | ||
| + | | ||
| + | except KeyboardInterrupt: | ||
| + | logger.info(" | ||
| + | finally: | ||
| + | if http_runner: | ||
| + | await http_runner.cleanup() | ||
| + | if client: | ||
| + | await client.close() | ||
| + | logger.info(" | ||
| + | |||
| + | if __name__ == " | ||
| + | try: | ||
| + | asyncio.run(main()) | ||
| + | except KeyboardInterrupt: | ||
| + | logger.info(" | ||
| + | except Exception as e: | ||
| + | logger.exception(f" | ||
| + | </ | ||
| + | |||
| + | ==== Запуск бота ==== | ||
| + | |||
| + | <code bash> | ||
| + | # Создаём .env файл с конфигурацией | ||
| + | cat > .env << EOF | ||
| + | MATRIX_HOMESERVER=https:// | ||
| + | MATRIX_USER=@bot: | ||
| + | MATRIX_PASSWORD=your_password | ||
| + | PICKLE_KEY=your_encryption_key | ||
| + | N8N_WEBHOOK_URL=http:// | ||
| + | N8N_TIMEOUT=30 | ||
| + | HTTP_API_PORT=5000 | ||
| + | EOF | ||
| + | |||
| + | # Запускаем бот | ||
| + | python matrix_bot.py | ||
| + | </ | ||
| + | |||
| + | ==== Использование с Docker ==== | ||
| + | |||
| + | **requirements.txt: | ||
| + | |||
| + | < | ||
| + | python-nio> | ||
| + | aiohttp> | ||
| + | python-dotenv> | ||
| + | </ | ||
| + | |||
| + | **Dockerfile: | ||
| + | |||
| + | <code dockerfile> | ||
| + | FROM python: | ||
| + | |||
| + | WORKDIR /app | ||
| + | |||
| + | COPY requirements.txt . | ||
| + | RUN pip install --no-cache-dir -r requirements.txt | ||
| + | |||
| + | COPY matrix_bot.py . | ||
| + | |||
| + | CMD [" | ||
| + | </ | ||
| + | |||
| + | ==== Основные функции ==== | ||
| + | |||
| + | ==== Message Callback ==== | ||
| + | |||
| + | Обрабатывает текстовые сообщения: | ||
| + | * Расшифровывает Megolm-сообщения (из шифрованных комнат) | ||
| + | * Определяет тип комнаты (DM или GROUP) | ||
| + | * В DM отвечает на все сообщения | ||
| + | * В GROUP отвечает только на обращение ('' | ||
| + | * Отправляет в n8n webhook | ||
| + | |||
| + | ==== Send to n8n ==== | ||
| + | |||
| + | Отправляет сообщение в n8n: | ||
| + | * text - очищенный текст | ||
| + | * room_id - ID комнаты | ||
| + | * sender - Matrix ID отправителя | ||
| + | * sender_name - Имя отправителя | ||
| + | * is_dm - флаг типа комнаты | ||
| + | * event_id - ID события | ||
| + | |||
| + | ==== Send Message Handler ==== | ||
| + | |||
| + | HTTP endpoint для ответов от n8n: | ||
| + | * Получает сообщение и параметры | ||
| + | * Добавляет HTML mention если есть sender_id | ||
| + | * Отправляет в Matrix комнату | ||
| ===== Технические детали ====== | ===== Технические детали ====== | ||