multi-bot finished
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.")
|
||||
Reference in New Issue
Block a user