Различия
Показаны различия между двумя версиями страницы.
| Предыдущая версия справа и слева Предыдущая версия Следующая версия | Предыдущая версия | ||
| vm:matrix-bot:01-install [2025/11/29 17:59] – admin | vm:matrix-bot:01-install [2025/12/01 12:34] (текущий) – [Полный код бота] admin | ||
|---|---|---|---|
| Строка 213: | Строка 213: | ||
| Ты помощник Администратора (Николай). | Ты помощник Администратора (Николай). | ||
| Отвечай кратко, | Отвечай кратко, | ||
| - | Обязательно используй контекст предыдущих сообщений для лучшего ответа. | + | Обязательно используй контекст предыдущих сообщений для лучшего ответа. |
| + | |||
| + | ПРАВИЛА ЧАТА (сообщай их ТОЛЬКО если тебя | ||
| + | 1. Сообщения | ||
| + | 2. Все сообщения | ||
| + | 3. В чате есть AI ассистент - может помочь с вопросами. | ||
| + | 4. Соблюдай законодательство РФ. | ||
| </ | </ | ||
| Строка 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('/ |
| - | | + | |
| + | 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(" | logger = logging.getLogger(" | ||
| Строка 756: | Строка 784: | ||
| app = None | app = None | ||
| http_runner = 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 обработчики ===== | # ===== HTTP API обработчики ===== | ||
| Строка 764: | Строка 821: | ||
| room_id = data.get(" | room_id = data.get(" | ||
| message = data.get(" | message = data.get(" | ||
| + | reply_to_event_id = data.get(" | ||
| sender_id = data.get(" | sender_id = data.get(" | ||
| sender_name = data.get(" | sender_name = data.get(" | ||
| Строка 775: | Строка 833: | ||
| logger.info(f" | logger.info(f" | ||
| logger.info(f" | logger.info(f" | ||
| + | if reply_to_event_id: | ||
| + | logger.info(f" | ||
| if sender_id: | if sender_id: | ||
| logger.info(f" | logger.info(f" | ||
| Строка 788: | Строка 848: | ||
| content[" | content[" | ||
| content[" | content[" | ||
| + | | ||
| + | # Добавляем reply-to если указан event_id | ||
| + | if reply_to_event_id: | ||
| + | content[" | ||
| + | " | ||
| + | " | ||
| + | } | ||
| + | } | ||
| + | if " | ||
| + | content[" | ||
| + | content[" | ||
| + | content[" | ||
| | | ||
| # Отправляем сообщение в Matrix | # Отправляем сообщение в Matrix | ||
| Строка 798: | Строка 870: | ||
| logger.info(f" | logger.info(f" | ||
| return web.json_response({" | return web.json_response({" | ||
| + | except LocalProtocolError as e: | ||
| + | logger.error(f" | ||
| + | return web.json_response( | ||
| + | {" | ||
| + | status=500 | ||
| + | ) | ||
| except Exception as e: | except Exception as e: | ||
| logger.error(f" | logger.error(f" | ||
| Строка 915: | Строка 993: | ||
| | | ||
| logger.info(f" | logger.info(f" | ||
| + | logger.info(f" | ||
| | | ||
| - | # Определяем количество участников | + | # ===== ИСПРАВЛЕНО: Правильная обработка None в member_count ===== |
| member_count = getattr(room, | member_count = getattr(room, | ||
| | | ||
| + | # Если 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, | member_count = len(room.users) if hasattr(room, | ||
| Строка 924: | Строка 1004: | ||
| if member_count == 0 and hasattr(room, | if member_count == 0 and hasattr(room, | ||
| member_count = getattr(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" | logger.info(f" | ||
| Строка 931: | Строка 1020: | ||
| is_dm = True | is_dm = True | ||
| logger.info(f" | logger.info(f" | ||
| - | elif member_count <= 1: | + | elif member_count <= 1: # ← Теперь member_count никогда не будет None! |
| is_dm = True | is_dm = True | ||
| - | logger.info(f" | + | logger.info(f" |
| else: | else: | ||
| is_dm = False | is_dm = False | ||
| Строка 943: | Строка 1032: | ||
| logger.info(f" | logger.info(f" | ||
| else: | else: | ||
| + | # В группе только на обращение | ||
| if not text.lower().startswith(f" | if not text.lower().startswith(f" | ||
| logger.info(f" | logger.info(f" | ||
| Строка 983: | Строка 1073: | ||
| async def member_callback(room, | async def member_callback(room, | ||
| - | """ | + | """ |
| try: | try: | ||
| if event.membership != " | if event.membership != " | ||
| Строка 1006: | Строка 1096: | ||
| new_user_name = user.name or new_user_id | new_user_name = user.name or new_user_id | ||
| | | ||
| - | logger.info(f" | + | logger.info(f" |
| | | ||
| - | plain_message = f" | + | plain_message = f" |
| | | ||
| - | html_message = f'< | + | html_message = f'< |
| | | ||
| await client.room_send( | await client.room_send( | ||
| Строка 1023: | Строка 1113: | ||
| ) | ) | ||
| | | ||
| - | logger.info(f" | + | logger.info(f" |
| | | ||
| except Exception as e: | except Exception as e: | ||
| logger.error(f" | logger.error(f" | ||
| + | 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" | logger.info(f" | ||
| + | logger.info(f" | ||
| # ===== Основной цикл ===== | # ===== Основной цикл ===== | ||
| async def main(): | async def main(): | ||
| - | global client | + | global client, sync_error_count |
| | | ||
| logger.info(f" | logger.info(f" | ||
| Строка 1056: | Строка 1148: | ||
| logger.info(f" | logger.info(f" | ||
| | | ||
| + | # Загружаем сохранённые 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" |
| else: | else: | ||
| client = AsyncClient(HOMESERVER, | client = AsyncClient(HOMESERVER, | ||
| client.user_id = USER | client.user_id = USER | ||
| + | logger.info(" | ||
| | | ||
| if not PASSWORD: | if not PASSWORD: | ||
| Строка 1081: | Строка 1176: | ||
| logger.error(f" | logger.error(f" | ||
| return | return | ||
| - | logger.info(f" | + | logger.info(f" |
| | | ||
| save_credentials(client.device_id, | save_credentials(client.device_id, | ||
| | | ||
| + | # Загрузка crypto store | ||
| try: | try: | ||
| client.load_store() | client.load_store() | ||
| logger.info(" | logger.info(" | ||
| except Exception as e: | except Exception as e: | ||
| - | logger.warning(f" | + | logger.warning(f" |
| | | ||
| + | # ===== НОВОЕ: Первичная синхронизация с обработкой next_batch ошибок ===== | ||
| logger.info(" | logger.info(" | ||
| - | | + | |
| - | await client.sync(timeout=30000, | + | for sync_attempt in range(max_sync_retries): |
| - | logger.info(f" | + | |
| - | except Exception as e: | + | sync_response = await client.sync(timeout=30000, |
| - | logger.error(f" | + | logger.info(f" |
| - | | + | |
| + | break | ||
| + | |||
| + | | ||
| + | | ||
| + | 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: | ||
| + | | ||
| + | if sync_attempt_num < max_sync_retries: | ||
| + | await asyncio.sleep(5) | ||
| + | else: | ||
| + | logger.error(f" | ||
| + | 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" | + | 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(message_callback, | client.add_event_callback(message_callback, | ||
| Строка 1117: | Строка 1261: | ||
| | | ||
| | | ||
| + | # Запускаем HTTP сервер | ||
| await start_http_server() | await start_http_server() | ||
| | | ||
| - | logger.info(f" | + | logger.info(f" |
| + | logger.info(f" | ||
| | | ||
| 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" | + | |
| - | await asyncio.sleep(30) | + | 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: | ||
| + | # Другие ошибки | ||
| + | | ||
| + | |||
| + | if consecutive_errors > 3: | ||
| + | logger.warning(f" | ||
| + | | ||
| + | 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(" | + | logger.info(" |
| except Exception as e: | except Exception as e: | ||
| logger.exception(f" | logger.exception(f" | ||