main functional bot. BugFixing required

This commit is contained in:
2025-08-17 16:16:49 +09:00
parent 8e782bd741
commit 43dda889f8
3 changed files with 332 additions and 88 deletions

124
app/bots/bot_runner.py Normal file
View 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}")

View File

@@ -992,23 +992,18 @@ 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
) )
# Инициализация планировщика # Инициализация бота
scheduler = AsyncIOScheduler()
scheduler.add_job(cleanup_old_sessions, 'interval', minutes=30)
scheduler.start()
app = Application.builder().token(settings.editor_bot_token).build() app = Application.builder().token(settings.editor_bot_token).build()
# Регистрация обработчиков # Создание обработчика постов
post_conv = ConversationHandler( post_conv = ConversationHandler(
entry_points=[CommandHandler("newpost", newpost)], entry_points=[CommandHandler("newpost", newpost)],
states={ states={
@@ -1048,6 +1043,7 @@ def main():
fallbacks=[CommandHandler("start", start)], fallbacks=[CommandHandler("start", start)],
) )
# Создание обработчика шаблонов
tpl_conv = ConversationHandler( tpl_conv = ConversationHandler(
entry_points=[CommandHandler("tpl_new", tpl_new_start)], entry_points=[CommandHandler("tpl_new", tpl_new_start)],
states={ states={
@@ -1070,17 +1066,13 @@ def main():
fallbacks=[CommandHandler("start", start)], fallbacks=[CommandHandler("start", start)],
) )
# Регистрация всех обработчиков
app.add_handler(CommandHandler("start", start)) app.add_handler(CommandHandler("start", start))
app.add_handler(post_conv) app.add_handler(post_conv)
app.add_handler(tpl_conv) app.add_handler(tpl_conv)
app.add_handler(CommandHandler("tpl_list", tpl_list)) app.add_handler(CommandHandler("tpl_list", tpl_list))
# Запуск бота return app
app.run_polling(allowed_updates=Update.ALL_TYPES)
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()

View File

@@ -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"