add
This commit is contained in:
@@ -528,6 +528,38 @@ def build_router(
|
||||
builder.adjust(1)
|
||||
return builder.as_markup()
|
||||
|
||||
def build_admin_broadcast_mode_keyboard():
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(
|
||||
text="📣 Уведомление",
|
||||
callback_data="admin:broadcast_mode:notice",
|
||||
)
|
||||
builder.button(
|
||||
text="✅ Опрос Да/Нет",
|
||||
callback_data="admin:broadcast_mode:poll",
|
||||
)
|
||||
builder.button(
|
||||
text="🛠 Админ-панель",
|
||||
callback_data=f"panel:{PANEL_SECTION_ADMIN}",
|
||||
)
|
||||
builder.adjust(1)
|
||||
return builder.as_markup()
|
||||
|
||||
def build_broadcast_poll_keyboard(*, poll_id: int, selected_answer: str | None = None):
|
||||
yes_text = "✅ Да" if selected_answer == "yes" else "Да"
|
||||
no_text = "✅ Нет" if selected_answer == "no" else "Нет"
|
||||
yes_callback = (
|
||||
f"broadcast_poll:yes:{poll_id}" if poll_id > 0 else "admin:broadcast_preview_answer"
|
||||
)
|
||||
no_callback = (
|
||||
f"broadcast_poll:no:{poll_id}" if poll_id > 0 else "admin:broadcast_preview_answer"
|
||||
)
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(text=yes_text, callback_data=yes_callback)
|
||||
builder.button(text=no_text, callback_data=no_callback)
|
||||
builder.adjust(2)
|
||||
return builder.as_markup()
|
||||
|
||||
def get_message_html_text(message: Message) -> tuple[str, str]:
|
||||
raw_text = (message.text or message.caption or "").strip()
|
||||
formatted_text = (
|
||||
@@ -537,33 +569,90 @@ def build_router(
|
||||
formatted_text = html.escape(raw_text)
|
||||
return raw_text, formatted_text
|
||||
|
||||
async def show_admin_broadcast_preview(message: Message, *, text_html: str, state: FSMContext) -> None:
|
||||
def render_broadcast_text(
|
||||
*,
|
||||
brand_name: str,
|
||||
body_html: str,
|
||||
mode: str,
|
||||
) -> str:
|
||||
title = "Опрос" if mode == "poll" else "Уведомление"
|
||||
lines = [
|
||||
f"<b>📣 {html.escape(brand_name)} · {title}</b>",
|
||||
"",
|
||||
f"<blockquote>{body_html}</blockquote>",
|
||||
]
|
||||
if mode == "poll":
|
||||
lines.extend(["", "<i>Выберите один из вариантов ниже.</i>"])
|
||||
return "\n".join(lines)
|
||||
|
||||
async def show_admin_broadcast_preview(
|
||||
message: Message,
|
||||
*,
|
||||
body_html: str,
|
||||
mode: str,
|
||||
state: FSMContext,
|
||||
) -> None:
|
||||
config = await get_runtime_config()
|
||||
text_html = render_broadcast_text(
|
||||
brand_name=config.bot_brand_name,
|
||||
body_html=body_html,
|
||||
mode=mode,
|
||||
)
|
||||
recipient_count = len(await sync_service.get_broadcast_telegram_ids())
|
||||
await state.update_data(broadcast_text_html=text_html)
|
||||
await state.update_data(
|
||||
broadcast_body_html=body_html,
|
||||
broadcast_text_html=text_html,
|
||||
broadcast_mode=mode,
|
||||
)
|
||||
await message.answer(
|
||||
"📣 Предпросмотр рассылки.\n\n"
|
||||
f"Получателей: <b>{recipient_count}</b>\n"
|
||||
"Если всё верно, нажмите кнопку подтверждения ниже."
|
||||
)
|
||||
await message.answer(
|
||||
text_html,
|
||||
reply_markup=build_admin_broadcast_preview_keyboard(),
|
||||
)
|
||||
if mode == "poll":
|
||||
await message.answer(text_html, reply_markup=build_broadcast_poll_keyboard(poll_id=0))
|
||||
await message.answer(
|
||||
"У пользователей под этим сообщением будут кнопки <b>Да</b> и <b>Нет</b>.",
|
||||
reply_markup=build_admin_broadcast_preview_keyboard(),
|
||||
)
|
||||
return
|
||||
|
||||
async def send_admin_broadcast(bot_instance: Bot, *, text_html: str) -> tuple[int, int, int]:
|
||||
await message.answer(text_html, reply_markup=build_admin_broadcast_preview_keyboard())
|
||||
|
||||
async def send_admin_broadcast(
|
||||
bot_instance: Bot,
|
||||
*,
|
||||
text_html: str,
|
||||
poll_id: int | None = None,
|
||||
) -> tuple[int, int, int]:
|
||||
telegram_ids = await sync_service.get_broadcast_telegram_ids()
|
||||
sent_count = 0
|
||||
blocked_count = 0
|
||||
failed_count = 0
|
||||
reply_markup = (
|
||||
build_broadcast_poll_keyboard(poll_id=poll_id)
|
||||
if poll_id is not None
|
||||
else None
|
||||
)
|
||||
|
||||
for telegram_id in telegram_ids:
|
||||
try:
|
||||
await bot_instance.send_message(chat_id=telegram_id, text=text_html)
|
||||
await send_branded(
|
||||
bot_instance,
|
||||
chat_id=telegram_id,
|
||||
text=text_html,
|
||||
reply_markup=reply_markup,
|
||||
)
|
||||
sent_count += 1
|
||||
except TelegramRetryAfter as exc:
|
||||
await asyncio.sleep(exc.retry_after + 1)
|
||||
try:
|
||||
await bot_instance.send_message(chat_id=telegram_id, text=text_html)
|
||||
await send_branded(
|
||||
bot_instance,
|
||||
chat_id=telegram_id,
|
||||
text=text_html,
|
||||
reply_markup=reply_markup,
|
||||
)
|
||||
sent_count += 1
|
||||
except TelegramForbiddenError:
|
||||
blocked_count += 1
|
||||
@@ -582,6 +671,48 @@ def build_router(
|
||||
|
||||
return sent_count, blocked_count, failed_count
|
||||
|
||||
def broadcast_answer_label(answer: str) -> str:
|
||||
return "Да" if answer == "yes" else "Нет"
|
||||
|
||||
async def notify_broadcast_poll_answer(
|
||||
bot_instance: Bot,
|
||||
*,
|
||||
poll_id: int,
|
||||
question_html: str,
|
||||
answer: str,
|
||||
previous_answer: str | None,
|
||||
telegram_id: int,
|
||||
username: str | None,
|
||||
full_name: str,
|
||||
) -> None:
|
||||
config = await get_runtime_config()
|
||||
if config.broadcast_results_chat_id is None:
|
||||
return
|
||||
|
||||
username_text = f"@{username}" if username else "—"
|
||||
changed_text = (
|
||||
f"\nПредыдущий ответ: <b>{broadcast_answer_label(previous_answer)}</b>"
|
||||
if previous_answer and previous_answer != answer
|
||||
else ""
|
||||
)
|
||||
await bot_instance.send_message(
|
||||
chat_id=config.broadcast_results_chat_id,
|
||||
message_thread_id=config.broadcast_results_message_thread_id,
|
||||
text=(
|
||||
"<b>📬 Ответ на опрос рассылки</b>\n\n"
|
||||
f"Опрос: <code>{poll_id}</code>\n"
|
||||
f"Ответ: <b>{broadcast_answer_label(answer)}</b>{changed_text}\n\n"
|
||||
"<b>Пользователь</b>\n"
|
||||
"<blockquote>"
|
||||
f"ID: <code>{telegram_id}</code>\n"
|
||||
f"Имя: <b>{html.escape(full_name or str(telegram_id))}</b>\n"
|
||||
f"Username: <b>{html.escape(username_text)}</b>"
|
||||
"</blockquote>\n\n"
|
||||
"<b>Вопрос</b>\n"
|
||||
f"<blockquote>{question_html}</blockquote>"
|
||||
),
|
||||
)
|
||||
|
||||
def admin_settings_callback(action: str, value: str | None = None) -> str:
|
||||
return f"admin:{action}:{value}" if value else f"admin:{action}"
|
||||
|
||||
@@ -1887,6 +2018,7 @@ def build_router(
|
||||
expire_at=expire_at,
|
||||
traffic_limit_bytes=traffic_limit_bytes,
|
||||
brand_name=config.bot_brand_name,
|
||||
success_text=config.payment_success_text,
|
||||
is_renewal=is_renewal,
|
||||
)
|
||||
reply_markup = build_issued_access_keyboard(subscription_url=subscription_url)
|
||||
@@ -2208,11 +2340,30 @@ def build_router(
|
||||
return
|
||||
|
||||
source_text = callback.message.caption or callback.message.text or ""
|
||||
updated_text = f"{source_text}\n\n<b>Статус</b>: {status_text}"
|
||||
if callback.message.photo or callback.message.document:
|
||||
await callback.message.edit_caption(caption=updated_text, reply_markup=None)
|
||||
else:
|
||||
await callback.message.edit_text(updated_text, reply_markup=None)
|
||||
status_suffix = f"\n\n<b>Статус</b>: {status_text}"
|
||||
is_caption = bool(callback.message.photo or callback.message.document)
|
||||
max_length = 1024 if is_caption else 4096
|
||||
|
||||
updated_text = f"{source_text}{status_suffix}"
|
||||
if len(updated_text) > max_length:
|
||||
# Truncate the original text so the status fits within the limit
|
||||
available = max_length - len(status_suffix) - 3 # 3 for "..."
|
||||
updated_text = f"{source_text[:max(0, available)]}...{status_suffix}"
|
||||
|
||||
try:
|
||||
if is_caption:
|
||||
await callback.message.edit_caption(caption=updated_text, reply_markup=None)
|
||||
else:
|
||||
await callback.message.edit_text(updated_text, reply_markup=None)
|
||||
except TelegramBadRequest:
|
||||
# Fallback: just remove the keyboard without appending status
|
||||
try:
|
||||
if is_caption:
|
||||
await callback.message.edit_reply_markup(reply_markup=None)
|
||||
else:
|
||||
await callback.message.edit_reply_markup(reply_markup=None)
|
||||
except TelegramBadRequest:
|
||||
pass
|
||||
|
||||
@router.message(CommandStart())
|
||||
@router.message(Command("menu"))
|
||||
@@ -2279,6 +2430,7 @@ def build_router(
|
||||
text = (command.args or "").strip()
|
||||
if not text:
|
||||
await state.set_state(AdminBroadcastStates.waiting_for_text)
|
||||
await state.update_data(broadcast_mode="notice")
|
||||
await message.answer(
|
||||
"📣 Отправьте текст рассылки следующим сообщением.\n\n"
|
||||
"Чтобы отменить действие, отправьте <code>/cancel</code>."
|
||||
@@ -2291,7 +2443,44 @@ def build_router(
|
||||
return
|
||||
|
||||
await state.set_state(AdminBroadcastStates.waiting_for_text)
|
||||
await show_admin_broadcast_preview(message, text_html=text_html, state=state)
|
||||
await show_admin_broadcast_preview(
|
||||
message,
|
||||
body_html=text_html,
|
||||
mode="notice",
|
||||
state=state,
|
||||
)
|
||||
|
||||
@router.message(Command("poll"))
|
||||
async def cmd_poll(message: Message, command: CommandObject, state: FSMContext) -> None:
|
||||
if not await ensure_sender(message):
|
||||
return
|
||||
if not settings.is_admin(message.from_user.id):
|
||||
await message.answer("Эта команда доступна только администраторам.")
|
||||
return
|
||||
|
||||
await state.clear()
|
||||
text = (command.args or "").strip()
|
||||
if not text:
|
||||
await state.set_state(AdminBroadcastStates.waiting_for_text)
|
||||
await state.update_data(broadcast_mode="poll")
|
||||
await message.answer(
|
||||
"✅ Отправьте текст вопроса для опроса Да/Нет следующим сообщением.\n\n"
|
||||
"Чтобы отменить действие, отправьте <code>/cancel</code>."
|
||||
)
|
||||
return
|
||||
|
||||
text_html = html.escape(text)
|
||||
if len(text_html) > 3800:
|
||||
await message.answer("Текст слишком длинный. Максимум — около 3800 символов.")
|
||||
return
|
||||
|
||||
await state.set_state(AdminBroadcastStates.waiting_for_text)
|
||||
await show_admin_broadcast_preview(
|
||||
message,
|
||||
body_html=text_html,
|
||||
mode="poll",
|
||||
state=state,
|
||||
)
|
||||
|
||||
@router.callback_query(F.data == "guides:menu")
|
||||
async def on_guides_menu(callback: CallbackQuery, state: FSMContext) -> None:
|
||||
@@ -2851,14 +3040,113 @@ def build_router(
|
||||
return
|
||||
|
||||
await state.clear()
|
||||
await state.set_state(AdminBroadcastStates.waiting_for_text)
|
||||
await callback.message.answer(
|
||||
"📣 Отправьте текст рассылки следующим сообщением.\n\n"
|
||||
"Бот покажет предпросмотр и попросит подтвердить отправку.\n"
|
||||
"Чтобы отменить действие, отправьте <code>/cancel</code>."
|
||||
"📣 Выберите тип рассылки.",
|
||||
reply_markup=build_admin_broadcast_mode_keyboard(),
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
@router.callback_query(F.data.startswith("admin:broadcast_mode:"))
|
||||
async def on_admin_broadcast_mode(callback: CallbackQuery, state: FSMContext) -> None:
|
||||
if callback.from_user is None or callback.message is None:
|
||||
await callback.answer()
|
||||
return
|
||||
if not settings.is_admin(callback.from_user.id):
|
||||
await callback.answer("Недостаточно прав", show_alert=True)
|
||||
return
|
||||
|
||||
mode = (callback.data or "").split(":")[-1]
|
||||
if mode not in {"notice", "poll"}:
|
||||
await callback.answer("Тип рассылки не найден", show_alert=True)
|
||||
return
|
||||
|
||||
await state.clear()
|
||||
await state.set_state(AdminBroadcastStates.waiting_for_text)
|
||||
await state.update_data(broadcast_mode=mode)
|
||||
if mode == "poll":
|
||||
prompt = (
|
||||
"✅ Отправьте текст вопроса для опроса Да/Нет следующим сообщением.\n\n"
|
||||
"Бот покажет предпросмотр и попросит подтвердить отправку.\n"
|
||||
"Чтобы отменить действие, отправьте <code>/cancel</code>."
|
||||
)
|
||||
else:
|
||||
prompt = (
|
||||
"📣 Отправьте текст уведомления следующим сообщением.\n\n"
|
||||
"Бот красиво оформит текст, покажет предпросмотр и попросит подтвердить отправку.\n"
|
||||
"Чтобы отменить действие, отправьте <code>/cancel</code>."
|
||||
)
|
||||
|
||||
await callback.message.answer(prompt)
|
||||
await callback.answer()
|
||||
|
||||
@router.callback_query(F.data == "admin:broadcast_preview_answer")
|
||||
async def on_admin_broadcast_preview_answer(callback: CallbackQuery) -> None:
|
||||
await callback.answer("Это предпросмотр. Пользователи смогут выбрать Да или Нет.", show_alert=True)
|
||||
|
||||
@router.callback_query(F.data.startswith("broadcast_poll:"))
|
||||
async def on_broadcast_poll_answer(callback: CallbackQuery) -> None:
|
||||
if callback.from_user is None:
|
||||
await callback.answer()
|
||||
return
|
||||
|
||||
parts = (callback.data or "").split(":")
|
||||
if len(parts) != 3:
|
||||
await callback.answer("Опрос не найден", show_alert=True)
|
||||
return
|
||||
|
||||
answer = parts[1]
|
||||
try:
|
||||
poll_id = int(parts[2])
|
||||
except ValueError:
|
||||
await callback.answer("Опрос не найден", show_alert=True)
|
||||
return
|
||||
|
||||
result = await sync_service.record_broadcast_poll_answer(
|
||||
poll_id=poll_id,
|
||||
telegram_id=callback.from_user.id,
|
||||
username=callback.from_user.username,
|
||||
first_name=callback.from_user.first_name,
|
||||
last_name=callback.from_user.last_name,
|
||||
answer=answer,
|
||||
)
|
||||
if result is None:
|
||||
await callback.answer("Опрос уже недоступен", show_alert=True)
|
||||
return
|
||||
|
||||
if callback.message is not None:
|
||||
try:
|
||||
await callback.message.edit_reply_markup(
|
||||
reply_markup=build_broadcast_poll_keyboard(
|
||||
poll_id=poll_id,
|
||||
selected_answer=result.answer.answer,
|
||||
)
|
||||
)
|
||||
except TelegramBadRequest as exc:
|
||||
if "message is not modified" not in str(exc).lower():
|
||||
raise
|
||||
|
||||
full_name = " ".join(
|
||||
part for part in [callback.from_user.first_name, callback.from_user.last_name] if part
|
||||
).strip()
|
||||
try:
|
||||
await notify_broadcast_poll_answer(
|
||||
callback.bot,
|
||||
poll_id=poll_id,
|
||||
question_html=result.poll.question_text,
|
||||
answer=result.answer.answer,
|
||||
previous_answer=result.previous_answer,
|
||||
telegram_id=callback.from_user.id,
|
||||
username=callback.from_user.username,
|
||||
full_name=full_name,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
await callback.answer(
|
||||
f"Спасибо, ответ «{broadcast_answer_label(result.answer.answer)}» принят.",
|
||||
show_alert=True,
|
||||
)
|
||||
|
||||
@router.callback_query(F.data == "admin:broadcast_cancel")
|
||||
async def on_admin_broadcast_cancel(callback: CallbackQuery, state: FSMContext) -> None:
|
||||
if callback.from_user is None:
|
||||
@@ -2884,6 +3172,8 @@ def build_router(
|
||||
|
||||
state_data = await state.get_data()
|
||||
text_html = str(state_data.get("broadcast_text_html") or "").strip()
|
||||
body_html = str(state_data.get("broadcast_body_html") or "").strip()
|
||||
mode = str(state_data.get("broadcast_mode") or "notice").strip()
|
||||
if not text_html:
|
||||
await state.clear()
|
||||
await callback.answer("Текст рассылки не найден", show_alert=True)
|
||||
@@ -2894,9 +3184,18 @@ def build_router(
|
||||
if callback.message is not None:
|
||||
await callback.message.answer("📣 Рассылка запущена. Пришлю итог после отправки.")
|
||||
|
||||
poll_id: int | None = None
|
||||
if mode == "poll":
|
||||
poll = await sync_service.create_broadcast_poll(
|
||||
question_text=body_html or text_html,
|
||||
created_by_telegram_id=callback.from_user.id,
|
||||
)
|
||||
poll_id = poll.id
|
||||
|
||||
sent_count, blocked_count, failed_count = await send_admin_broadcast(
|
||||
callback.bot,
|
||||
text_html=text_html,
|
||||
poll_id=poll_id,
|
||||
)
|
||||
if callback.message is not None:
|
||||
await callback.message.answer(
|
||||
@@ -2904,6 +3203,7 @@ def build_router(
|
||||
f"Отправлено: <b>{sent_count}</b>\n"
|
||||
f"Недоступны/заблокировали бота: <b>{blocked_count}</b>\n"
|
||||
f"Ошибок отправки: <b>{failed_count}</b>"
|
||||
+ (f"\nID опроса: <code>{poll_id}</code>" if poll_id is not None else "")
|
||||
)
|
||||
|
||||
@router.callback_query(F.data == "admin:sync_all")
|
||||
@@ -3281,6 +3581,7 @@ def build_router(
|
||||
await callback.answer("Недостаточно прав", show_alert=True)
|
||||
return
|
||||
|
||||
await callback.answer()
|
||||
await clear_interactive_states(state)
|
||||
raw_page = (callback.data or "").split(":", maxsplit=2)[2]
|
||||
try:
|
||||
@@ -3294,7 +3595,6 @@ def build_router(
|
||||
text=render_admin_users_page_text(page_data=page_data),
|
||||
reply_markup=build_admin_users_page_keyboard(page_data=page_data),
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
@router.callback_query(F.data.startswith("admin:user_view:"))
|
||||
async def on_admin_user_profile(callback: CallbackQuery, state: FSMContext) -> None:
|
||||
@@ -3318,9 +3618,10 @@ def build_router(
|
||||
await callback.answer("Некорректный пользователь", show_alert=True)
|
||||
return
|
||||
|
||||
await callback.answer()
|
||||
profile = await sync_service.get_admin_telegram_user_profile(telegram_id=telegram_id)
|
||||
if profile is None:
|
||||
await callback.answer("Пользователь не найден", show_alert=True)
|
||||
await callback.message.answer("Пользователь не найден.")
|
||||
return
|
||||
|
||||
await show_branded_view(
|
||||
@@ -3332,7 +3633,6 @@ def build_router(
|
||||
page=page,
|
||||
),
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
# ── Admin: Block / Unblock user ──────────────────────────────────
|
||||
|
||||
@@ -4080,14 +4380,21 @@ def build_router(
|
||||
return
|
||||
|
||||
raw_text, text_html = get_message_html_text(message)
|
||||
state_data = await state.get_data()
|
||||
mode = str(state_data.get("broadcast_mode") or "notice")
|
||||
if not raw_text:
|
||||
await message.answer("Отправьте текст рассылки одним сообщением.")
|
||||
return
|
||||
if len(text_html) > 4000:
|
||||
await message.answer("Текст слишком длинный. Максимум — около 4000 символов.")
|
||||
if len(text_html) > 3800:
|
||||
await message.answer("Текст слишком длинный. Максимум — около 3800 символов.")
|
||||
return
|
||||
|
||||
await show_admin_broadcast_preview(message, text_html=text_html, state=state)
|
||||
await show_admin_broadcast_preview(
|
||||
message,
|
||||
body_html=text_html,
|
||||
mode=mode if mode in {"notice", "poll"} else "notice",
|
||||
state=state,
|
||||
)
|
||||
|
||||
@router.message(AdminConfigStates.waiting_for_value)
|
||||
async def on_admin_setting_value(message: Message, state: FSMContext) -> None:
|
||||
@@ -4257,6 +4564,235 @@ def build_router(
|
||||
await append_review_status(callback, status_text="❌ отклонено")
|
||||
await callback.answer("Оплата отклонена")
|
||||
|
||||
if True:
|
||||
|
||||
def _format_device_label(device) -> str:
|
||||
parts = []
|
||||
if device.device_model:
|
||||
parts.append(device.device_model)
|
||||
if device.platform:
|
||||
parts.append(device.platform)
|
||||
if device.os_version:
|
||||
parts.append(device.os_version)
|
||||
if parts:
|
||||
return " · ".join(parts)
|
||||
if device.user_agent:
|
||||
ua = device.user_agent
|
||||
return ua[:40] + "..." if len(ua) > 40 else ua
|
||||
return device.hwid[:16] + "..."
|
||||
|
||||
def _build_devices_list_text(
|
||||
devices,
|
||||
*,
|
||||
user_uuid_short: str,
|
||||
hwid_device_limit: int | None,
|
||||
) -> str:
|
||||
limit_text = str(hwid_device_limit) if hwid_device_limit else "∞"
|
||||
lines = [
|
||||
"<b>📱 Подключённые устройства</b>",
|
||||
f"<blockquote>Подключено: <b>{len(devices)}</b> / <b>{limit_text}</b></blockquote>",
|
||||
]
|
||||
if not devices:
|
||||
lines.extend([
|
||||
"",
|
||||
"<i>Нет подключённых устройств.</i>",
|
||||
])
|
||||
else:
|
||||
for idx, device in enumerate(devices, start=1):
|
||||
label = html.escape(_format_device_label(device))
|
||||
connected = format_datetime(device.created_at)
|
||||
lines.append(
|
||||
f"\n{idx}. <b>{label}</b>"
|
||||
f"\n<blockquote>Подключено: {connected}</blockquote>"
|
||||
)
|
||||
|
||||
lines.extend([
|
||||
"",
|
||||
"<i>Отключите устройство, чтобы освободить слот для нового подключения.</i>",
|
||||
])
|
||||
return "\n".join(lines)
|
||||
|
||||
def _build_devices_keyboard(devices, *, user_uuid: str):
|
||||
builder = InlineKeyboardBuilder()
|
||||
for device in devices:
|
||||
label = _format_device_label(device)
|
||||
short_hwid = device.hwid[:12]
|
||||
builder.button(
|
||||
text=f"❌ {label[:30]}",
|
||||
callback_data=f"devices:remove:{user_uuid}:{short_hwid}",
|
||||
)
|
||||
if len(devices) > 1:
|
||||
builder.button(
|
||||
text="🗑 Отключить все устройства",
|
||||
callback_data=f"devices:remove_all:{user_uuid}",
|
||||
)
|
||||
builder.button(
|
||||
text="↻ Обновить",
|
||||
callback_data="devices:list",
|
||||
)
|
||||
builder.button(
|
||||
text="🏠 Главное меню",
|
||||
callback_data=f"panel:{PANEL_SECTION_HOME}",
|
||||
)
|
||||
builder.adjust(1)
|
||||
return builder.as_markup()
|
||||
|
||||
@router.callback_query(F.data == "devices:list")
|
||||
async def on_devices_list(callback: CallbackQuery) -> None:
|
||||
if callback.from_user is None:
|
||||
await callback.answer()
|
||||
return
|
||||
|
||||
telegram_id = callback.from_user.id
|
||||
cached_users = await sync_service.get_cached_users_for_telegram(telegram_id)
|
||||
if not cached_users:
|
||||
await callback.answer("У вас нет активной подписки.", show_alert=True)
|
||||
return
|
||||
|
||||
primary = None
|
||||
for user in cached_users:
|
||||
if user.record.status.upper() == "ACTIVE":
|
||||
primary = user
|
||||
break
|
||||
if primary is None:
|
||||
primary = cached_users[0]
|
||||
|
||||
user_uuid = str(primary.record.rw_uuid)
|
||||
try:
|
||||
hwid_response = await payment_service._remnawave_client.get_user_hwid_devices(user_uuid)
|
||||
except RemnawaveApiError as exc:
|
||||
await callback.answer(f"Ошибка API: {exc.message}", show_alert=True)
|
||||
return
|
||||
|
||||
text = _build_devices_list_text(
|
||||
hwid_response.devices,
|
||||
user_uuid_short=primary.record.short_uuid,
|
||||
hwid_device_limit=primary.record.hwid_device_limit,
|
||||
)
|
||||
keyboard = _build_devices_keyboard(
|
||||
hwid_response.devices,
|
||||
user_uuid=user_uuid,
|
||||
)
|
||||
|
||||
await show_text_view(callback, text=text, reply_markup=keyboard)
|
||||
await callback.answer()
|
||||
|
||||
@router.callback_query(F.data.startswith("devices:remove_all:"))
|
||||
async def on_devices_remove_all(callback: CallbackQuery) -> None:
|
||||
if callback.from_user is None:
|
||||
await callback.answer()
|
||||
return
|
||||
|
||||
telegram_id = callback.from_user.id
|
||||
parts = (callback.data or "").split(":", maxsplit=2)
|
||||
if len(parts) < 3:
|
||||
await callback.answer("Некорректные данные.", show_alert=True)
|
||||
return
|
||||
|
||||
user_uuid = parts[2]
|
||||
|
||||
# Verify the user_uuid belongs to the caller
|
||||
cached_users = await sync_service.get_cached_users_for_telegram(telegram_id)
|
||||
matching = any(str(u.record.rw_uuid) == user_uuid for u in cached_users)
|
||||
if not matching:
|
||||
await callback.answer("Доступ запрещён.", show_alert=True)
|
||||
return
|
||||
|
||||
try:
|
||||
await payment_service._remnawave_client.delete_all_user_hwid_devices(user_uuid)
|
||||
except RemnawaveApiError as exc:
|
||||
await callback.answer(f"Ошибка: {exc.message}", show_alert=True)
|
||||
return
|
||||
|
||||
primary = next((u for u in cached_users if str(u.record.rw_uuid) == user_uuid), cached_users[0])
|
||||
|
||||
try:
|
||||
hwid_response = await payment_service._remnawave_client.get_user_hwid_devices(user_uuid)
|
||||
except RemnawaveApiError:
|
||||
hwid_response = type("R", (), {"devices": [], "total": 0})()
|
||||
|
||||
text = _build_devices_list_text(
|
||||
hwid_response.devices,
|
||||
user_uuid_short=primary.record.short_uuid,
|
||||
hwid_device_limit=primary.record.hwid_device_limit,
|
||||
)
|
||||
keyboard = _build_devices_keyboard(
|
||||
hwid_response.devices,
|
||||
user_uuid=user_uuid,
|
||||
)
|
||||
|
||||
await show_text_view(callback, text=text, reply_markup=keyboard)
|
||||
await callback.answer("Все устройства отключены")
|
||||
|
||||
@router.callback_query(F.data.startswith("devices:remove:"))
|
||||
async def on_devices_remove(callback: CallbackQuery) -> None:
|
||||
if callback.from_user is None:
|
||||
await callback.answer()
|
||||
return
|
||||
|
||||
telegram_id = callback.from_user.id
|
||||
parts = (callback.data or "").split(":", maxsplit=3)
|
||||
if len(parts) < 4:
|
||||
await callback.answer("Некорректные данные.", show_alert=True)
|
||||
return
|
||||
|
||||
user_uuid = parts[2]
|
||||
short_hwid = parts[3]
|
||||
|
||||
# Verify the user_uuid belongs to the caller
|
||||
cached_users = await sync_service.get_cached_users_for_telegram(telegram_id)
|
||||
matching = any(str(u.record.rw_uuid) == user_uuid for u in cached_users)
|
||||
if not matching:
|
||||
await callback.answer("Доступ запрещён.", show_alert=True)
|
||||
return
|
||||
|
||||
# Find the full HWID matching the short prefix
|
||||
try:
|
||||
current_devices = await payment_service._remnawave_client.get_user_hwid_devices(user_uuid)
|
||||
except RemnawaveApiError as exc:
|
||||
await callback.answer(f"Ошибка: {exc.message}", show_alert=True)
|
||||
return
|
||||
|
||||
target_device = None
|
||||
for device in current_devices.devices:
|
||||
if device.hwid.startswith(short_hwid):
|
||||
target_device = device
|
||||
break
|
||||
|
||||
if target_device is None:
|
||||
await callback.answer("Устройство не найдено или уже отключено.", show_alert=True)
|
||||
return
|
||||
|
||||
try:
|
||||
await payment_service._remnawave_client.delete_user_hwid_device(
|
||||
user_uuid=user_uuid,
|
||||
hwid=target_device.hwid,
|
||||
)
|
||||
except RemnawaveApiError as exc:
|
||||
await callback.answer(f"Ошибка: {exc.message}", show_alert=True)
|
||||
return
|
||||
|
||||
primary = next((u for u in cached_users if str(u.record.rw_uuid) == user_uuid), cached_users[0])
|
||||
|
||||
try:
|
||||
hwid_response = await payment_service._remnawave_client.get_user_hwid_devices(user_uuid)
|
||||
except RemnawaveApiError:
|
||||
hwid_response = type("R", (), {"devices": [], "total": 0})()
|
||||
|
||||
text = _build_devices_list_text(
|
||||
hwid_response.devices,
|
||||
user_uuid_short=primary.record.short_uuid,
|
||||
hwid_device_limit=primary.record.hwid_device_limit,
|
||||
)
|
||||
keyboard = _build_devices_keyboard(
|
||||
hwid_response.devices,
|
||||
user_uuid=user_uuid,
|
||||
)
|
||||
|
||||
device_label = _format_device_label(target_device)
|
||||
await show_text_view(callback, text=text, reply_markup=keyboard)
|
||||
await callback.answer(f"Устройство «{device_label[:30]}» отключено")
|
||||
|
||||
if True:
|
||||
|
||||
@router.callback_query(F.data.startswith("ticket_admin:note:"))
|
||||
|
||||
Reference in New Issue
Block a user