diff --git a/olgram/commands/bot_actions.py b/olgram/commands/bot_actions.py index efb289f..f1f670a 100644 --- a/olgram/commands/bot_actions.py +++ b/olgram/commands/bot_actions.py @@ -1,11 +1,13 @@ """ Здесь работа с конкретным ботом """ +import asyncio from aiogram import types from aiogram.utils.exceptions import TelegramAPIError, Unauthorized from aiogram import Bot as AioBot -from olgram.models.models import Bot +from olgram.models.models import Bot, User from server.server import unregister_token +from typing import Optional from locales.locale import _ @@ -87,6 +89,28 @@ async def select_chat(bot: Bot, call: types.CallbackQuery, chat: str): await call.answer(_("Выбран чат {0}").format(chat_obj.name)) +async def start_broadcast(bot: Bot, call: types.CallbackQuery, text: Optional[str]): + if not text: + return await call.answer(_("Отправьте текст для рассылки")) + + user_chat_ids = await User.all().values_list("telegram_id", flat=True) + a_bot = AioBot(bot.decrypted_token()) + count = 0 + await call.answer(_("Рассылка начата")) + try: + for telegram_id in user_chat_ids: + try: + if await a_bot.send_message(telegram_id, text, parse_mode="HTML"): + count += 1 + except Unauthorized: + continue + else: + await asyncio.sleep(0.05) # 20 messages per second (Limit: 30 messages per second) + finally: + await call.bot.send_message(call.from_user.id, _("Рассылка закончена. Сообщений отправлено: {0}").format(count)) + await a_bot.session.close() + + async def threads(bot: Bot, call: types.CallbackQuery): bot.enable_threads = not bot.enable_threads await bot.save(update_fields=["enable_threads"]) diff --git a/olgram/commands/info.py b/olgram/commands/info.py index fdce6ee..b1766dd 100644 --- a/olgram/commands/info.py +++ b/olgram/commands/info.py @@ -37,4 +37,4 @@ async def info(message: types.Message, state: FSMContext): _("Входящих сообщений у всех ботов: {0}\n").format(income_messages) + _("Исходящих сообщений у всех ботов: {0}\n").format(outgoing_messages) + _("Промо-кодов выдано: {0}\n").format(promo_count) + - _("Рекламную плашку выключили: {0}\n".format(olgram_text_disabled))) + _("Рекламную плашку выключили: {0}\n").format(olgram_text_disabled)) diff --git a/olgram/commands/menu.py b/olgram/commands/menu.py index 9f1b095..9d188e7 100644 --- a/olgram/commands/menu.py +++ b/olgram/commands/menu.py @@ -119,14 +119,19 @@ async def send_bot_menu(bot: Bot, call: types.CallbackQuery): chat=empty)) ) keyboard.insert( - types.InlineKeyboardButton(text=_("<< Назад"), - callback_data=menu_callback.new(level=0, bot_id=empty, operation=empty, chat=empty)) + types.InlineKeyboardButton(text=_("Рассылка"), + callback_data=menu_callback.new(level=2, bot_id=bot.id, operation="broadcast", + chat=empty)) ) keyboard.insert( types.InlineKeyboardButton(text=_("Опции"), callback_data=menu_callback.new(level=2, bot_id=bot.id, operation="settings", chat=empty)) ) + keyboard.insert( + types.InlineKeyboardButton(text=_("<< Назад"), + callback_data=menu_callback.new(level=0, bot_id=empty, operation=empty, chat=empty)) + ) await edit_or_create(call, dedent(_(""" Управление ботом @{0}. @@ -202,6 +207,41 @@ async def send_bot_settings_menu(bot: Bot, call: types.CallbackQuery): await edit_or_create(call, text, reply_markup=keyboard, parse_mode="HTML") +async def send_bot_broadcast_menu(bot: Bot, call: ty.Optional[types.CallbackQuery] = None, + chat_id: ty.Optional[int] = None, text: ty.Optional[str] = None): + if call: + await call.answer() + keyboard = types.InlineKeyboardMarkup(row_width=2) + keyboard.insert( + types.InlineKeyboardButton(text=_("<< Назад"), + callback_data=menu_callback.new(level=1, bot_id=bot.id, operation=empty, chat=empty)) + ) + keyboard.insert( + types.InlineKeyboardButton(text=_("Начать рассылку"), + callback_data=menu_callback.new(level=3, bot_id=bot.id, operation="start_broadcast", + chat=empty)) + ) + + if text: + text = dedent(_(""" + Сейчас вы редактируете текст, который будет отправлен всем пользователям бота @{0} после начала рассылки. + + Текущий текст: +
+ {1} ++ Отправьте сообщение, чтобы изменить текст. + """)).format(bot.name, text) + else: + text = _( + "Отправьте сообщение с текстом, который будет отправлен всем пользователям бота @{0} после начала рассылки." + ).format(bot.name) + if call: + await edit_or_create(call, text, keyboard, parse_mode="HTML") + else: + await AioBot.get_current().send_message(chat_id, text, reply_markup=keyboard, parse_mode="HTML") + + async def send_bot_text_menu(bot: Bot, call: ty.Optional[types.CallbackQuery] = None, chat_id: ty.Optional[int] = None): if call: await call.answer() @@ -342,7 +382,17 @@ async def send_bot_templates_menu(bot: Bot, call: ty.Optional[types.CallbackQuer await AioBot.get_current().send_message(chat_id, text, reply_markup=keyboard, parse_mode="HTML") -@dp.message_handler(state="wait_start_text", content_types="text", regexp="^[^/].+") # Not command +@dp.message_handler(state="wait_broadcast_text", content_types="text", regexp="^[^/].*") # Not command +async def broadcast_text_received(message: types.Message, state: FSMContext): + broadcast_text = message.html_text + async with state.proxy() as proxy: + bot_id = proxy.get("bot_id") + proxy["broadcast_text"] = broadcast_text + bot = await Bot.get_or_none(pk=bot_id) + await send_bot_broadcast_menu(bot, chat_id=message.chat.id, text=broadcast_text) + + +@dp.message_handler(state="wait_start_text", content_types="text", regexp="^[^/].*") # Not command async def start_text_received(message: types.Message, state: FSMContext): async with state.proxy() as proxy: bot_id = proxy.get("bot_id") @@ -352,7 +402,7 @@ async def start_text_received(message: types.Message, state: FSMContext): await send_bot_text_menu(bot, chat_id=message.chat.id) -@dp.message_handler(state="wait_second_text", content_types="text", regexp="^[^/].+") # Not command +@dp.message_handler(state="wait_second_text", content_types="text", regexp="^[^/].*") # Not command async def second_text_received(message: types.Message, state: FSMContext): async with state.proxy() as proxy: bot_id = proxy.get("bot_id") @@ -362,7 +412,7 @@ async def second_text_received(message: types.Message, state: FSMContext): await send_bot_second_text_menu(bot, chat_id=message.chat.id) -@dp.message_handler(state="wait_template", content_types="text", regexp="^[^/](.+)?") # Not command +@dp.message_handler(state="wait_template", content_types="text", regexp="^[^/].*") # Not command async def template_received(message: types.Message, state: FSMContext): async with state.proxy() as proxy: bot_id = proxy.get("bot_id") @@ -423,6 +473,11 @@ async def callback(call: types.CallbackQuery, callback_data: dict, state: FSMCon return await send_bot_statistic_menu(bot, call) if operation == "settings": return await send_bot_settings_menu(bot, call) + if operation == "broadcast": + await state.set_state("wait_broadcast_text") + async with state.proxy() as proxy: + proxy["bot_id"] = bot.id + return await send_bot_broadcast_menu(bot, call) if operation == "text": await state.set_state("wait_start_text") async with state.proxy() as proxy: @@ -457,6 +512,10 @@ async def callback(call: types.CallbackQuery, callback_data: dict, state: FSMCon if operation == "reset_second_text": await bot_actions.reset_bot_second_text(bot, call) return await send_bot_second_text_menu(bot, call) + if operation == "start_broadcast": + async with state.proxy() as proxy: + text = proxy.get("broadcast_text") + return await bot_actions.start_broadcast(bot, call, text) if operation == "templates": await state.set_state("wait_template") async with state.proxy() as proxy: diff --git a/olgram/settings.py b/olgram/settings.py index 704ff7c..dd7d2a6 100644 --- a/olgram/settings.py +++ b/olgram/settings.py @@ -91,8 +91,8 @@ class ServerSettings(AbstractSettings): return "/cert/public.pem" @classmethod - def append_text(cls) -> str: - return "\n\nЭтот бот создан с помощью @OlgramBot" + def append_text(cls, _: ty.Callable) -> str: + return _("\n\nЭтот бот создан с помощью @OlgramBot") @classmethod @lru_cache diff --git a/server/custom.py b/server/custom.py index d17aed6..d58f66e 100644 --- a/server/custom.py +++ b/server/custom.py @@ -213,14 +213,13 @@ async def handle_operator_message(message: types.Message, super_chat_id: int, bo async def message_handler(message: types.Message, *args, **kwargs): - _ = _get_translator(message) bot = db_bot_instance.get() if message.text and message.text == "/start": # На команду start нужно ответить, не пересылая сообщение никуда text = bot.start_text if bot.enable_olgram_text: - text += _(ServerSettings.append_text()) + text += ServerSettings.append_text(_get_translator(message)) return SendMessage(chat_id=message.chat.id, text=text, parse_mode="HTML") if message.text and message.text == "/security_policy":