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")