Files
remnawave-minishop/bot/handlers/admin/broadcast.py
T

209 lines
8.8 KiB
Python

import logging
import asyncio
from aiogram import Router, F, types, Bot
from aiogram.fsm.context import FSMContext
from typing import Optional
from sqlalchemy.ext.asyncio import AsyncSession
from config.settings import Settings
from db.dal import user_dal, message_log_dal
from bot.states.admin_states import AdminStates
from bot.keyboards.inline.admin_keyboards import get_broadcast_confirmation_keyboard, get_back_to_admin_panel_keyboard, get_admin_panel_keyboard
from bot.middlewares.i18n import JsonI18n
router = Router(name="admin_broadcast_router")
async def broadcast_message_prompt_handler(callback: types.CallbackQuery,
state: FSMContext, i18n_data: dict,
settings: Settings,
session: AsyncSession):
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
if not i18n:
logging.error("i18n missing in broadcast_message_prompt_handler")
await callback.answer("Language service error.", show_alert=True)
return
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
prompt_text = _("admin_broadcast_enter_message")
if callback.message:
try:
await callback.message.edit_text(
prompt_text,
reply_markup=get_back_to_admin_panel_keyboard(
current_lang, i18n))
except Exception as e:
logging.warning(
f"Could not edit message for broadcast prompt: {e}. Sending new."
)
await callback.message.answer(
prompt_text,
reply_markup=get_back_to_admin_panel_keyboard(
current_lang, i18n))
await callback.answer()
await state.set_state(AdminStates.waiting_for_broadcast_message)
@router.message(AdminStates.waiting_for_broadcast_message, F.text)
async def process_broadcast_message_handler(message: types.Message,
state: FSMContext, i18n_data: dict,
settings: Settings,
session: AsyncSession):
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
if not i18n:
logging.error("i18n missing in process_broadcast_message_handler")
await message.reply("Language service error.")
return
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
broadcast_message_text = message.html_text
await state.update_data(broadcast_message=broadcast_message_text)
preview_snippet = (
broadcast_message_text[:200] +
"...") if len(broadcast_message_text) > 200 else broadcast_message_text
confirmation_prompt = _("admin_broadcast_confirm_prompt",
message_preview=preview_snippet)
await message.answer(confirmation_prompt,
reply_markup=get_broadcast_confirmation_keyboard(
current_lang, i18n),
parse_mode="HTML")
await state.set_state(AdminStates.confirming_broadcast)
@router.callback_query(F.data == "admin_action:main",
AdminStates.waiting_for_broadcast_message)
async def cancel_broadcast_at_prompt_stage(callback: types.CallbackQuery,
state: FSMContext,
settings: Settings, i18n_data: dict,
session: AsyncSession):
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
if not i18n or not callback.message:
await callback.answer("Error cancelling.", show_alert=True)
return
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
try:
await callback.message.edit_text(
_("admin_broadcast_cancelled_nav_back"), reply_markup=None)
except Exception:
await callback.message.answer(_("admin_broadcast_cancelled_nav_back"))
await callback.answer(_("admin_broadcast_cancelled_alert"))
await state.clear()
await callback.message.answer(_(key="admin_panel_title"),
reply_markup=get_admin_panel_keyboard(
i18n, current_lang, settings))
@router.callback_query(F.data.startswith("broadcast_final_action:"),
AdminStates.confirming_broadcast)
async def confirm_broadcast_callback_handler(callback: types.CallbackQuery,
state: FSMContext,
i18n_data: dict, bot: Bot,
settings: Settings,
session: AsyncSession):
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
if not i18n or not callback.message:
await callback.answer("Error processing broadcast confirmation.",
show_alert=True)
return
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
action = callback.data.split(":")[1]
user_fsm_data = await state.get_data()
broadcast_message_to_send = user_fsm_data.get("broadcast_message")
if action == "send":
if not broadcast_message_to_send:
await callback.message.edit_text(
_("admin_broadcast_error_no_message"))
await state.clear()
await callback.answer(_("admin_broadcast_error_no_message_alert"),
show_alert=True)
return
await callback.message.edit_text(_("admin_broadcast_sending_started"),
reply_markup=None)
await callback.answer()
user_ids_for_broadcast = await user_dal.get_all_active_user_ids_for_broadcast(
session)
sent_count = 0
failed_count = 0
logging.info(
f"Admin {callback.from_user.id} starting broadcast: '{broadcast_message_to_send[:50]}...' to {len(user_ids_for_broadcast)} users."
)
admin_user = callback.from_user
for user_id_to_send in user_ids_for_broadcast:
try:
await bot.send_message(user_id_to_send,
broadcast_message_to_send,
parse_mode="HTML")
sent_count += 1
log_payload_sent = {
"user_id": admin_user.id,
"telegram_username": admin_user.username,
"telegram_first_name": admin_user.first_name,
"event_type": "admin_broadcast_sent",
"content":
f"To user {user_id_to_send}: {broadcast_message_to_send[:70]}...",
"is_admin_event": True,
"target_user_id": user_id_to_send
}
await message_log_dal.create_message_log(
session, log_payload_sent)
except Exception as e:
failed_count += 1
logging.warning(
f"Failed to send broadcast to user {user_id_to_send}: {type(e).__name__} - {e}"
)
log_payload_failed = {
"user_id": admin_user.id,
"telegram_username": admin_user.username,
"telegram_first_name": admin_user.first_name,
"event_type": "admin_broadcast_failed",
"content":
f"For user {user_id_to_send}: {type(e).__name__} - {str(e)[:70]}...",
"is_admin_event": True,
"target_user_id": user_id_to_send
}
await message_log_dal.create_message_log(
session, log_payload_failed)
await asyncio.sleep(0.05)
try:
await session.commit()
except Exception as e_commit:
await session.rollback()
logging.error(f"Error committing broadcast logs: {e_commit}")
result_message = _("admin_broadcast_finished_stats",
sent_count=sent_count,
failed_count=failed_count)
await callback.message.answer(
result_message,
reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n))
elif action == "cancel":
await callback.message.edit_text(
_("admin_broadcast_cancelled"),
reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n))
await callback.answer()
await state.clear()