miltu-bot refactor

This commit is contained in:
2025-08-08 11:37:11 +09:00
parent 9750735089
commit ce28f29c69
12 changed files with 518 additions and 178 deletions

View File

@@ -1,72 +1,60 @@
from django.core.management.base import BaseCommand, CommandError
from telegram.ext import ApplicationBuilder, CommandHandler, MessageHandler, ChatMemberHandler, filters
from django.db import transaction
from bot.models import TelegramBot, BotConfig
from bot.handlers import on_my_chat_member, upsert_chat_from_update
# bot/management/commands/runbots.py
import asyncio
import logging
import signal
from django.core.management.base import BaseCommand
from bot.models import TelegramBot
from bot.bot_factory import build_application_for_bot
async def cmd_start(update, context):
await upsert_chat_from_update(update, context)
await update.message.reply_text("Я на связи. Добавьте меня в группу — запомню чат автоматически.")
async def cmd_groups(update, context):
"""
/groups — покажем список чатов, где бот сейчас состоит.
(Лучше ограничить доступ: например, только admin_user_ids из BotConfig)
"""
bot_conf = await context.application.bot_data.get("bot_conf")
admin_ids = bot_conf.admin_user_ids if bot_conf else []
if update.effective_user and admin_ids and update.effective_user.id not in admin_ids:
return
from bot.models import TelegramChat
qs = TelegramChat.objects.filter(is_member=True).exclude(type="private").order_by("type", "title")
if not qs.exists():
await update.message.reply_text("Пока ни одной группы/канала не найдено.")
return
lines = []
for ch in qs:
label = ch.title or ch.username or ch.id
lines.append(f"{label} [{ch.type}] — {ch.id}")
await update.message.reply_text("\n".join(lines)[:3900])
log = logging.getLogger(__name__)
class Command(BaseCommand):
help = "Запуск телеграм-бота (polling) на основе активного TelegramBot"
def add_arguments(self, parser):
parser.add_argument("--bot-id", type=int, help="ID TelegramBot (если не указан — берём первый активный)")
help = "Запуск ВСЕХ активных ботов (PTB 22.3) в одном процессе."
def handle(self, *args, **options):
bot_id = options.get("bot_id")
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
asyncio.run(self._amain())
async def _amain(self):
bots = list(TelegramBot.objects.filter(is_active=True))
if not bots:
self.stderr.write(self.style.ERROR("Нет активных ботов (is_active=True)."))
return
apps = []
try:
if bot_id:
bot = TelegramBot.objects.get(id=bot_id, is_active=True)
else:
bot = TelegramBot.objects.filter(is_active=True).first()
if not bot:
raise CommandError("Нет активного TelegramBot.")
except TelegramBot.DoesNotExist:
raise CommandError("Указанный TelegramBot не найден или неактивен.")
# Инициализация и старт polling для каждого бота
for tb in bots:
app, allowed_updates = build_application_for_bot(tb)
await app.initialize()
await app.start()
# в 22.x polling запускается через updater
await app.updater.start_polling(allowed_updates=allowed_updates)
apps.append(app)
log.info("Bot started: %s (@%s)", tb.name, tb.username or "")
conf = getattr(bot, "config", None)
# Ожидание сигнала остановки
stop_event = asyncio.Event()
app = ApplicationBuilder().token(bot.token).build()
# Сохраним конфиг в bot_data, чтобы был доступ в хендлерах:
app.bot_data["bot_conf"] = conf
def _stop(*_):
stop_event.set()
# Команды
app.add_handler(CommandHandler("start", cmd_start))
app.add_handler(CommandHandler("groups", cmd_groups))
loop = asyncio.get_running_loop()
for sig in (signal.SIGINT, signal.SIGTERM):
try:
loop.add_signal_handler(sig, _stop)
except NotImplementedError:
# Windows
pass
# Обновление списка чатов при изменении статуса бота
app.add_handler(ChatMemberHandler(on_my_chat_member, ChatMemberHandler.MY_CHAT_MEMBER))
await stop_event.wait()
# Любые входящие сообщения — обновляем last_message_at для чатов
app.add_handler(MessageHandler(filters.ALL, upsert_chat_from_update))
# allowed_updates (если заданы в конфиге)
allowed_updates = conf.allowed_updates if conf and conf.allowed_updates else None
self.stdout.write(self.style.SUCCESS(f"Бот {bot} запущен (polling)."))
app.run_polling(allowed_updates=allowed_updates, drop_pending_updates=True)
finally:
# Корректная остановка всех приложений
for app in reversed(apps):
try:
await app.updater.stop()
await app.stop()
await app.shutdown()
except Exception:
log.exception("Shutdown error")