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:
@@ -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=(
|
||||
|
||||
@@ -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()),
|
||||
]
|
||||
]
|
||||
),
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user