multi-bot finished

This commit is contained in:
2025-08-08 12:09:20 +09:00
parent 423ebf625b
commit 927da228c8
3 changed files with 93 additions and 17 deletions

View File

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

View File

@@ -1,6 +1,7 @@
# bot/bot_factory.py # bot/bot_factory.py
import logging import logging
from typing import Optional, Tuple from typing import Optional, Sequence, Tuple
from telegram.constants import ParseMode from telegram.constants import ParseMode
from telegram.ext import ( from telegram.ext import (
Application, Application,
@@ -11,22 +12,25 @@ from telegram.ext import (
Defaults, Defaults,
filters, filters,
) )
from .models import TelegramBot, BotConfig from .models import TelegramBot, BotConfig
from .handlers import start, ping, echo, my_chat_member_update, error_handler from .handlers import start, ping, echo, my_chat_member_update, error_handler
from .services import build_services from .services import build_services
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def _resolve_parse_mode(pm: Optional[str]): def _resolve_parse_mode(pm: Optional[str]):
if not pm or pm == "None": if not pm or pm == "None":
return None return None
return getattr(ParseMode, pm, ParseMode.HTML) 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) parse_mode = _resolve_parse_mode(cfg.parse_mode)
defaults = Defaults(parse_mode=parse_mode) if parse_mode else None defaults = Defaults(parse_mode=parse_mode) if parse_mode else None
allowed_updates = cfg.allowed_updates or 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 = builder.build()
# зарегистрировать хендлеры # Хендлеры
app.add_handler(CommandHandler("start", start)) app.add_handler(CommandHandler("start", start))
app.add_handler(CommandHandler("ping", ping)) app.add_handler(CommandHandler("ping", ping))
app.add_handler(ChatMemberHandler(my_chat_member_update, ChatMemberHandler.MY_CHAT_MEMBER)) app.add_handler(ChatMemberHandler(my_chat_member_update, ChatMemberHandler.MY_CHAT_MEMBER))
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, echo)) app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, echo))
# Глобальная обработка ошибок
app.add_error_handler(error_handler) app.add_error_handler(error_handler)
# инжект зависимостей для КОНКРЕТНОГО бота # DI: сервисы для этого бота
services = build_services(tb) services = build_services(tb)
app.bot_data["services"] = services app.bot_data["services"] = services
app.bot_data["allowed_updates"] = allowed_updates 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 tb.name, tb.username or "", parse_mode, allowed_updates
) )
return app, 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

View File

@@ -1,11 +1,12 @@
# bot/management/commands/runbots.py
import asyncio import asyncio
import logging import logging
import signal import signal
from typing import Sequence from typing import Sequence, Tuple
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from bot.models import TelegramBot from bot.models import TelegramBot, BotConfig
from bot.bot_factory import build_application_for_bot from bot.bot_factory import get_all_active_bots_with_configs, build_application_for_bot
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -15,19 +16,20 @@ class Command(BaseCommand):
def handle(self, *args, **options): def handle(self, *args, **options):
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s") logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
# ORM здесь, в синхронном контексте # ВАЖНО: ORM здесь, в синхронном контексте
bots: Sequence[TelegramBot] = list(TelegramBot.objects.filter(is_active=True)) bot_cfg_pairs: Sequence[Tuple[TelegramBot, BotConfig]] = get_all_active_bots_with_configs()
if not bots: if not bot_cfg_pairs:
self.stderr.write(self.style.ERROR("Нет активных ботов (is_active=True).")) self.stderr.write(self.style.ERROR("Нет активных ботов (is_active=True)."))
return 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 = [] apps = []
try: try:
for tb in bots: # Ни одной ORM-операции тут — только PTB
app, allowed_updates = build_application_for_bot(tb) for tb, cfg in bot_cfg_pairs:
app, allowed_updates = build_application_for_bot(tb, cfg)
await app.initialize() await app.initialize()
await app.start() await app.start()
await app.updater.start_polling(allowed_updates=allowed_updates) await app.updater.start_polling(allowed_updates=allowed_updates)
@@ -57,3 +59,4 @@ class Command(BaseCommand):
await app.shutdown() await app.shutdown()
except Exception: except Exception:
log.exception("Shutdown error") log.exception("Shutdown error")
log.info("All bots stopped gracefully.")