Compare commits

...
1 Commits
Author SHA1 Message Date
machka df9e36928c Broadcast HTML fix and subscription keyboards improvements 2025-05-21 19:30:25 +00:00
2 changed files with 179 additions and 162 deletions
+113 -88
View File
@@ -11,16 +11,23 @@ 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.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):
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:
@@ -35,25 +42,28 @@ async def broadcast_message_prompt_handler(callback: types.CallbackQuery,
try:
await callback.message.edit_text(
prompt_text,
reply_markup=get_back_to_admin_panel_keyboard(
current_lang, i18n))
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))
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):
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:
@@ -63,28 +73,35 @@ async def process_broadcast_message_handler(message: types.Message,
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
broadcast_message_text = message.html_text
await state.update_data(broadcast_message=broadcast_message_text)
# Сохраняем в state исходный текст и entities
text = message.text or message.caption or ""
entities = message.entities or message.caption_entities or []
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 state.update_data(
broadcast_text=text,
broadcast_entities=entities,
)
await message.answer(confirmation_prompt,
reply_markup=get_broadcast_confirmation_keyboard(
current_lang, i18n),
parse_mode="HTML")
preview_snippet = (text[:200] + "...") if len(text) > 200 else 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),
)
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):
@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:
@@ -94,96 +111,104 @@ async def cancel_broadcast_at_prompt_stage(callback: types.CallbackQuery,
try:
await callback.message.edit_text(
_("admin_broadcast_cancelled_nav_back"), reply_markup=None)
_("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))
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):
@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)
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"))
text = user_fsm_data.get("broadcast_text")
entities = user_fsm_data.get("broadcast_entities", [])
if not text:
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)
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.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)
user_ids = 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
logging.info(
f"Admin {admin_user.id} broadcasting '{text[:50]}...' to {len(user_ids)} users."
)
for user_id_to_send in user_ids_for_broadcast:
for uid in user_ids:
try:
await bot.send_message(user_id_to_send,
broadcast_message_to_send,
parse_mode="HTML")
await bot.send_message(
chat_id=uid,
text=text,
entities=entities,
)
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)
session,
{
"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 {uid}: {text[:70]}...",
"is_admin_event": True,
"target_user_id": uid,
},
)
except Exception as e:
failed_count += 1
logging.warning(
f"Failed to send broadcast to user {user_id_to_send}: {type(e).__name__} - {e}"
f"Failed to send broadcast to {uid}: {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)
session,
{
"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 {uid}: {type(e).__name__} {str(e)[:70]}...",
"is_admin_event": True,
"target_user_id": uid,
},
)
await asyncio.sleep(0.05)
try:
@@ -192,17 +217,17 @@ async def confirm_broadcast_callback_handler(callback: types.CallbackQuery,
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)
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))
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))
reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
)
await callback.answer()
await state.clear()
+66 -74
View File
@@ -1,6 +1,7 @@
import logging
from aiogram import Router, F, types, Bot
from aiogram.filters import Command
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from typing import Optional, Dict, Any, Union
from datetime import datetime, timezone
from sqlalchemy.ext.asyncio import AsyncSession
@@ -257,97 +258,88 @@ async def reshow_subscription_options_callback(callback: types.CallbackQuery,
async def my_subscription_command_handler(
event: Union[types.Message, types.CallbackQuery], i18n_data: dict,
settings: Settings, panel_service: PanelApiService,
subscription_service: SubscriptionService, session: AsyncSession,
bot: Bot):
target_message_obj = event.message if isinstance(
event, types.CallbackQuery) else event
user = event.from_user
if isinstance(event, types.CallbackQuery):
await event.answer()
event: Union[types.Message, types.CallbackQuery],
i18n_data: dict,
settings: Settings,
panel_service: PanelApiService,
subscription_service: SubscriptionService,
session: AsyncSession,
bot: Bot
):
target = event.message if isinstance(event, types.CallbackQuery) else event
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
get_text = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs
) if i18n else key
i18n: JsonI18n = i18n_data.get("i18n_instance")
get_text = lambda key, **kw: i18n.gettext(current_lang, key, **kw)
if not i18n or not target_message_obj:
if not i18n or not target:
if isinstance(event, types.Message):
await event.answer(get_text("error_occurred_try_again"))
return
if not panel_service or not subscription_service:
logging.error(
"PanelService or SubscriptionService is missing in my_subscription_command_handler."
)
await target_message_obj.answer(get_text("error_service_unavailable"))
await target.answer(get_text("error_service_unavailable"))
return
active_sub_details = await subscription_service.get_active_subscription_details(
session, user.id)
active = await subscription_service.get_active_subscription_details(session, event.from_user.id)
sub_info_text_content = ""
if active_sub_details:
end_date_obj = active_sub_details.get('end_date')
days_left = 0
if end_date_obj:
if end_date_obj.tzinfo is None:
end_date_obj = end_date_obj.replace(tzinfo=timezone.utc)
days_left = (end_date_obj.date() - datetime.now().date()).days
if not active:
text = get_text("subscription_not_active")
actual_config_link = active_sub_details.get('config_link') or get_text(
"config_link_not_available")
buy_button = InlineKeyboardButton(
text=get_text("menu_subscribe_inline", default="Купить"),
callback_data="main_action:subscribe"
)
back_markup = get_back_to_main_menu_markup(current_lang, i18n)
traffic_limit_bytes = active_sub_details.get('traffic_limit_bytes')
traffic_used_bytes = active_sub_details.get('traffic_used_bytes')
kb = InlineKeyboardMarkup(
inline_keyboard=[
[buy_button],
*back_markup.inline_keyboard
]
)
traffic_limit_gb_str = get_text("traffic_unlimited")
if traffic_limit_bytes and traffic_limit_bytes > 0:
traffic_limit_gb_str = f"{traffic_limit_bytes / (1024**3):.2f} GB"
if isinstance(event, types.CallbackQuery):
await event.answer()
try:
await event.message.edit_text(text, reply_markup=kb)
except:
await event.message.answer(text, reply_markup=kb)
else:
await event.answer(text, reply_markup=kb)
return
traffic_used_gb_str = get_text("traffic_na")
if traffic_used_bytes is not None:
traffic_used_gb_str = f"{traffic_used_bytes / (1024**3):.2f} GB"
end_date = active.get("end_date")
days_left = (
(end_date.date() - datetime.now().date()).days
if end_date else 0
)
text = get_text(
"my_subscription_details",
end_date=end_date.strftime("%Y-%m-%d") if end_date else "N/A",
days_left=max(0, days_left),
status=active.get("status_from_panel", get_text("status_active")).capitalize(),
config_link=active.get("config_link") or get_text("config_link_not_available"),
traffic_limit=(
f"{active['traffic_limit_bytes'] / 2**30:.2f} GB"
if active.get("traffic_limit_bytes")
else get_text("traffic_unlimited")
),
traffic_used=(
f"{active['traffic_used_bytes'] / 2**30:.2f} GB"
if active.get("traffic_used_bytes") is not None
else get_text("traffic_na")
)
)
markup = get_back_to_main_menu_markup(current_lang, i18n)
sub_info_text_content = get_text(
"my_subscription_details",
end_date=end_date_obj.strftime("%Y-%m-%d")
if end_date_obj else "N/A",
days_left=max(0, days_left),
status=active_sub_details.get(
'status_from_panel', get_text('status_active')).capitalize(),
config_link=actual_config_link,
traffic_limit=traffic_limit_gb_str,
traffic_used=traffic_used_gb_str)
else:
sub_info_text_content = get_text("subscription_not_active")
logging.info(
f"User {user.id} no active sub details for 'my_subscription'.")
reply_markup_val = get_back_to_main_menu_markup(current_lang, i18n)
if isinstance(event, types.CallbackQuery) and event.message:
if isinstance(event, types.CallbackQuery):
await event.answer()
try:
await event.message.edit_text(sub_info_text_content,
reply_markup=reply_markup_val,
parse_mode="HTML",
disable_web_page_preview=True)
except Exception as e_edit:
logging.warning(
f"Edit 'my_subscription' failed: {e_edit}. Sending new message to chat {target_message_obj.chat.id}."
)
await bot.send_message(chat_id=target_message_obj.chat.id,
text=sub_info_text_content,
reply_markup=reply_markup_val,
parse_mode="HTML",
disable_web_page_preview=True)
await event.message.edit_text(text, reply_markup=markup, parse_mode="HTML", disable_web_page_preview=True)
except:
await bot.send_message(chat_id=target.chat.id, text=text, reply_markup=markup, parse_mode="HTML", disable_web_page_preview=True)
else:
await target_message_obj.answer(sub_info_text_content,
reply_markup=reply_markup_val,
parse_mode="HTML",
disable_web_page_preview=True)
await target.answer(text, reply_markup=markup, parse_mode="HTML", disable_web_page_preview=True)
@router.message(Command("connect"))