fix: mass sender broadcasting / tariff group display in admin / correct device limit handling / configurable referral QR button / navigation fixes / fixed traffic notifications / fixed country shuffling

This commit is contained in:
Capybara-z
2025-07-27 21:34:04 +03:00
parent 357c8a01e7
commit 76c0c468f5
8 changed files with 85 additions and 34 deletions
+63 -19
View File
@@ -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"📤 <b>Рассылка начата!</b>\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=(
+1 -1
View File
@@ -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()),
]
]
),
+5 -2
View File
@@ -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 = (
"<b>🔑 Информация о подписке</b>\n\n"
@@ -585,6 +587,7 @@ async def handle_key_edit(
f"⏰ <b>Истекает:</b> {expiry_date} (МСК)\n"
f"🌐 <b>Кластер:</b> {key_details.get('cluster_name', '')}\n"
f"🆔 <b>ID клиента:</b> {key_details.get('tg_id', '')}\n"
f"📁 <b>Группа:</b> {subgroup_title}\n"
f"📦 <b>Тариф:</b> {tariff_name}\n"
)
if alias:
+1 -1
View File
@@ -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(
+3 -3
View File
@@ -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:
+8 -5
View File
@@ -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]
@@ -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:
+3 -2
View File
@@ -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")