vm:matrix-bot:01-install

Различия

Показаны различия между двумя версиями страницы.

Ссылка на это сравнение

Предыдущая версия справа и слева Предыдущая версия
Следующая версия
Предыдущая версия
vm:matrix-bot:01-install [2025/11/29 17:59] adminvm:matrix-bot:01-install [2025/12/01 12:34] (текущий) – [Полный код бота] admin
Строка 213: Строка 213:
 Ты помощник Администратора (Николай). Ты помощник Администратора (Николай).
 Отвечай кратко, используй emoji. Отвечай кратко, используй emoji.
-Обязательно используй контекст предыдущих сообщений для лучшего ответа. Отвечай с числовыми результатами и референциями на предыдущие вычисления.+Обязательно используй контекст предыдущих сообщений для лучшего ответа. 
 + 
 +ПРАВИЛА ЧАТА (сообщай их ТОЛЬКО если тебя о них спросили есть они или нет или просят сказать какие правила в этом чате, перечисляй только эти правила и не добавляй другие от себя). 
 +1. Сообщения в чате сохраняются одну неделю. 
 +2. Все сообщения старше недели удаляются. 
 +3. В чате есть AI ассистент - может помочь с вопросами. 
 +4. Соблюдай законодательство РФ.
 </code> </code>
  
Строка 704: Строка 710:
 import os import os
 import logging import logging
 +from logging.handlers import RotatingFileHandler
 import json import json
 import aiohttp import aiohttp
Строка 723: Строка 730:
 ) )
  
-logging.basicConfig+# ===== Настройка логирования ===== 
-    level=logging.INFO, +os.makedirs('/app/logs', exist_ok=True) 
-    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'+ 
 +formatter = logging.Formatter( 
 +    '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
 ) )
 +
 +file_handler = RotatingFileHandler(
 +    '/app/logs/matrix_bot.log',
 +    maxBytes=50*1024*1024,
 +    backupCount=10,
 +    encoding='utf-8'
 +)
 +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("matrix-bot") logger = logging.getLogger("matrix-bot")
  
Строка 756: Строка 784:
 app = None app = None
 http_runner = None http_runner = None
 +sync_error_count = 0
 +
 +# ===== НОВОЕ: Функция очистки sync токенов =====
 +def clear_sync_tokens():
 +    """Очищает sync токены для полной пересинхронизации после перезагрузки"""
 +    try:
 +        store_path = Path(STORE_PATH)
 +        
 +        # Список файлов токенов которые нужно удалить
 +        token_files = [
 +            store_path / "sync_token.json",
 +            store_path / ".matrix-nio-sync-token"
 +        ]
 +        
 +        for token_file in token_files:
 +            if token_file.exists():
 +                token_file.unlink()
 +                logger.info(f"🗑️ Удалён sync token файл: {token_file}")
 +        
 +        # Удаляем базу комнат для полной пересинхронизации
 +        db_file = store_path / "rooms.json"
 +        if db_file.exists():
 +            db_file.unlink()
 +            logger.info(f"🗑️ Удалена база комнат для пересинхронизации")
 +        
 +        logger.info("✅ Sync токены очищены, будет выполнена полная пересинхронизация")
 +        
 +    except Exception as e:
 +        logger.warning(f"⚠️ Ошибка очистки sync tokens: {e}")
  
 # ===== HTTP API обработчики ===== # ===== HTTP API обработчики =====
Строка 764: Строка 821:
         room_id = data.get("room_id")         room_id = data.get("room_id")
         message = data.get("message")         message = data.get("message")
 +        reply_to_event_id = data.get("reply_to_event_id")
         sender_id = data.get("sender_id")         sender_id = data.get("sender_id")
         sender_name = data.get("sender_name")         sender_name = data.get("sender_name")
Строка 775: Строка 833:
         logger.info(f"📤 Получен запрос отправить сообщение в комнату {room_id}")         logger.info(f"📤 Получен запрос отправить сообщение в комнату {room_id}")
         logger.info(f"💬 Текст: {message[:100]}")         logger.info(f"💬 Текст: {message[:100]}")
 +        if reply_to_event_id:
 +            logger.info(f"↩️ Reply to event: {reply_to_event_id}")
         if sender_id:         if sender_id:
             logger.info(f"👤 Mention user: {sender_name} ({sender_id})")             logger.info(f"👤 Mention user: {sender_name} ({sender_id})")
Строка 788: Строка 848:
             content["format"] = "org.matrix.custom.html"             content["format"] = "org.matrix.custom.html"
             content["formatted_body"] = f'<a href="https://matrix.to/#/{sender_id}">{sender_name}</a>: {message}'             content["formatted_body"] = f'<a href="https://matrix.to/#/{sender_id}">{sender_name}</a>: {message}'
 +        
 +        # Добавляем reply-to если указан event_id
 +        if reply_to_event_id:
 +            content["m.relates_to"] = {
 +                "m.in_reply_to": {
 +                    "event_id": reply_to_event_id
 +                }
 +            }
 +            if "formatted_body" not in content:
 +                content["format"] = "org.matrix.custom.html"
 +                content["formatted_body"] = message
 +            content["formatted_body"] = f'<mx-reply><blockquote><a href="https://matrix.to/#/{room_id}/{reply_to_event_id}">In reply to</a></blockquote></mx-reply>{content["formatted_body"]}'
                  
         # Отправляем сообщение в Matrix         # Отправляем сообщение в Matrix
Строка 798: Строка 870:
             logger.info(f"✅ Сообщение отправлено в {room_id}")             logger.info(f"✅ Сообщение отправлено в {room_id}")
             return web.json_response({"status": "ok", "room_id": room_id})             return web.json_response({"status": "ok", "room_id": room_id})
 +        except LocalProtocolError as e:
 +            logger.error(f"❌ Ошибка отправки (protocol): {e}")
 +            return web.json_response(
 +                {"error": f"Protocol error: {str(e)}"},
 +                status=500
 +            )
         except Exception as e:         except Exception as e:
             logger.error(f"❌ Ошибка отправки: {e}")             logger.error(f"❌ Ошибка отправки: {e}")
Строка 915: Строка 993:
          
     logger.info(f"💬 Message text: {text[:100]}")     logger.info(f"💬 Message text: {text[:100]}")
 +    logger.info(f"🆔 Event ID: {event.event_id}")
          
-    # Определяем количество участников+    # ===== ИСПРАВЛЕНО: Правильная обработка None в member_count =====
     member_count = getattr(room, 'member_count', None)     member_count = getattr(room, 'member_count', None)
          
 +    # Если member_count None, пробуем другие способы
     if member_count is None or member_count == 0:     if member_count is None or member_count == 0:
         member_count = len(room.users) if hasattr(room, 'users') and room.users else 0         member_count = len(room.users) if hasattr(room, 'users') and room.users else 0
Строка 924: Строка 1004:
     if member_count == 0 and hasattr(room, 'joined_count'):     if member_count == 0 and hasattr(room, 'joined_count'):
         member_count = getattr(room, 'joined_count', 0)         member_count = getattr(room, 'joined_count', 0)
 +    
 +    if member_count == 0 and hasattr(room, 'summary'):
 +        member_count = getattr(room.summary, 'joined_member_count', 0)
 +    
 +    # ===== КРИТИЧНО: Проверка что member_count не None перед сравнением =====
 +    # Если всё ещё None (после перезагрузки/бекапа), используем default значение
 +    if member_count is None:
 +        logger.warning(f"⚠️ member_count is None (после перезагрузки?), используем default=2 (DM)")
 +        member_count = 2
          
     logger.info(f"🔍 Room info: members={member_count}, name={room.display_name or room.name or 'None'}")     logger.info(f"🔍 Room info: members={member_count}, name={room.display_name or room.name or 'None'}")
Строка 931: Строка 1020:
         is_dm = True         is_dm = True
         logger.info(f"🔍 Room type: DM (2 members)")         logger.info(f"🔍 Room type: DM (2 members)")
-    elif member_count <= 1:+    elif member_count <= 1:  # ← Теперь member_count никогда не будет None!
         is_dm = True         is_dm = True
-        logger.info(f"🔍 Room type: DM (fallback)")+        logger.info(f"🔍 Room type: DM (fallback, members={member_count})")
     else:     else:
         is_dm = False         is_dm = False
Строка 943: Строка 1032:
         logger.info(f"💬 DM message, sending to n8n")         logger.info(f"💬 DM message, sending to n8n")
     else:     else:
 +        # В группе только на обращение
         if not text.lower().startswith(f"{BOT_NICKNAME.lower()}:"):         if not text.lower().startswith(f"{BOT_NICKNAME.lower()}:"):
             logger.info(f"⏭️ Ignoring group message without mention")             logger.info(f"⏭️ Ignoring group message without mention")
Строка 983: Строка 1073:
  
 async def member_callback(room, event): async def member_callback(room, event):
-    """Обработка событий присоединения новых пользователей"""+    """Обработка событий присоединения/выхода пользователей"""
     try:     try:
         if event.membership != "join":         if event.membership != "join":
Строка 1006: Строка 1096:
             new_user_name = user.name or new_user_id             new_user_name = user.name or new_user_id
                  
-        logger.info(f"👋 Sending welcome message to {new_user_name}")+        logger.info(f"👋 Sending welcome message to {new_user_name} in {room_name}")
                  
-        plain_message = f"{new_user_name}: Добро пожаловать в чат!"+        plain_message = f"{new_user_name}: Добро пожаловать в чат! Я AI ChatBot и могу помочь с различными вопросами, если обратиться ко мне (в чате необходимо нажать на мое имя что бы написать мне вопрос). Приватные чаты шифруются. Срок хранения переписки на сервере одна неделя."
                  
-        html_message = f'<a href="https://matrix.to/#/{new_user_id}">{new_user_name}</a>: Добро пожаловать в чат!'+        html_message = f'<a href="https://matrix.to/#/{new_user_id}">{new_user_name}</a>: Добро пожаловать в чат! Я AI ChatBot и могу помочь с различными вопросами, если обратиться ко мне (в чате необходимо нажать на мое имя что бы написать мне вопрос). Приватные чаты шифруются. Срок хранения переписки на сервере одна неделя.'
                  
         await client.room_send(         await client.room_send(
Строка 1023: Строка 1113:
         )         )
                  
-        logger.info(f"✅ Welcome message sent")+        logger.info(f"✅ Welcome message sent to {new_user_name}")
                  
     except Exception as e:     except Exception as e:
         logger.error(f"❌ Ошибка в member_callback: {e}")         logger.error(f"❌ Ошибка в member_callback: {e}")
 +        logger.exception(e)
  
 async def to_device_callback(event): async def to_device_callback(event):
Строка 1046: Строка 1137:
     await site.start()     await site.start()
     logger.info(f"🌐 HTTP API запущен на порту {HTTP_API_PORT}")     logger.info(f"🌐 HTTP API запущен на порту {HTTP_API_PORT}")
 +    logger.info(f"📡 Endpoints: /send_message (POST), /health (GET)")
  
 # ===== Основной цикл ===== # ===== Основной цикл =====
 async def main(): async def main():
-    global client+    global client, sync_error_count
          
     logger.info(f"🚀 Запуск Matrix бота")     logger.info(f"🚀 Запуск Matrix бота")
Строка 1056: Строка 1148:
     logger.info(f"📡 n8n webhook: {N8N_WEBHOOK_URL}")     logger.info(f"📡 n8n webhook: {N8N_WEBHOOK_URL}")
          
 +    # Загружаем сохранённые credentials
     device_id, access_token = load_credentials()     device_id, access_token = load_credentials()
          
 +    # Создаём клиента с сохранёнными данными
     if device_id and access_token:     if device_id and access_token:
         client = AsyncClient(         client = AsyncClient(
Строка 1068: Строка 1162:
         client.access_token = access_token         client.access_token = access_token
         client.user_id = USER         client.user_id = USER
-        logger.info(f"✅ Используем существующую сессию")+        logger.info(f"✅ Используем существующую сессию, device_id: {device_id}")
     else:     else:
         client = AsyncClient(HOMESERVER, USER, config=config, store_path=STORE_PATH)         client = AsyncClient(HOMESERVER, USER, config=config, store_path=STORE_PATH)
         client.user_id = USER         client.user_id = USER
 +        logger.info("Выполняется первичный логин...")
                  
         if not PASSWORD:         if not PASSWORD:
Строка 1081: Строка 1176:
             logger.error(f"❌ Login failed: {login_resp}")             logger.error(f"❌ Login failed: {login_resp}")
             return             return
-        logger.info(f"✅ Залогинились как {USER}")+        logger.info(f"✅ Залогинились как {USER}, device_id: {client.device_id}")
                  
         save_credentials(client.device_id, client.access_token)         save_credentials(client.device_id, client.access_token)
          
 +    # Загрузка crypto store
     try:     try:
         client.load_store()         client.load_store()
         logger.info("✅ Crypto store загружен")         logger.info("✅ Crypto store загружен")
     except Exception as e:     except Exception as e:
-        logger.warning(f"Crypto store: {e}")+        logger.warning(f"Crypto store не загружен: {e}")
          
 +    # ===== НОВОЕ: Первичная синхронизация с обработкой next_batch ошибок =====
     logger.info("⏳ Выполняется первичная синхронизация...")     logger.info("⏳ Выполняется первичная синхронизация...")
-    try+    max_sync_retries = 3 
-        await client.sync(timeout=30000, full_state=True) +    for sync_attempt in range(max_sync_retries)
-        logger.info(f"✅ Синхронизация завершена"+        try: 
-    except Exception as e: +            sync_response = await client.sync(timeout=30000, full_state=True) 
-        logger.error(f"❌ Ошибка синхронизации: {e}"+            logger.info(f"✅ Первичная синхронизация завершена"
-        return+            sync_error_count = 0 
 +            break 
 +             
 +        except Exception as e: 
 +            error_str = str(e) 
 +            sync_attempt_num = sync_attempt + 1 
 +             
 +            # Проверяем на ошибку next_batch 
 +            if "next_batch" in error_str or "'next_batch' is a required property" in error_str: 
 +                logger.error(f"❌ Sync error (попытка {sync_attempt_num}/{max_sync_retries}): next_batch missing"
 +                logger.warning(f"🔄 Это происходит после перезагрузки - Matrix сервер вернул некорректный ответ"
 +                 
 +                if sync_attempt_num < max_sync_retries: 
 +                    logger.info(f"🗑️ Очищаю sync токены и пробую ещё раз..."
 +                    clear_sync_tokens() 
 +                    await asyncio.sleep(5)  # Дождёмся перед следующей попыткой 
 +                else: 
 +                    logger.error(f"❌ Не удалось выполнить синхронизацию после {max_sync_retries} попыток"
 +                    logger.info("💡 Попробуем продолжить и восстановиться в основном цикле..."
 +                    break 
 +            else: 
 +                logger.error(f"❌ Ошибка синхронизации (попытка {sync_attempt_num}/{max_sync_retries}): {e}"
 +                if sync_attempt_num < max_sync_retries: 
 +                    await asyncio.sleep(5) 
 +                else: 
 +                    logger.error(f"❌ Не удалось выполнить синхронизацию после {max_sync_retries} попыток"
 +                    logger.info("💡 Попробуем продолжить и восстановиться в основном цикле..."
 +                    break
          
 +    # Загрузка ключей шифрования при необходимости
     if client.should_upload_keys:     if client.should_upload_keys:
         logger.info("📤 Загрузка ключей шифрования...")         logger.info("📤 Загрузка ключей шифрования...")
         try:         try:
             await client.keys_upload()             await client.keys_upload()
-            logger.info("✅ Ключи загружены")+            logger.info("✅ Ключи шифрования загружены")
         except Exception as e:         except Exception as e:
-            logger.warning(f"⚠️ Ошибка ключей: {e}")+            logger.warning(f"⚠️ Ошибка загрузки ключей: {e}")
          
 +    # Запрос ключей для шифрованных комнат
 +    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, ignore_unverified_devices=True)
 +                    shared_count += 1
 +                except Exception as e:
 +                    logger.warning(f"⚠️ Share session error for {room_id}: {e}")
 +        if shared_count > 0:
 +            logger.info(f"🔐 Ключи общей сессии переданы для {shared_count} шифрованных комнат")
 +    except Exception as e:
 +        if "No key query required" not in str(e):
 +            logger.warning(f"⚠️ Keys query error: {e}")
 +    
 +    # Регистрируем callbacks для новых событий
     client.add_event_callback(message_callback, RoomMessageText)     client.add_event_callback(message_callback, RoomMessageText)
     client.add_event_callback(message_callback, MegolmEvent)     client.add_event_callback(message_callback, MegolmEvent)
Строка 1117: Строка 1261:
                                    KeyVerificationMac))                                    KeyVerificationMac))
          
 +    # Запускаем HTTP сервер
     await start_http_server()     await start_http_server()
          
-    logger.info(f"🤖 Бот готов к работе!")+    logger.info(f"🤖 Бот полностью готов к работе!") 
 +    logger.info(f"✅ Listening for messages...")
          
     try:     try:
 +        # ===== НОВОЕ: Основной цикл с обработкой ошибок next_batch =====
 +        consecutive_errors = 0
         while True:         while True:
             try:             try:
                 await client.sync(timeout=30000)                 await client.sync(timeout=30000)
 +                consecutive_errors = 0  # Reset при успехе
 +                
             except Exception as e:             except Exception as e:
-                logger.error(f"❌ Ошибка sync: {e}"+                error_str = str(e) 
-                await asyncio.sleep(30)+                consecutive_errors += 1 
 +                 
 +                # Проверяем на ошибку next_batch 
 +                if "next_batch" in error_str or "'next_batch' is a required property" in error_str: 
 +                    logger.warning(f"⚠️ Sync error #{consecutive_errors}: next_batch missing (обычно после перезагрузки)"
 +                     
 +                    if consecutive_errors == 1: 
 +                        logger.info("🗑️ Первая ошибка - очищаю sync токены..."
 +                        clear_sync_tokens() 
 +                     
 +                    # Экспоненциальная задержка: 5s, 10s, 20s, 40s... 
 +                    delay = min(5 * (2 ** (consecutive_errors - 1)), 120) 
 +                    logger.info(f"⏳ Жду {delay}s перед повторной попыткой..."
 +                    await asyncio.sleep(delay) 
 +                     
 +                    if consecutive_errors > 5: 
 +                        logger.error("❌ Слишком много consecutive ошибок (>5), контейнер перезагружается..."
 +                        break 
 +                else: 
 +                    # Другие ошибки 
 +                    logger.error(f"❌ Ошибка sync #{consecutive_errors}: {e}"
 +                     
 +                    if consecutive_errors > 3: 
 +                        logger.warning(f"⚠️ {consecutive_errors} ошибок подряд, даю больше времени..."
 +                        await asyncio.sleep(30) 
 +                    else: 
 +                        await asyncio.sleep(10) 
 +                    
     except KeyboardInterrupt:     except KeyboardInterrupt:
-        logger.info("⏹️ Завершение")+        logger.info("⏹️ Получен сигнал завершения")
     finally:     finally:
         if http_runner:         if http_runner:
Строка 1141: Строка 1318:
         asyncio.run(main())         asyncio.run(main())
     except KeyboardInterrupt:     except KeyboardInterrupt:
-        logger.info("Bot остановлен")+        logger.info("Bot остановлен пользователем")
     except Exception as e:     except Exception as e:
         logger.exception(f"❌ Fatal error: {e}")         logger.exception(f"❌ Fatal error: {e}")
  • vm/matrix-bot/01-install.1764439149.txt.gz
  • Последнее изменение: 2025/11/29 17:59
  • admin