Files
fckbot/bots/bot.py
2026-06-30 15:55:06 +00:00

180 lines
6.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Точка входа для запуска XMPP-бота.
С расширенной диагностикой событий и использованием Pydantic-конфига (AppConfig).
"""
import argparse
import asyncio
import sys
import logging
import signal
from pathlib import Path
# Импорт нового загрузчика и модели конфига
from rag.utils.config_loader import load_config
from rag.config_models import AppConfig
from rag.utils.logger import setup_logging
# Импорт класса Bot (который теперь принимает AppConfig)
from bots.xmpp.client import Bot
logger = logging.getLogger(__name__)
async def shutdown(sig, bot, stop_event):
"""Graceful shutdown с обработкой сигналов."""
logger.info(f"Получен сигнал {sig.name}, начинаю graceful shutdown...")
stop_event.set()
try:
if hasattr(bot, 'disconnect'):
await bot.disconnect()
except Exception as e:
logger.error(f"Ошибка отключения: {e}")
# Отменяем все задачи, кроме текущей
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
for task in tasks:
task.cancel()
await asyncio.gather(*tasks, return_exceptions=True)
# Закрываем HTTP-сессию RAG-клиента
try:
await bot.rag_client.close()
except Exception:
pass
logger.info("Бот завершён")
async def main_async(profile_dir: str):
"""
Асинхронная инициализация и запуск бота.
"""
# 1. Загружаем конфигурацию через Pydantic (AppConfig)
try:
config: AppConfig = load_config(profile_dir)
except Exception as e:
logger.error(f"Ошибка загрузки конфигурации: {e}")
sys.exit(1)
# 2. Настраиваем логирование в режиме DEBUG для диагностики
# (можно переключить через --log-level, но пока жёстко)
setup_logging(config.name, config.log_file_path, log_level=logging.DEBUG)
logger.info(f"Запуск бота {config.name} с профилем {profile_dir}")
# 3. Создаём экземпляр бота
bot = Bot(config)
# ---- ДЛЯ ОТЛАДКИ (как в старом коде) ----
# Отключаем SRV-записи, чтобы явно указать сервер и порт.
# Если в конфиге есть xmpp_server и xmpp_port, используем их,
# иначе оставляем как есть (slixmpp попытается определить автоматически).
bot.use_srv = False
# Если в конфиге явно не заданы, оставляем значения по умолчанию,
# но для отладки можно жёстко прописать IP.
# В production лучше брать из конфига.
if config.xmpp_server:
bot.xmpp_server = config.xmpp_server
else:
# fallback для тестовой среды
bot.xmpp_server = "192.168.1.150"
if config.xmpp_port:
bot.xmpp_port = config.xmpp_port
else:
bot.xmpp_port = 5222
# ---- ДОБАВЛЯЕМ ВСЕ ОБРАБОТЧИКИ СОБЫТИЙ (как в старом коде) ----
def on_session_start(event):
logger.info("✅ СЕССИЯ УСПЕШНО УСТАНОВЛЕНА! Бот готов.")
bot.send_presence(pshow='chat', pstatus=config.status_message)
def on_failed_auth(event):
logger.error("❌ ОШИБКА АВТОРИЗАЦИИ! Проверьте пароль и JID.")
# Принудительно завершаем бота
asyncio.create_task(shutdown(None, bot, stop_event))
def on_disconnected(event):
logger.warning("⚠️ Соединение разорвано.")
def on_connection_error(event):
logger.error(f"❌ ОШИБКА СОЕДИНЕНИЯ: {event}")
def on_stream_error(event):
logger.error(f"❌ ОШИБКА ПОТОКА: {event}")
def on_stream_start(event):
logger.info("🔄 ПОТОК ЗАПУЩЕН")
def on_stream_end(event):
logger.info("🔄 ПОТОК ЗАВЕРШЁН")
def on_roster_update(event):
logger.info("📇 Ростер обновлён")
def on_presence(event):
logger.debug(f"Присутствие: {event}")
bot.add_event_handler("session_start", on_session_start)
bot.add_event_handler("failed_auth", on_failed_auth)
bot.add_event_handler("disconnected", on_disconnected)
bot.add_event_handler("connection_error", on_connection_error)
bot.add_event_handler("stream_error", on_stream_error)
bot.add_event_handler("stream_start", on_stream_start)
bot.add_event_handler("stream_end", on_stream_end)
bot.add_event_handler("roster_update", on_roster_update)
bot.add_event_handler("presence", on_presence)
# ---- ОБРАБОТКА СИГНАЛОВ ----
stop_event = asyncio.Event()
loop = asyncio.get_running_loop()
for sig in (signal.SIGINT, signal.SIGTERM):
loop.add_signal_handler(
sig, lambda s=sig: asyncio.create_task(shutdown(s, bot, stop_event))
)
# ---- ПОДКЛЮЧЕНИЕ ----
logger.info(f"Попытка подключения к {bot.xmpp_server}:{bot.xmpp_port}...")
bot.connect()
logger.info("connect() вызван, ожидаем событий...")
# Ждём сигнала остановки
await stop_event.wait()
def main():
"""Синхронная точка входа."""
parser = argparse.ArgumentParser(description="Запуск XMPP-бота")
parser.add_argument(
'--profile-dir',
required=True,
help="Путь к директории профиля бота (например, /usr/local/etc/bots/metabot)"
)
parser.add_argument(
"--log-level",
default="debug",
choices=["debug", "info", "warning", "error", "critical"],
help="Уровень логирования (по умолчанию debug)"
)
args = parser.parse_args()
profile_dir = Path(args.profile_dir).resolve()
if not profile_dir.exists():
print(f"Ошибка: директория {profile_dir} не существует")
sys.exit(1)
# Создаём событийный цикл
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
loop.run_until_complete(main_async(str(profile_dir)))
except KeyboardInterrupt:
logger.info("Бот остановлен пользователем")
except Exception as e:
logger.exception(f"Критическая ошибка: {e}")
sys.exit(1)
finally:
loop.close()
if __name__ == '__main__':
main()