From 927da228c8636e2bcdd565a38c16f6fb1548fd01 Mon Sep 17 00:00:00 2001 From: "Andrey K. Choi" Date: Fri, 8 Aug 2025 12:09:20 +0900 Subject: [PATCH] multi-bot finished --- backups/backup_2025-08-08_11-51-44.sql | 28 ++++++++++++ bot/bot_factory.py | 59 +++++++++++++++++++++++--- bot/management/commands/runbot.py | 23 +++++----- 3 files changed, 93 insertions(+), 17 deletions(-) create mode 100644 backups/backup_2025-08-08_11-51-44.sql diff --git a/backups/backup_2025-08-08_11-51-44.sql b/backups/backup_2025-08-08_11-51-44.sql new file mode 100644 index 0000000..5de8699 --- /dev/null +++ b/backups/backup_2025-08-08_11-51-44.sql @@ -0,0 +1,28 @@ +/*M!999999\- enable the sandbox mode */ +-- MariaDB dump 10.19-11.6.2-MariaDB, for debian-linux-gnu (x86_64) +-- +-- Host: 127.0.0.1 Database: tg_autopost +-- ------------------------------------------------------ +-- Server version 11.6.2-MariaDB-ubu2404-log + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!40101 SET NAMES utf8mb4 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*M!100616 SET @OLD_NOTE_VERBOSITY=@@NOTE_VERBOSITY, NOTE_VERBOSITY=0 */; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*M!100616 SET NOTE_VERBOSITY=@OLD_NOTE_VERBOSITY */; + +-- Dump completed on 2025-08-08 11:51:47 diff --git a/bot/bot_factory.py b/bot/bot_factory.py index 9a022b8..549cf4a 100644 --- a/bot/bot_factory.py +++ b/bot/bot_factory.py @@ -1,6 +1,7 @@ # bot/bot_factory.py import logging -from typing import Optional, Tuple +from typing import Optional, Sequence, Tuple + from telegram.constants import ParseMode from telegram.ext import ( Application, @@ -11,22 +12,25 @@ from telegram.ext import ( Defaults, filters, ) + from .models import TelegramBot, BotConfig from .handlers import start, ping, echo, my_chat_member_update, error_handler from .services import build_services logger = logging.getLogger(__name__) + def _resolve_parse_mode(pm: Optional[str]): if not pm or pm == "None": return None return getattr(ParseMode, pm, ParseMode.HTML) -def build_application_for_bot(tb: TelegramBot) -> Tuple[Application, Optional[list]]: - cfg = BotConfig.objects.filter(bot=tb).first() - if not cfg: - raise RuntimeError(f"BotConfig не найден для бота '{tb}'") +def build_application_for_bot(tb: TelegramBot, cfg: BotConfig) -> Tuple[Application, Optional[list]]: + """ + Фабрика PTB Application для КОНКРЕТНОГО бота. + ВАЖНО: никакого ORM внутри — cfg передаётся извне. + """ parse_mode = _resolve_parse_mode(cfg.parse_mode) defaults = Defaults(parse_mode=parse_mode) if parse_mode else None allowed_updates = cfg.allowed_updates or None @@ -37,14 +41,16 @@ def build_application_for_bot(tb: TelegramBot) -> Tuple[Application, Optional[li app = builder.build() - # зарегистрировать хендлеры + # Хендлеры app.add_handler(CommandHandler("start", start)) app.add_handler(CommandHandler("ping", ping)) app.add_handler(ChatMemberHandler(my_chat_member_update, ChatMemberHandler.MY_CHAT_MEMBER)) app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, echo)) + + # Глобальная обработка ошибок app.add_error_handler(error_handler) - # инжект зависимостей для КОНКРЕТНОГО бота + # DI: сервисы для этого бота services = build_services(tb) app.bot_data["services"] = services app.bot_data["allowed_updates"] = allowed_updates @@ -54,3 +60,42 @@ def build_application_for_bot(tb: TelegramBot) -> Tuple[Application, Optional[li tb.name, tb.username or "—", parse_mode, allowed_updates ) return app, allowed_updates + + +def get_first_active_bot_with_config() -> Tuple[TelegramBot, BotConfig]: + """ + СИНХРОННО! Забирает первого активного бота и его конфиг. + """ + tb = TelegramBot.objects.filter(is_active=True).first() + if not tb: + raise RuntimeError("Нет активного TelegramBot (is_active=True). Создайте запись и включите её.") + cfg = BotConfig.objects.filter(bot=tb).first() + if not cfg: + raise RuntimeError(f"Для бота '{tb}' не найден BotConfig. Создайте связанную запись BotConfig.") + return tb, cfg + + +def get_all_active_bots_with_configs() -> Sequence[Tuple[TelegramBot, BotConfig]]: + """ + СИНХРОННО! Возвращает все (bot, cfg) для активных ботов. + Бросает, если хотя бы у одного нет BotConfig. + """ + bots = list(TelegramBot.objects.filter(is_active=True)) + if not bots: + return [] + + cfg_map = {c.bot_id: c for c in BotConfig.objects.filter(bot__in=bots)} + result: list[Tuple[TelegramBot, BotConfig]] = [] + missing = [] + for b in bots: + cfg = cfg_map.get(b.id) + if not cfg: + missing.append(b) + else: + result.append((b, cfg)) + + if missing: + names = ", ".join(f"{b.name}(@{b.username or '—'})" for b in missing) + raise RuntimeError(f"Нет BotConfig для ботов: {names}") + + return result diff --git a/bot/management/commands/runbot.py b/bot/management/commands/runbot.py index 11f0beb..4089a61 100644 --- a/bot/management/commands/runbot.py +++ b/bot/management/commands/runbot.py @@ -1,11 +1,12 @@ +# bot/management/commands/runbots.py import asyncio import logging import signal -from typing import Sequence +from typing import Sequence, Tuple from django.core.management.base import BaseCommand -from bot.models import TelegramBot -from bot.bot_factory import build_application_for_bot +from bot.models import TelegramBot, BotConfig +from bot.bot_factory import get_all_active_bots_with_configs, build_application_for_bot log = logging.getLogger(__name__) @@ -15,19 +16,20 @@ class Command(BaseCommand): def handle(self, *args, **options): logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s") - # ORM здесь, в синхронном контексте - bots: Sequence[TelegramBot] = list(TelegramBot.objects.filter(is_active=True)) - if not bots: + # ВАЖНО: ORM здесь, в синхронном контексте + bot_cfg_pairs: Sequence[Tuple[TelegramBot, BotConfig]] = get_all_active_bots_with_configs() + if not bot_cfg_pairs: self.stderr.write(self.style.ERROR("Нет активных ботов (is_active=True).")) return - asyncio.run(self._amain(bots)) + asyncio.run(self._amain(bot_cfg_pairs)) - async def _amain(self, bots: Sequence[TelegramBot]): + async def _amain(self, bot_cfg_pairs: Sequence[Tuple[TelegramBot, BotConfig]]): apps = [] try: - for tb in bots: - app, allowed_updates = build_application_for_bot(tb) + # Ни одной ORM-операции тут — только PTB + for tb, cfg in bot_cfg_pairs: + app, allowed_updates = build_application_for_bot(tb, cfg) await app.initialize() await app.start() await app.updater.start_polling(allowed_updates=allowed_updates) @@ -57,3 +59,4 @@ class Command(BaseCommand): await app.shutdown() except Exception: log.exception("Shutdown error") + log.info("All bots stopped gracefully.") \ No newline at end of file