main functional bot. BugFixing required
This commit is contained in:
124
app/bots/bot_runner.py
Normal file
124
app/bots/bot_runner.py
Normal file
@@ -0,0 +1,124 @@
|
||||
import logging
|
||||
import asyncio
|
||||
import signal
|
||||
from contextlib import suppress
|
||||
from typing import Optional, Any
|
||||
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from telegram.ext import Application
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class BotApplication:
|
||||
"""Класс для управления жизненным циклом бота и планировщика."""
|
||||
|
||||
def __init__(self):
|
||||
self.app: Optional[Application] = None
|
||||
self.scheduler: Optional[AsyncIOScheduler] = None
|
||||
self.loop: Optional[asyncio.AbstractEventLoop] = None
|
||||
self._shutdown: bool = False
|
||||
|
||||
def signal_handler(self, signum: int, frame: Any) -> None:
|
||||
"""Обработчик сигналов для корректного завершения."""
|
||||
logger.info("Received shutdown signal")
|
||||
self._shutdown = True
|
||||
if self.loop and self.loop.is_running():
|
||||
self.loop.create_task(self.shutdown())
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
"""Корректное завершение работы всех компонентов."""
|
||||
logger.info("Starting graceful shutdown")
|
||||
|
||||
# Останавливаем планировщик
|
||||
if self.scheduler and self.scheduler.running:
|
||||
logger.debug("Shutting down scheduler")
|
||||
with suppress(Exception):
|
||||
self.scheduler.shutdown()
|
||||
|
||||
# Останавливаем приложение
|
||||
if self.app:
|
||||
logger.debug("Shutting down application")
|
||||
with suppress(Exception):
|
||||
await self.app.stop()
|
||||
with suppress(Exception):
|
||||
await self.app.shutdown()
|
||||
|
||||
# Ждем завершения всех задач
|
||||
if self.loop:
|
||||
tasks = [t for t in asyncio.all_tasks(self.loop)
|
||||
if t is not asyncio.current_task(self.loop)]
|
||||
if tasks:
|
||||
logger.debug(f"Cancelling {len(tasks)} pending tasks")
|
||||
for task in tasks:
|
||||
task.cancel()
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
async def run_bot(self, app: Application, cleanup_job) -> None:
|
||||
"""Запуск бота и планировщика."""
|
||||
try:
|
||||
self.app = app
|
||||
if not self.app:
|
||||
logger.critical("Failed to initialize application")
|
||||
return
|
||||
|
||||
# Настройка планировщика
|
||||
self.scheduler = AsyncIOScheduler()
|
||||
self.scheduler.add_job(cleanup_job, 'interval', minutes=30)
|
||||
|
||||
# Инициализация приложения
|
||||
await self.app.initialize()
|
||||
await self.app.start()
|
||||
|
||||
# Запуск планировщика
|
||||
self.scheduler.start()
|
||||
|
||||
# Запуск бота и ожидание завершения
|
||||
stop_event = asyncio.Event()
|
||||
|
||||
def stop_polling():
|
||||
stop_event.set()
|
||||
|
||||
# Устанавливаем обработчик для остановки
|
||||
self.app.stop_signals = None # Отключаем встроенную обработку сигналов
|
||||
|
||||
try:
|
||||
# Запускаем поллинг
|
||||
await self.app.updater.start_polling(drop_pending_updates=True)
|
||||
|
||||
# Ждем сигнала остановки
|
||||
await stop_event.wait()
|
||||
except Exception as e:
|
||||
if not self._shutdown:
|
||||
logger.error(f"Polling error: {e}")
|
||||
await asyncio.sleep(1)
|
||||
|
||||
except Exception as e:
|
||||
logger.critical(f"Critical error: {e}")
|
||||
finally:
|
||||
await self.shutdown()
|
||||
|
||||
def run_bot(app: Application, cleanup_job) -> None:
|
||||
"""Функция запуска бота с правильной обработкой сигналов и циклом событий."""
|
||||
bot_app = BotApplication()
|
||||
|
||||
# Настройка обработчиков сигналов
|
||||
signal.signal(signal.SIGINT, bot_app.signal_handler)
|
||||
signal.signal(signal.SIGTERM, bot_app.signal_handler)
|
||||
|
||||
# Создание и настройка цикла событий
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
bot_app.loop = loop
|
||||
|
||||
try:
|
||||
loop.run_until_complete(bot_app.run_bot(app, cleanup_job))
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
try:
|
||||
if not loop.is_closed():
|
||||
# Очистка
|
||||
loop.run_until_complete(loop.shutdown_asyncgens())
|
||||
loop.close()
|
||||
except Exception as e:
|
||||
logger.error(f"Error during loop cleanup: {e}")
|
||||
@@ -992,95 +992,87 @@ async def _dispatch_with_eta(uid: int, when: datetime) -> None:
|
||||
logger.error(f"Error in _dispatch_with_eta: {e}")
|
||||
raise
|
||||
|
||||
def main():
|
||||
"""Инициализация и запуск бота."""
|
||||
try:
|
||||
# Настройка логирования
|
||||
logging.basicConfig(
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
level=logging.INFO
|
||||
)
|
||||
|
||||
# Инициализация планировщика
|
||||
scheduler = AsyncIOScheduler()
|
||||
scheduler.add_job(cleanup_old_sessions, 'interval', minutes=30)
|
||||
scheduler.start()
|
||||
|
||||
app = Application.builder().token(settings.editor_bot_token).build()
|
||||
|
||||
# Регистрация обработчиков
|
||||
post_conv = ConversationHandler(
|
||||
entry_points=[CommandHandler("newpost", newpost)],
|
||||
states={
|
||||
CHOOSE_CHANNEL: [CallbackQueryHandler(choose_channel, pattern=r"^channel:")],
|
||||
CHOOSE_TYPE: [CallbackQueryHandler(choose_type, pattern=r"^type:")],
|
||||
CHOOSE_FORMAT: [CallbackQueryHandler(choose_format, pattern=r"^fmt:")],
|
||||
ENTER_TEXT: [
|
||||
MessageHandler(filters.TEXT & ~filters.COMMAND, enter_text),
|
||||
CallbackQueryHandler(choose_template_open, pattern=r"^tpl:choose$"),
|
||||
],
|
||||
SELECT_TEMPLATE: [
|
||||
CallbackQueryHandler(choose_template_apply, pattern=r"^tpluse:"),
|
||||
CallbackQueryHandler(choose_template_preview, pattern=r"^tplprev:"),
|
||||
CallbackQueryHandler(choose_template_navigate, pattern=r"^tplpage:"),
|
||||
CallbackQueryHandler(choose_template_cancel, pattern=r"^tpl:cancel$"),
|
||||
],
|
||||
PREVIEW_VARS: [
|
||||
MessageHandler(filters.TEXT & ~filters.COMMAND, preview_collect_vars)
|
||||
],
|
||||
PREVIEW_CONFIRM: [
|
||||
CallbackQueryHandler(preview_confirm, pattern=r"^pv:(use|edit)$"),
|
||||
CallbackQueryHandler(choose_template_cancel, pattern=r"^tpl:cancel$"),
|
||||
],
|
||||
ENTER_MEDIA: [
|
||||
MessageHandler(filters.TEXT & ~filters.COMMAND, enter_media)
|
||||
],
|
||||
EDIT_KEYBOARD: [
|
||||
MessageHandler(filters.TEXT & ~filters.COMMAND, edit_keyboard)
|
||||
],
|
||||
CONFIRM_SEND: [
|
||||
CallbackQueryHandler(confirm_send, pattern=r"^send:")
|
||||
],
|
||||
ENTER_SCHEDULE: [
|
||||
MessageHandler(filters.TEXT & ~filters.COMMAND, enter_schedule)
|
||||
],
|
||||
},
|
||||
fallbacks=[CommandHandler("start", start)],
|
||||
)
|
||||
def init_application():
|
||||
"""Инициализация приложения и регистрация обработчиков."""
|
||||
# Настройка логирования
|
||||
logging.basicConfig(
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
level=logging.INFO
|
||||
)
|
||||
|
||||
# Инициализация бота
|
||||
app = Application.builder().token(settings.editor_bot_token).build()
|
||||
|
||||
# Создание обработчика постов
|
||||
post_conv = ConversationHandler(
|
||||
entry_points=[CommandHandler("newpost", newpost)],
|
||||
states={
|
||||
CHOOSE_CHANNEL: [CallbackQueryHandler(choose_channel, pattern=r"^channel:")],
|
||||
CHOOSE_TYPE: [CallbackQueryHandler(choose_type, pattern=r"^type:")],
|
||||
CHOOSE_FORMAT: [CallbackQueryHandler(choose_format, pattern=r"^fmt:")],
|
||||
ENTER_TEXT: [
|
||||
MessageHandler(filters.TEXT & ~filters.COMMAND, enter_text),
|
||||
CallbackQueryHandler(choose_template_open, pattern=r"^tpl:choose$"),
|
||||
],
|
||||
SELECT_TEMPLATE: [
|
||||
CallbackQueryHandler(choose_template_apply, pattern=r"^tpluse:"),
|
||||
CallbackQueryHandler(choose_template_preview, pattern=r"^tplprev:"),
|
||||
CallbackQueryHandler(choose_template_navigate, pattern=r"^tplpage:"),
|
||||
CallbackQueryHandler(choose_template_cancel, pattern=r"^tpl:cancel$"),
|
||||
],
|
||||
PREVIEW_VARS: [
|
||||
MessageHandler(filters.TEXT & ~filters.COMMAND, preview_collect_vars)
|
||||
],
|
||||
PREVIEW_CONFIRM: [
|
||||
CallbackQueryHandler(preview_confirm, pattern=r"^pv:(use|edit)$"),
|
||||
CallbackQueryHandler(choose_template_cancel, pattern=r"^tpl:cancel$"),
|
||||
],
|
||||
ENTER_MEDIA: [
|
||||
MessageHandler(filters.TEXT & ~filters.COMMAND, enter_media)
|
||||
],
|
||||
EDIT_KEYBOARD: [
|
||||
MessageHandler(filters.TEXT & ~filters.COMMAND, edit_keyboard)
|
||||
],
|
||||
CONFIRM_SEND: [
|
||||
CallbackQueryHandler(confirm_send, pattern=r"^send:")
|
||||
],
|
||||
ENTER_SCHEDULE: [
|
||||
MessageHandler(filters.TEXT & ~filters.COMMAND, enter_schedule)
|
||||
],
|
||||
},
|
||||
fallbacks=[CommandHandler("start", start)],
|
||||
)
|
||||
|
||||
tpl_conv = ConversationHandler(
|
||||
entry_points=[CommandHandler("tpl_new", tpl_new_start)],
|
||||
states={
|
||||
TPL_NEW_NAME: [
|
||||
MessageHandler(filters.TEXT & ~filters.COMMAND, tpl_new_name)
|
||||
],
|
||||
TPL_NEW_TYPE: [
|
||||
CallbackQueryHandler(tpl_new_type, pattern=r"^tpltype:")
|
||||
],
|
||||
TPL_NEW_FORMAT: [
|
||||
CallbackQueryHandler(tpl_new_format, pattern=r"^tplfmt:")
|
||||
],
|
||||
TPL_NEW_CONTENT: [
|
||||
MessageHandler(filters.TEXT & ~filters.COMMAND, tpl_new_content)
|
||||
],
|
||||
TPL_NEW_KB: [
|
||||
MessageHandler(filters.TEXT & ~filters.COMMAND, tpl_new_kb)
|
||||
],
|
||||
},
|
||||
fallbacks=[CommandHandler("start", start)],
|
||||
)
|
||||
# Создание обработчика шаблонов
|
||||
tpl_conv = ConversationHandler(
|
||||
entry_points=[CommandHandler("tpl_new", tpl_new_start)],
|
||||
states={
|
||||
TPL_NEW_NAME: [
|
||||
MessageHandler(filters.TEXT & ~filters.COMMAND, tpl_new_name)
|
||||
],
|
||||
TPL_NEW_TYPE: [
|
||||
CallbackQueryHandler(tpl_new_type, pattern=r"^tpltype:")
|
||||
],
|
||||
TPL_NEW_FORMAT: [
|
||||
CallbackQueryHandler(tpl_new_format, pattern=r"^tplfmt:")
|
||||
],
|
||||
TPL_NEW_CONTENT: [
|
||||
MessageHandler(filters.TEXT & ~filters.COMMAND, tpl_new_content)
|
||||
],
|
||||
TPL_NEW_KB: [
|
||||
MessageHandler(filters.TEXT & ~filters.COMMAND, tpl_new_kb)
|
||||
],
|
||||
},
|
||||
fallbacks=[CommandHandler("start", start)],
|
||||
)
|
||||
|
||||
app.add_handler(CommandHandler("start", start))
|
||||
app.add_handler(post_conv)
|
||||
app.add_handler(tpl_conv)
|
||||
app.add_handler(CommandHandler("tpl_list", tpl_list))
|
||||
|
||||
# Запуск бота
|
||||
app.run_polling(allowed_updates=Update.ALL_TYPES)
|
||||
|
||||
except Exception as e:
|
||||
logger.critical(f"Critical error in main: {e}")
|
||||
raise
|
||||
# Регистрация всех обработчиков
|
||||
app.add_handler(CommandHandler("start", start))
|
||||
app.add_handler(post_conv)
|
||||
app.add_handler(tpl_conv)
|
||||
app.add_handler(CommandHandler("tpl_list", tpl_list))
|
||||
|
||||
return app
|
||||
|
||||
# -------- Вспомогательные функции для шаблонов ---------
|
||||
|
||||
@@ -1511,5 +1503,132 @@ async def choose_template_cancel(update: Update, context: CallbackContext) -> in
|
||||
)
|
||||
return ENTER_TEXT
|
||||
|
||||
import asyncio
|
||||
import signal
|
||||
from contextlib import suppress
|
||||
|
||||
class BotApplication:
|
||||
"""Класс для управления жизненным циклом бота и планировщика."""
|
||||
|
||||
def __init__(self):
|
||||
self.app = None
|
||||
self.scheduler = None
|
||||
self.loop = None
|
||||
self._shutdown = False
|
||||
|
||||
def signal_handler(self, signum, frame):
|
||||
"""Обработчик сигналов для корректного завершения."""
|
||||
self._shutdown = True
|
||||
if self.loop:
|
||||
self.loop.stop()
|
||||
|
||||
async def shutdown(self):
|
||||
"""Корректное завершение работы всех компонентов."""
|
||||
if self.scheduler:
|
||||
with suppress(Exception):
|
||||
self.scheduler.shutdown()
|
||||
|
||||
if self.app:
|
||||
with suppress(Exception):
|
||||
await self.app.stop()
|
||||
with suppress(Exception):
|
||||
await self.app.shutdown()
|
||||
|
||||
async def run_bot(self):
|
||||
"""Запуск бота и планировщика."""
|
||||
try:
|
||||
# Инициализация приложения
|
||||
self.app = init_application()
|
||||
if not self.app:
|
||||
logger.critical("Failed to initialize application")
|
||||
return
|
||||
|
||||
# Настройка планировщика
|
||||
self.scheduler = AsyncIOScheduler()
|
||||
self.scheduler.add_job(cleanup_old_sessions, 'interval', minutes=30)
|
||||
|
||||
# Инициализация приложения
|
||||
await self.app.initialize()
|
||||
await self.app.start()
|
||||
|
||||
# Запуск планировщика
|
||||
self.scheduler.start()
|
||||
|
||||
# Запуск бота
|
||||
while not self._shutdown:
|
||||
try:
|
||||
await self.app.updater.start_polling()
|
||||
except Exception as e:
|
||||
if not self._shutdown:
|
||||
logger.error(f"Polling error: {e}")
|
||||
await asyncio.sleep(1)
|
||||
else:
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
logger.critical(f"Critical error: {e}")
|
||||
finally:
|
||||
await self.shutdown()
|
||||
|
||||
async def init_bot():
|
||||
"""Инициализация и настройка бота."""
|
||||
try:
|
||||
application = init_application()
|
||||
if not application:
|
||||
logger.critical("Не удалось инициализировать приложение")
|
||||
return None
|
||||
return application
|
||||
except Exception as e:
|
||||
logger.critical(f"Ошибка при инициализации бота: {e}")
|
||||
return None
|
||||
|
||||
def start_bot():
|
||||
"""Запуск бота с правильным управлением циклом событий."""
|
||||
loop = None
|
||||
try:
|
||||
# Создаем и настраиваем цикл событий
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
# Инициализируем бота
|
||||
application = loop.run_until_complete(init_bot())
|
||||
if not application:
|
||||
return
|
||||
|
||||
# Запускаем бота с правильным управлением циклом событий
|
||||
from app.bots.bot_runner import run_bot
|
||||
run_bot(application, cleanup_old_sessions)
|
||||
except Exception as e:
|
||||
logger.critical(f"Критическая ошибка при запуске бота: {e}")
|
||||
raise
|
||||
finally:
|
||||
if loop:
|
||||
try:
|
||||
loop.close()
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при закрытии цикла событий: {e}")
|
||||
try:
|
||||
# Создаем и настраиваем цикл событий
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
# Инициализируем бота
|
||||
application = loop.run_until_complete(init_bot())
|
||||
if not application:
|
||||
return
|
||||
|
||||
# Запускаем бота с правильным управлением циклом событий
|
||||
from app.bots.bot_runner import run_bot
|
||||
run_bot(application, cleanup_old_sessions)
|
||||
except Exception as e:
|
||||
logger.critical(f"Критическая ошибка при запуске бота: {e}")
|
||||
raise
|
||||
finally:
|
||||
if loop:
|
||||
try:
|
||||
loop.close()
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при закрытии цикла событий: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
start_bot()
|
||||
|
||||
Reference in New Issue
Block a user