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}")
|
logger.error(f"Error in _dispatch_with_eta: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def main():
|
def init_application():
|
||||||
"""Инициализация и запуск бота."""
|
"""Инициализация приложения и регистрация обработчиков."""
|
||||||
try:
|
# Настройка логирования
|
||||||
# Настройка логирования
|
logging.basicConfig(
|
||||||
logging.basicConfig(
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
level=logging.INFO
|
||||||
level=logging.INFO
|
)
|
||||||
)
|
|
||||||
|
# Инициализация бота
|
||||||
# Инициализация планировщика
|
app = Application.builder().token(settings.editor_bot_token).build()
|
||||||
scheduler = AsyncIOScheduler()
|
|
||||||
scheduler.add_job(cleanup_old_sessions, 'interval', minutes=30)
|
# Создание обработчика постов
|
||||||
scheduler.start()
|
post_conv = ConversationHandler(
|
||||||
|
entry_points=[CommandHandler("newpost", newpost)],
|
||||||
app = Application.builder().token(settings.editor_bot_token).build()
|
states={
|
||||||
|
CHOOSE_CHANNEL: [CallbackQueryHandler(choose_channel, pattern=r"^channel:")],
|
||||||
# Регистрация обработчиков
|
CHOOSE_TYPE: [CallbackQueryHandler(choose_type, pattern=r"^type:")],
|
||||||
post_conv = ConversationHandler(
|
CHOOSE_FORMAT: [CallbackQueryHandler(choose_format, pattern=r"^fmt:")],
|
||||||
entry_points=[CommandHandler("newpost", newpost)],
|
ENTER_TEXT: [
|
||||||
states={
|
MessageHandler(filters.TEXT & ~filters.COMMAND, enter_text),
|
||||||
CHOOSE_CHANNEL: [CallbackQueryHandler(choose_channel, pattern=r"^channel:")],
|
CallbackQueryHandler(choose_template_open, pattern=r"^tpl:choose$"),
|
||||||
CHOOSE_TYPE: [CallbackQueryHandler(choose_type, pattern=r"^type:")],
|
],
|
||||||
CHOOSE_FORMAT: [CallbackQueryHandler(choose_format, pattern=r"^fmt:")],
|
SELECT_TEMPLATE: [
|
||||||
ENTER_TEXT: [
|
CallbackQueryHandler(choose_template_apply, pattern=r"^tpluse:"),
|
||||||
MessageHandler(filters.TEXT & ~filters.COMMAND, enter_text),
|
CallbackQueryHandler(choose_template_preview, pattern=r"^tplprev:"),
|
||||||
CallbackQueryHandler(choose_template_open, pattern=r"^tpl:choose$"),
|
CallbackQueryHandler(choose_template_navigate, pattern=r"^tplpage:"),
|
||||||
],
|
CallbackQueryHandler(choose_template_cancel, pattern=r"^tpl:cancel$"),
|
||||||
SELECT_TEMPLATE: [
|
],
|
||||||
CallbackQueryHandler(choose_template_apply, pattern=r"^tpluse:"),
|
PREVIEW_VARS: [
|
||||||
CallbackQueryHandler(choose_template_preview, pattern=r"^tplprev:"),
|
MessageHandler(filters.TEXT & ~filters.COMMAND, preview_collect_vars)
|
||||||
CallbackQueryHandler(choose_template_navigate, pattern=r"^tplpage:"),
|
],
|
||||||
CallbackQueryHandler(choose_template_cancel, pattern=r"^tpl:cancel$"),
|
PREVIEW_CONFIRM: [
|
||||||
],
|
CallbackQueryHandler(preview_confirm, pattern=r"^pv:(use|edit)$"),
|
||||||
PREVIEW_VARS: [
|
CallbackQueryHandler(choose_template_cancel, pattern=r"^tpl:cancel$"),
|
||||||
MessageHandler(filters.TEXT & ~filters.COMMAND, preview_collect_vars)
|
],
|
||||||
],
|
ENTER_MEDIA: [
|
||||||
PREVIEW_CONFIRM: [
|
MessageHandler(filters.TEXT & ~filters.COMMAND, enter_media)
|
||||||
CallbackQueryHandler(preview_confirm, pattern=r"^pv:(use|edit)$"),
|
],
|
||||||
CallbackQueryHandler(choose_template_cancel, pattern=r"^tpl:cancel$"),
|
EDIT_KEYBOARD: [
|
||||||
],
|
MessageHandler(filters.TEXT & ~filters.COMMAND, edit_keyboard)
|
||||||
ENTER_MEDIA: [
|
],
|
||||||
MessageHandler(filters.TEXT & ~filters.COMMAND, enter_media)
|
CONFIRM_SEND: [
|
||||||
],
|
CallbackQueryHandler(confirm_send, pattern=r"^send:")
|
||||||
EDIT_KEYBOARD: [
|
],
|
||||||
MessageHandler(filters.TEXT & ~filters.COMMAND, edit_keyboard)
|
ENTER_SCHEDULE: [
|
||||||
],
|
MessageHandler(filters.TEXT & ~filters.COMMAND, enter_schedule)
|
||||||
CONFIRM_SEND: [
|
],
|
||||||
CallbackQueryHandler(confirm_send, pattern=r"^send:")
|
},
|
||||||
],
|
fallbacks=[CommandHandler("start", start)],
|
||||||
ENTER_SCHEDULE: [
|
)
|
||||||
MessageHandler(filters.TEXT & ~filters.COMMAND, enter_schedule)
|
|
||||||
],
|
|
||||||
},
|
|
||||||
fallbacks=[CommandHandler("start", start)],
|
|
||||||
)
|
|
||||||
|
|
||||||
tpl_conv = ConversationHandler(
|
# Создание обработчика шаблонов
|
||||||
entry_points=[CommandHandler("tpl_new", tpl_new_start)],
|
tpl_conv = ConversationHandler(
|
||||||
states={
|
entry_points=[CommandHandler("tpl_new", tpl_new_start)],
|
||||||
TPL_NEW_NAME: [
|
states={
|
||||||
MessageHandler(filters.TEXT & ~filters.COMMAND, tpl_new_name)
|
TPL_NEW_NAME: [
|
||||||
],
|
MessageHandler(filters.TEXT & ~filters.COMMAND, tpl_new_name)
|
||||||
TPL_NEW_TYPE: [
|
],
|
||||||
CallbackQueryHandler(tpl_new_type, pattern=r"^tpltype:")
|
TPL_NEW_TYPE: [
|
||||||
],
|
CallbackQueryHandler(tpl_new_type, pattern=r"^tpltype:")
|
||||||
TPL_NEW_FORMAT: [
|
],
|
||||||
CallbackQueryHandler(tpl_new_format, pattern=r"^tplfmt:")
|
TPL_NEW_FORMAT: [
|
||||||
],
|
CallbackQueryHandler(tpl_new_format, pattern=r"^tplfmt:")
|
||||||
TPL_NEW_CONTENT: [
|
],
|
||||||
MessageHandler(filters.TEXT & ~filters.COMMAND, tpl_new_content)
|
TPL_NEW_CONTENT: [
|
||||||
],
|
MessageHandler(filters.TEXT & ~filters.COMMAND, tpl_new_content)
|
||||||
TPL_NEW_KB: [
|
],
|
||||||
MessageHandler(filters.TEXT & ~filters.COMMAND, tpl_new_kb)
|
TPL_NEW_KB: [
|
||||||
],
|
MessageHandler(filters.TEXT & ~filters.COMMAND, tpl_new_kb)
|
||||||
},
|
],
|
||||||
fallbacks=[CommandHandler("start", start)],
|
},
|
||||||
)
|
fallbacks=[CommandHandler("start", start)],
|
||||||
|
)
|
||||||
|
|
||||||
app.add_handler(CommandHandler("start", start))
|
# Регистрация всех обработчиков
|
||||||
app.add_handler(post_conv)
|
app.add_handler(CommandHandler("start", start))
|
||||||
app.add_handler(tpl_conv)
|
app.add_handler(post_conv)
|
||||||
app.add_handler(CommandHandler("tpl_list", tpl_list))
|
app.add_handler(tpl_conv)
|
||||||
|
app.add_handler(CommandHandler("tpl_list", tpl_list))
|
||||||
# Запуск бота
|
|
||||||
app.run_polling(allowed_updates=Update.ALL_TYPES)
|
return app
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.critical(f"Critical error in main: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
# -------- Вспомогательные функции для шаблонов ---------
|
# -------- Вспомогательные функции для шаблонов ---------
|
||||||
|
|
||||||
@@ -1511,5 +1503,132 @@ async def choose_template_cancel(update: Update, context: CallbackContext) -> in
|
|||||||
)
|
)
|
||||||
return ENTER_TEXT
|
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__":
|
if __name__ == "__main__":
|
||||||
main()
|
start_bot()
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from datetime import datetime
|
|||||||
from sqlalchemy import ForeignKey, String, BigInteger, Boolean, UniqueConstraint, func, DateTime
|
from sqlalchemy import ForeignKey, String, BigInteger, Boolean, UniqueConstraint, func, DateTime
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
from app.db.session import Base
|
from app.db.session import Base
|
||||||
|
from app.models.user import User # Добавляем импорт User
|
||||||
|
|
||||||
class Channel(Base):
|
class Channel(Base):
|
||||||
__tablename__ = "channels"
|
__tablename__ = "channels"
|
||||||
|
|||||||
Reference in New Issue
Block a user