diff --git a/handlers/admin/sender/sender_handler.py b/handlers/admin/sender/sender_handler.py index 807544b1..031051a3 100644 --- a/handlers/admin/sender/sender_handler.py +++ b/handlers/admin/sender/sender_handler.py @@ -1,3 +1,4 @@ +import asyncio import json import re @@ -19,9 +20,55 @@ from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb from .keyboard import AdminSenderCallback, build_clusters_kb, build_sender_kb + router = Router() +async def send_broadcast_batch(bot, messages, batch_size=15): + results = [] + + for i in range(0, len(messages), batch_size): + batch = messages[i:i + batch_size] + tasks = [] + + for msg in batch: + tg_id = msg["tg_id"] + text = msg["text"] + photo = msg.get("photo") + keyboard = msg.get("keyboard") + + if photo: + task = bot.send_photo( + chat_id=tg_id, + photo=photo, + caption=text, + parse_mode="HTML", + reply_markup=keyboard + ) + else: + task = bot.send_message( + chat_id=tg_id, + text=text, + parse_mode="HTML", + reply_markup=keyboard + ) + tasks.append(task) + + batch_results = await asyncio.gather(*tasks, return_exceptions=True) + + for result in batch_results: + if isinstance(result, Exception): + logger.error(f"❌ Ошибка отправки: {result}") + results.append(False) + else: + results.append(True) + + if i + batch_size < len(messages): + await asyncio.sleep(1.0) + + return results + + class AdminSender(StatesGroup): waiting_for_message = State() preview = State() @@ -243,26 +290,23 @@ async def handle_send_confirm(callback_query: CallbackQuery, state: FSMContext, await callback_query.message.edit_text(f"📤 Рассылка начата!\n👥 Количество получателей: {total_users}") + messages = [] for tg_id in tg_ids: - try: - if photo: - await callback_query.bot.send_photo( - chat_id=tg_id, - photo=photo, - caption=text_message, - parse_mode="HTML", - reply_markup=keyboard, - ) - else: - await callback_query.bot.send_message( - chat_id=tg_id, - text=text_message, - parse_mode="HTML", - reply_markup=keyboard, - ) - success_count += 1 - except Exception as e: - logger.error(f"❌ Ошибка отправки пользователю {tg_id}: {e}") + message_data = { + "tg_id": tg_id, + "text": text_message, + "photo": photo, + "keyboard": keyboard + } + messages.append(message_data) + + results = await send_broadcast_batch( + bot=callback_query.bot, + messages=messages, + batch_size=15 + ) + + success_count = sum(1 for result in results if result) await callback_query.message.answer( text=( diff --git a/handlers/admin/tariffs/tariffs_handler.py b/handlers/admin/tariffs/tariffs_handler.py index 3af8f09b..4d150780 100644 --- a/handlers/admin/tariffs/tariffs_handler.py +++ b/handlers/admin/tariffs/tariffs_handler.py @@ -332,7 +332,7 @@ async def confirm_tariff_deletion(callback: CallbackQuery, callback_data: AdminT inline_keyboard=[ [ InlineKeyboardButton(text="✅ Да", callback_data=f"confirm_delete_tariff|{tariff_id}"), - InlineKeyboardButton(text="❌ Отмена", callback_data=f"view|{tariff_id}"), + InlineKeyboardButton(text="❌ Отмена", callback_data=AdminTariffCallback(action=f"view|{tariff_id}").pack()), ] ] ), diff --git a/handlers/admin/users/users_handler.py b/handlers/admin/users/users_handler.py index 0866e1b8..3b31ace6 100644 --- a/handlers/admin/users/users_handler.py +++ b/handlers/admin/users/users_handler.py @@ -569,13 +569,15 @@ async def handle_key_edit( expiry_date = key_details.get("expiry_date") or "—" tariff_name = "—" + subgroup_title = "—" if key_details.get("tariff_id"): result = await session.execute( - select(Tariff.name, Tariff.group_code).where(Tariff.id == key_details["tariff_id"]) + select(Tariff.name, Tariff.subgroup_title).where(Tariff.id == key_details["tariff_id"]) ) row = result.first() if row: - tariff_name = f"{row[0]} ({row[1]})" + tariff_name = row[0] + subgroup_title = row[1] or "—" text = ( "🔑 Информация о подписке\n\n" @@ -585,6 +587,7 @@ async def handle_key_edit( f"⏰ Истекает: {expiry_date} (МСК)\n" f"🌐 Кластер: {key_details.get('cluster_name', '—')}\n" f"🆔 ID клиента: {key_details.get('tg_id', '—')}\n" + f"📁 Группа: {subgroup_title}\n" f"📦 Тариф: {tariff_name}\n" ) if alias: diff --git a/handlers/keys/key_renew.py b/handlers/keys/key_renew.py index d4eb3815..760bf1e6 100644 --- a/handlers/keys/key_renew.py +++ b/handlers/keys/key_renew.py @@ -210,7 +210,7 @@ async def show_tariffs_in_renew_subgroup(callback: CallbackQuery, state: FSMCont ) ) - builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="renew_menu")) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data=f"renew_key|{key_name}")) builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile")) await edit_or_send_message( diff --git a/handlers/keys/key_utils.py b/handlers/keys/key_utils.py index 1292c1e4..41ec239a 100644 --- a/handlers/keys/key_utils.py +++ b/handlers/keys/key_utils.py @@ -231,7 +231,7 @@ async def create_client_on_server( raise ValueError(f"Тариф с id={plan} не найден.") total_gb_value = int(tariff["traffic_limit"]) if tariff["traffic_limit"] else 0 - device_limit_value = int(tariff["device_limit"]) if tariff.get("device_limit") is not None else None + device_limit_value = int(tariff["device_limit"]) if tariff.get("device_limit") is not None else 0 try: logger.info( @@ -621,7 +621,7 @@ async def update_key_on_cluster( result.scalar_one_or_none() total_gb_bytes = int(traffic_limit * 1024**3) if traffic_limit is not None else 0 - device_limit_value = device_limit if device_limit is not None else None + device_limit_value = device_limit if device_limit is not None else 0 config = ClientConfig( client_id=remnawave_client_id, @@ -678,7 +678,7 @@ async def update_subscription( tariff = result.scalar_one_or_none() if tariff: traffic_limit = int(tariff.traffic_limit) if tariff.traffic_limit is not None else None - device_limit = int(tariff.device_limit) if tariff.device_limit is not None else None + device_limit = int(tariff.device_limit) if tariff.device_limit is not None else 0 else: logger.warning(f"[LOG] update_subscription: тариф с id={tariff_id} не найден!") else: diff --git a/handlers/keys/subscriptions.py b/handlers/keys/subscriptions.py index eea9217e..65b63243 100644 --- a/handlers/keys/subscriptions.py +++ b/handlers/keys/subscriptions.py @@ -57,14 +57,16 @@ async def combine_unique_lines( urls_with_query = [f"{url}?{query_string}" if query_string else url for url in urls] tasks = [fetch_url_content(url, identifier) for url in urls_with_query] results = await asyncio.gather(*tasks, return_exceptions=True) - all_lines = set() + all_lines = [] all_headers = [] for result in results: if isinstance(result, tuple): lines, headers = result - all_lines.update(filter(None, lines)) + for line in filter(None, lines): + if line not in all_lines: + all_lines.append(line) all_headers.append(headers) - return list(all_lines), all_headers + return all_lines, all_headers async def get_subscription_urls( @@ -88,6 +90,9 @@ async def get_subscription_urls( if include_remnawave_key: urls.append(include_remnawave_key) + if RANDOM_SUBSCRIPTIONS: + random.shuffle(urls) + return urls @@ -259,8 +264,6 @@ async def handle_subscription(request: web.Request) -> web.Response: query_string = request.query_string combined_subscriptions, headers_list = await combine_unique_lines(urls, tg_id or email, query_string) - if RANDOM_SUBSCRIPTIONS: - random.shuffle(combined_subscriptions) cleaned_subscriptions = [clean_subscription_line(line) for line in combined_subscriptions] diff --git a/handlers/notifications/special_notifications.py b/handlers/notifications/special_notifications.py index 74e8e121..7b21db83 100644 --- a/handlers/notifications/special_notifications.py +++ b/handlers/notifications/special_notifications.py @@ -120,7 +120,7 @@ async def notify_users_no_traffic(bot: Bot, session: AsyncSession, current_time: if expiry_time: expiry_dt = pytz.utc.localize(datetime.fromtimestamp(expiry_time / 1000)).astimezone(moscow_tz) - if (current_dt - (expiry_dt - timedelta(days=30))) < timedelta(hours=NOTIFY_INACTIVE_TRAFFIC): + if current_dt > expiry_dt: continue try: diff --git a/handlers/refferal.py b/handlers/refferal.py index 954bbdfa..405d661d 100644 --- a/handlers/refferal.py +++ b/handlers/refferal.py @@ -20,7 +20,7 @@ from sqlalchemy import desc, func, select from sqlalchemy.ext.asyncio import AsyncSession from bot import bot -from config import ADMIN_ID, INLINE_MODE, REFERRAL_BONUS_PERCENTAGES, TOP_REFERRAL_BUTTON, TRIAL_CONFIG, USERNAME_BOT +from config import ADMIN_ID, INLINE_MODE, REFERRAL_BONUS_PERCENTAGES, TOP_REFERRAL_BUTTON, TRIAL_CONFIG, USERNAME_BOT, REFERRAL_QR from database import ( add_referral, add_user, @@ -93,7 +93,8 @@ async def invite_handler(callback_query_or_message: Message | CallbackQuery, ses else: invite_text = INVITE_TEXT_NON_INLINE.format(referral_link=referral_link) builder.button(text=INVITE, switch_inline_query=invite_text) - builder.button(text=QR, callback_data=f"show_referral_qr|{chat_id}") + if REFERRAL_QR: + builder.button(text=QR, callback_data=f"show_referral_qr|{chat_id}") if TOP_REFERRAL_BUTTON: builder.button(text=TOP_FIVE, callback_data="top_referrals") builder.button(text=MAIN_MENU, callback_data="profile")