multi-bot finished
This commit is contained in:
28
backups/backup_2025-08-08_11-51-44.sql
Normal file
28
backups/backup_2025-08-08_11-51-44.sql
Normal 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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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.")
|
||||||
Reference in New Issue
Block a user