From 3ffdbbcdaf05e10f5adb447916314c2a837bc0f9 Mon Sep 17 00:00:00 2001 From: vrubelroman Date: Tue, 30 Jun 2026 20:46:11 +0000 Subject: [PATCH] feat(admin_bot): add /send_messages broadcast command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Admin sends /send_messages, the next text message is broadcast to every user in the users table. Sends via TELEGRAM_BOT_TOKEN (the client bot), not the admin bot's own token — broadcasting through context.bot would land in the user's chat with the admin bot, which they've never opened, instead of the client bot they actually use. --- admin_bot.py | 96 ++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 89 insertions(+), 7 deletions(-) diff --git a/admin_bot.py b/admin_bot.py index 4ef5b76..47ec209 100644 --- a/admin_bot.py +++ b/admin_bot.py @@ -3,10 +3,11 @@ Admin Telegram Bot Админский бот для получения статистики и всех скачанных видео """ import os +import asyncio import logging import sqlite3 from pathlib import Path -from telegram import Update +from telegram import Bot, Update from telegram.ext import Application, CommandHandler, ContextTypes, MessageHandler, filters from telegram.request import HTTPXRequest @@ -20,6 +21,10 @@ logger = logging.getLogger(__name__) # Токен админ бота из переменных окружения ADMIN_BOT_TOKEN = os.getenv('ADMIN_BOT_TOKEN') +# Токен клиентского бота — рассылка должна идти от его имени, иначе сообщения +# уходят в чат с админ-ботом, а не в чат с клиентским ботом, который видит пользователь +TELEGRAM_BOT_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN') + # Базовая директория проекта BASE_DIR = Path(__file__).resolve().parent DATA_DIR = BASE_DIR / 'data' @@ -69,6 +74,20 @@ def get_error_stats() -> dict[str, int]: return {} +def get_all_users() -> list[int]: + """Возвращает chat_id всех пользователей бота""" + try: + conn = sqlite3.connect(str(DB_FILE)) + cursor = conn.cursor() + cursor.execute('SELECT chat_id FROM users') + users = [row[0] for row in cursor.fetchall()] + conn.close() + return users + except Exception as e: + logger.error(f"Ошибка при получении списка пользователей: {e}") + return [] + + def save_admin_chat_id(chat_id: int): """Сохраняет chat_id админа в файл""" try: @@ -93,6 +112,33 @@ def get_admin_chat_id() -> int | None: return None +async def do_broadcast(text: str) -> tuple[int, int, int]: + """Рассылает текстовое сообщение всем пользователям клиентского бота. + + Отправлять нужно от имени клиентского бота (TELEGRAM_BOT_TOKEN), а не + админского — пользователи переписываются именно с клиентским ботом, и + письмо от админ-бота уйдёт в отдельный, незнакомый им чат. + """ + users = get_all_users() + success = 0 + failed = 0 + + async with Bot(token=TELEGRAM_BOT_TOKEN) as client_bot: + for chat_id in users: + try: + await client_bot.send_message(chat_id=chat_id, text=text) + success += 1 + except Exception as e: + error_str = str(e).lower() + if 'blocked' not in error_str and 'chat not found' not in error_str: + logger.warning(f"Не удалось отправить сообщение {chat_id}: {e}") + failed += 1 + + await asyncio.sleep(0.05) + + return success, failed, len(users) + + async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE): """Обрабатывает команду /start""" chat_id = update.message.chat_id @@ -103,18 +149,21 @@ async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await update.message.reply_text( "✅ Админ бот активирован! Теперь вы будете получать все скачанные видео.\n\n" "Доступные команды:\n" - "/stat — статистика бота" + "/stat — статистика бота\n" + "/send_messages — рассылка сообщения всем пользователям" ) else: await update.message.reply_text( "Это админский бот.\n\n" "Доступные команды:\n" - "/stat — статистика бота" + "/stat — статистика бота\n" + "/send_messages — рассылка сообщения всем пользователям" ) else: await update.message.reply_text( "Доступные команды:\n" - "/stat — статистика бота" + "/stat — статистика бота\n" + "/send_messages — рассылка сообщения всем пользователям" ) @@ -161,20 +210,51 @@ async def stat_command(update: Update, context: ContextTypes.DEFAULT_TYPE): await update.message.reply_text(stat_message) +async def send_messages_command(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Обрабатывает команду /send_messages — запрашивает текст для рассылки""" + if not TELEGRAM_BOT_TOKEN: + await update.message.reply_text("❌ TELEGRAM_BOT_TOKEN не задан, рассылка недоступна") + return + context.user_data['awaiting_broadcast'] = True + await update.message.reply_text( + "Введите текст сообщения для рассылки всем пользователям.\n" + "Чтобы отменить — /cancel" + ) + + +async def cancel_command(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Обрабатывает команду /cancel — отменяет ожидание текста рассылки""" + if context.user_data.pop('awaiting_broadcast', None): + await update.message.reply_text("❌ Рассылка отменена") + else: + await update.message.reply_text("Нечего отменять") + + async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE): """Обрабатывает все сообщения (на случай если админ отправит что-то)""" + if context.user_data.pop('awaiting_broadcast', False): + text = update.message.text + success, failed, total = await do_broadcast(text) + await update.message.reply_text( + f"📤 Рассылка завершена\n\n" + f"✅ Отправлено: {success}\n" + f"❌ Не доставлено: {failed}\n" + f"👥 Всего: {total}" + ) + return + # Сохраняем chat_id админа при первом сообщении chat_id = update.message.chat_id saved_chat_id = get_admin_chat_id() if saved_chat_id != chat_id: save_admin_chat_id(chat_id) if saved_chat_id is None: - await update.message.reply_text("✅ Админ бот активирован! Теперь вы будете получать все скачанные видео.\n\nДоступные команды: /stat") + await update.message.reply_text("✅ Админ бот активирован! Теперь вы будете получать все скачанные видео.\n\nДоступные команды: /stat, /send_messages") else: - await update.message.reply_text("Это админский бот. Доступные команды: /stat") + await update.message.reply_text("Это админский бот. Доступные команды: /stat, /send_messages") else: if update.message and update.message.text: - await update.message.reply_text("Это админский бот. Доступные команды: /stat") + await update.message.reply_text("Это админский бот. Доступные команды: /stat, /send_messages") def main(): @@ -199,6 +279,8 @@ def main(): # Регистрируем обработчики application.add_handler(CommandHandler("start", start_command)) application.add_handler(CommandHandler("stat", stat_command)) + application.add_handler(CommandHandler("send_messages", send_messages_command)) + application.add_handler(CommandHandler("cancel", cancel_command)) application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message)) # Запускаем бота