feat(admin_bot): add /send_messages broadcast command
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 1m47s
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 1m47s
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.
This commit is contained in:
parent
ac2bd8f644
commit
3ffdbbcdaf
1 changed files with 89 additions and 7 deletions
96
admin_bot.py
96
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))
|
||||
|
||||
# Запускаем бота
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue