mirror of
https://github.com/snoups/remnashop.git
synced 2026-04-26 12:35:38 +00:00
feat: redesign devices management page
This commit is contained in:
@@ -6,7 +6,8 @@ btn-back =
|
||||
|
||||
btn-common =
|
||||
.notification-close = ❌ Закрыть
|
||||
.devices-empty = ⚠️ Нет привязанных устройств
|
||||
.devices-empty = ✨ У вас нет подключённых устройств
|
||||
.cancel = Отмена
|
||||
|
||||
.squad-choice = { $selected ->
|
||||
[1] 🔘
|
||||
@@ -18,6 +19,13 @@ btn-common =
|
||||
*[other] { unit-day }
|
||||
}
|
||||
|
||||
btn-devices =
|
||||
.delete-all = 🗑 Удалить все устройства
|
||||
.reissue = 🔄 Перевыпустить подписку
|
||||
.confirm-delete = ✅ Да, удалить
|
||||
.confirm-reissue = ✅ Да, сбросить
|
||||
.cancel-reissue = ❌ Нет
|
||||
|
||||
btn-remnashop-info =
|
||||
.release-latest = 👀 Посмотреть
|
||||
.how-upgrade = ❓ Как обновить
|
||||
|
||||
@@ -49,11 +49,30 @@ msg-main-menu =
|
||||
}
|
||||
|
||||
msg-menu-devices =
|
||||
<b>📱 Мои устройства</b>
|
||||
<b>📱 Управление устройствами</b>
|
||||
|
||||
Здесь вы можете удалить привязанные устройства.
|
||||
|
||||
<i>Чтобы увеличить или уменьшить лимит вам нужно изменить подписку и выбрать нужное кол-во устройств.</i>
|
||||
📊 Подключено: <b>{ $current_count } / { $max_count }</b>
|
||||
|
||||
Нажмите на устройство чтобы удалить его.
|
||||
Если не хватает устройств — измените подписку.
|
||||
|
||||
msg-menu-devices-confirm-reissue =
|
||||
🔄 <b>Перевыпуск подписки</b>
|
||||
|
||||
Эта функция генерирует <b>новую</b> ссылку подписки.
|
||||
|
||||
⚠️ После сброса старая ссылка <b>перестанет работать</b>.
|
||||
Вам потребуется:
|
||||
1️⃣ Удалить старую подписку из приложения
|
||||
2️⃣ Добавить новую ссылку из раздела «Подключение»
|
||||
|
||||
Вы уверены, что хотите сбросить ссылку?
|
||||
|
||||
msg-menu-devices-confirm-delete =
|
||||
🗑 Удалить устройство <b>{ $selected_device_label }</b>?
|
||||
|
||||
msg-menu-devices-confirm-delete-all =
|
||||
🗑 Удалить <b>все устройства</b>?
|
||||
|
||||
msg-menu-invite =
|
||||
<b>👥 Пригласить друзей</b>
|
||||
|
||||
@@ -150,4 +150,9 @@ ntf-sync =
|
||||
.already-running = ⚠️ <i>Синхронизация уже выполняется. Пожалуйста, подождите.</i>
|
||||
|
||||
ntf-menu-editor =
|
||||
.button-saved = ✅ <i>Кнопка успешно сохранена.</i>
|
||||
.button-saved = ✅ <i>Кнопка успешно сохранена.</i>
|
||||
|
||||
ntf-devices =
|
||||
.deleted = ✅ Устройство удалено
|
||||
.all-deleted = ✅ Все устройства удалены
|
||||
.reissued = ✅ Подписка успешно перевыпущена
|
||||
|
||||
@@ -40,6 +40,8 @@ class Remnawave(Protocol):
|
||||
|
||||
async def reset_traffic(self, uuid: UUID) -> Optional[UserResponseDto]: ...
|
||||
|
||||
async def revoke_subscription(self, uuid: UUID) -> None: ...
|
||||
|
||||
def apply_sync(
|
||||
self,
|
||||
target: T,
|
||||
|
||||
@@ -150,6 +150,13 @@ class RemnawaveImpl(Remnawave):
|
||||
logger.debug(f"RemnaUser '{uuid}' not found in panel")
|
||||
return None
|
||||
|
||||
async def revoke_subscription(self, uuid: UUID) -> None:
|
||||
try:
|
||||
await self.sdk.users.revoke_user_subscription(uuid)
|
||||
logger.info(f"Subscription for RemnaUser '{uuid}' revoked successfully")
|
||||
except NotFoundError:
|
||||
logger.debug(f"RemnaUser '{uuid}' not found in panel")
|
||||
|
||||
def apply_sync(self, target: T, source: Union[SubscriptionDto, RemnaSubscriptionDto]) -> T:
|
||||
if not is_dataclass(target) or not is_dataclass(source):
|
||||
raise TypeError("Both target and source must be dataclasses")
|
||||
|
||||
@@ -2,8 +2,8 @@ from aiogram.enums import ButtonStyle
|
||||
from aiogram_dialog import Dialog, StartMode
|
||||
from aiogram_dialog.widgets.input import MessageInput
|
||||
from aiogram_dialog.widgets.kbd import (
|
||||
Button,
|
||||
CopyText,
|
||||
Button,
|
||||
ListGroup,
|
||||
Row,
|
||||
Start,
|
||||
@@ -25,11 +25,16 @@ from src.telegram.utils import require_permission
|
||||
from src.telegram.widgets import Banner, I18nFormat, IgnoreUpdate
|
||||
from src.telegram.window import Window
|
||||
|
||||
from .getters import devices_getter, invite_about_getter, invite_getter, menu_getter
|
||||
from .getters import device_confirm_delete_getter, devices_getter, invite_about_getter, invite_getter, menu_getter
|
||||
from .handlers import (
|
||||
on_device_delete,
|
||||
on_device_delete_confirm,
|
||||
on_device_delete_request,
|
||||
on_device_delete_all_confirm,
|
||||
on_device_delete_all_request,
|
||||
on_get_trial,
|
||||
on_invite,
|
||||
on_reissue_subscription_request,
|
||||
on_reissue_subscription_confirm,
|
||||
on_show_qr,
|
||||
on_withdraw_points,
|
||||
show_reason,
|
||||
@@ -115,24 +120,38 @@ devices = Window(
|
||||
Button(
|
||||
text=I18nFormat("btn-common.devices-empty"),
|
||||
id="devices_empty",
|
||||
when=F["devices_empty"],
|
||||
when=~F["has_devices"],
|
||||
),
|
||||
),
|
||||
ListGroup(
|
||||
Row(
|
||||
CopyText(
|
||||
text=Format("{item[platform]} - {item[device_model]}"),
|
||||
copy_text=Format("{item[platform]} - {item[device_model]}"),
|
||||
),
|
||||
Button(
|
||||
text=Format("❌"),
|
||||
id="delete",
|
||||
on_click=on_device_delete,
|
||||
text=Format("{item[label]}"),
|
||||
id="device_item",
|
||||
on_click=on_device_delete_request,
|
||||
),
|
||||
),
|
||||
id="devices_list",
|
||||
item_id_getter=lambda item: item["short_hwid"],
|
||||
items="devices",
|
||||
when=F["has_devices"],
|
||||
),
|
||||
Row(
|
||||
Button(
|
||||
text=I18nFormat("btn-devices.delete-all"),
|
||||
id="delete_all",
|
||||
on_click=on_device_delete_all_request,
|
||||
when=F["has_devices"],
|
||||
style=Style(ButtonStyle.DANGER),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
Button(
|
||||
text=I18nFormat("btn-devices.reissue"),
|
||||
id="reissue",
|
||||
on_click=on_reissue_subscription_request,
|
||||
style=Style(ButtonStyle.PRIMARY),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
SwitchTo(
|
||||
@@ -146,6 +165,48 @@ devices = Window(
|
||||
getter=devices_getter,
|
||||
)
|
||||
|
||||
device_confirm_delete = Window(
|
||||
Banner(BannerName.MENU),
|
||||
I18nFormat("msg-menu-devices-confirm-delete"),
|
||||
Row(
|
||||
Button(
|
||||
text=I18nFormat("btn-devices.confirm-delete"),
|
||||
id="confirm_delete",
|
||||
on_click=on_device_delete_confirm,
|
||||
style=Style(ButtonStyle.DANGER),
|
||||
),
|
||||
SwitchTo(
|
||||
text=I18nFormat("btn-common.cancel"),
|
||||
id="cancel",
|
||||
state=MainMenu.DEVICES,
|
||||
),
|
||||
),
|
||||
IgnoreUpdate(),
|
||||
state=MainMenu.DEVICE_CONFIRM_DELETE,
|
||||
getter=device_confirm_delete_getter,
|
||||
)
|
||||
|
||||
device_confirm_delete_all = Window(
|
||||
Banner(BannerName.MENU),
|
||||
I18nFormat("msg-menu-devices-confirm-delete-all"),
|
||||
Row(
|
||||
Button(
|
||||
text=I18nFormat("btn-devices.confirm-delete"),
|
||||
id="confirm_delete_all",
|
||||
on_click=on_device_delete_all_confirm,
|
||||
style=Style(ButtonStyle.DANGER),
|
||||
),
|
||||
SwitchTo(
|
||||
text=I18nFormat("btn-common.cancel"),
|
||||
id="cancel",
|
||||
state=MainMenu.DEVICES,
|
||||
),
|
||||
),
|
||||
IgnoreUpdate(),
|
||||
state=MainMenu.DEVICE_CONFIRM_DELETE_ALL,
|
||||
getter=device_confirm_delete_getter,
|
||||
)
|
||||
|
||||
invite = Window(
|
||||
Banner(BannerName.REFERRAL),
|
||||
I18nFormat("msg-menu-invite"),
|
||||
@@ -220,9 +281,33 @@ invite_about = Window(
|
||||
)
|
||||
|
||||
|
||||
device_confirm_reissue = Window(
|
||||
Banner(BannerName.MENU),
|
||||
I18nFormat("msg-menu-devices-confirm-reissue"),
|
||||
Row(
|
||||
Button(
|
||||
text=I18nFormat("btn-devices.confirm-reissue"),
|
||||
id="confirm_reissue",
|
||||
on_click=on_reissue_subscription_confirm,
|
||||
style=Style(ButtonStyle.DANGER),
|
||||
),
|
||||
SwitchTo(
|
||||
text=I18nFormat("btn-devices.cancel-reissue"),
|
||||
id="cancel_reissue",
|
||||
state=MainMenu.DEVICES,
|
||||
),
|
||||
),
|
||||
IgnoreUpdate(),
|
||||
state=MainMenu.DEVICE_CONFIRM_REISSUE,
|
||||
getter=device_confirm_delete_getter,
|
||||
)
|
||||
|
||||
router = Dialog(
|
||||
menu,
|
||||
devices,
|
||||
device_confirm_delete,
|
||||
device_confirm_delete_all,
|
||||
device_confirm_reissue,
|
||||
invite,
|
||||
invite_about,
|
||||
)
|
||||
|
||||
@@ -98,6 +98,21 @@ async def menu_getter(
|
||||
raise MenuRenderError(str(e)) from e
|
||||
|
||||
|
||||
PLATFORM_ICONS = {
|
||||
"ios": "🍎",
|
||||
"android": "🤖",
|
||||
"windows": "🖥️",
|
||||
"macos": "💻",
|
||||
"linux": "🐧",
|
||||
"router": "📡",
|
||||
"tv": "📺",
|
||||
}
|
||||
|
||||
|
||||
def get_platform_icon(platform: str) -> str:
|
||||
return PLATFORM_ICONS.get(platform.lower(), "📱")
|
||||
|
||||
|
||||
@inject
|
||||
async def devices_getter(
|
||||
dialog_manager: DialogManager,
|
||||
@@ -120,6 +135,7 @@ async def devices_getter(
|
||||
"platform": device.platform,
|
||||
"device_model": device.device_model,
|
||||
"user_agent": device.user_agent,
|
||||
"label": f"{get_platform_icon(device.platform)} {device.platform} ({device.device_model})",
|
||||
}
|
||||
for device in devices
|
||||
]
|
||||
@@ -131,9 +147,20 @@ async def devices_getter(
|
||||
"max_count": i18n_format_device_limit(current_subscription.device_limit),
|
||||
"devices": formatted_devices,
|
||||
"devices_empty": len(devices) == 0,
|
||||
"has_devices": len(devices) > 0,
|
||||
}
|
||||
|
||||
|
||||
@inject
|
||||
async def device_confirm_delete_getter(
|
||||
dialog_manager: DialogManager,
|
||||
user: UserDto,
|
||||
**kwargs: Any,
|
||||
) -> dict[str, Any]:
|
||||
selected_label = dialog_manager.dialog_data.get("selected_device_label", "")
|
||||
return {"selected_device_label": selected_label}
|
||||
|
||||
|
||||
@inject
|
||||
async def invite_getter(
|
||||
dialog_manager: DialogManager,
|
||||
|
||||
@@ -80,40 +80,132 @@ async def on_get_trial(
|
||||
|
||||
|
||||
@inject
|
||||
async def on_device_delete(
|
||||
async def on_device_delete_request(
|
||||
callback: CallbackQuery,
|
||||
widget: Button,
|
||||
dialog_manager: DialogManager,
|
||||
) -> None:
|
||||
selected_short_hwid = dialog_manager.item_id # type: ignore[attr-defined]
|
||||
hwid_map = dialog_manager.dialog_data.get("hwid_map", [])
|
||||
device = next((d for d in hwid_map if d["short_hwid"] == selected_short_hwid), None)
|
||||
|
||||
if not device:
|
||||
raise ValueError(f"Device not found for hwid '{selected_short_hwid}'")
|
||||
|
||||
dialog_manager.dialog_data["selected_short_hwid"] = selected_short_hwid
|
||||
dialog_manager.dialog_data["selected_device_label"] = device["label"]
|
||||
await dialog_manager.switch_to(state=MainMenu.DEVICE_CONFIRM_DELETE)
|
||||
|
||||
|
||||
@inject
|
||||
async def on_device_delete_confirm(
|
||||
callback: CallbackQuery,
|
||||
widget: Button,
|
||||
dialog_manager: DialogManager,
|
||||
subscription_dao: FromDishka[SubscriptionDao],
|
||||
remnawave: FromDishka[Remnawave],
|
||||
i18n: FromDishka[TranslatorRunner],
|
||||
) -> None:
|
||||
selected_short_hwid = dialog_manager.item_id # type: ignore[attr-defined]
|
||||
user: UserDto = dialog_manager.middleware_data[USER_KEY]
|
||||
hwid_map = dialog_manager.dialog_data.get("hwid_map")
|
||||
selected_short_hwid = dialog_manager.dialog_data.get("selected_short_hwid")
|
||||
hwid_map = dialog_manager.dialog_data.get("hwid_map", [])
|
||||
|
||||
if not hwid_map:
|
||||
raise ValueError(f"Selected '{selected_short_hwid}' HWID, but 'hwid_map' is missing")
|
||||
if not selected_short_hwid or not hwid_map:
|
||||
raise ValueError("Missing selected device data")
|
||||
|
||||
full_hwid = next((d["hwid"] for d in hwid_map if d["short_hwid"] == selected_short_hwid), None)
|
||||
|
||||
if not full_hwid:
|
||||
raise ValueError(f"Full HWID not found for '{selected_short_hwid}'")
|
||||
|
||||
current_subscription = await subscription_dao.get_current(user.telegram_id)
|
||||
|
||||
if not (current_subscription and current_subscription.device_limit):
|
||||
raise ValueError("User has no active subscription or device limit unlimited")
|
||||
|
||||
devices = await remnawave.delete_device(
|
||||
await remnawave.delete_device(
|
||||
user_uuid=current_subscription.user_remna_id,
|
||||
hwid=full_hwid,
|
||||
hwid_uuid=full_hwid,
|
||||
)
|
||||
logger.info(f"{user.log} Deleted device '{full_hwid}'")
|
||||
await callback.answer(
|
||||
text=i18n.get("ntf-devices.deleted"),
|
||||
show_alert=True,
|
||||
)
|
||||
await dialog_manager.switch_to(state=MainMenu.DEVICES)
|
||||
|
||||
if devices:
|
||||
return
|
||||
|
||||
await dialog_manager.switch_to(state=MainMenu.MAIN)
|
||||
@inject
|
||||
async def on_device_delete_all_request(
|
||||
callback: CallbackQuery,
|
||||
widget: Button,
|
||||
dialog_manager: DialogManager,
|
||||
) -> None:
|
||||
dialog_manager.dialog_data["selected_device_label"] = ""
|
||||
await dialog_manager.switch_to(state=MainMenu.DEVICE_CONFIRM_DELETE_ALL)
|
||||
|
||||
|
||||
@inject
|
||||
async def on_device_delete_all_confirm(
|
||||
callback: CallbackQuery,
|
||||
widget: Button,
|
||||
dialog_manager: DialogManager,
|
||||
subscription_dao: FromDishka[SubscriptionDao],
|
||||
remnawave: FromDishka[Remnawave],
|
||||
i18n: FromDishka[TranslatorRunner],
|
||||
) -> None:
|
||||
user: UserDto = dialog_manager.middleware_data[USER_KEY]
|
||||
hwid_map = dialog_manager.dialog_data.get("hwid_map", [])
|
||||
|
||||
current_subscription = await subscription_dao.get_current(user.telegram_id)
|
||||
if not (current_subscription and current_subscription.device_limit):
|
||||
raise ValueError("User has no active subscription or device limit unlimited")
|
||||
|
||||
for device in hwid_map:
|
||||
await remnawave.delete_device(
|
||||
user_uuid=current_subscription.user_remna_id,
|
||||
hwid_uuid=device["hwid"],
|
||||
)
|
||||
|
||||
logger.info(f"{user.log} Deleted all devices ({len(hwid_map)})")
|
||||
|
||||
await callback.answer(
|
||||
text=i18n.get("ntf-devices.all-deleted"),
|
||||
show_alert=True,
|
||||
)
|
||||
await dialog_manager.switch_to(state=MainMenu.DEVICES)
|
||||
|
||||
|
||||
@inject
|
||||
async def on_reissue_subscription_request(
|
||||
callback: CallbackQuery,
|
||||
widget: Button,
|
||||
dialog_manager: DialogManager,
|
||||
) -> None:
|
||||
await dialog_manager.switch_to(state=MainMenu.DEVICE_CONFIRM_REISSUE)
|
||||
|
||||
|
||||
@inject
|
||||
async def on_reissue_subscription_confirm(
|
||||
callback: CallbackQuery,
|
||||
widget: Button,
|
||||
dialog_manager: DialogManager,
|
||||
subscription_dao: FromDishka[SubscriptionDao],
|
||||
remnawave: FromDishka[Remnawave],
|
||||
i18n: FromDishka[TranslatorRunner],
|
||||
) -> None:
|
||||
user: UserDto = dialog_manager.middleware_data[USER_KEY]
|
||||
current_subscription = await subscription_dao.get_current(user.telegram_id)
|
||||
|
||||
if not current_subscription:
|
||||
raise ValueError(f"No active subscription for user '{user.telegram_id}'")
|
||||
|
||||
await remnawave.revoke_subscription(current_subscription.user_remna_id)
|
||||
logger.info(f"{user.log} Reissued subscription")
|
||||
|
||||
await callback.answer(
|
||||
text=i18n.get("ntf-devices.reissued"),
|
||||
show_alert=True,
|
||||
)
|
||||
await dialog_manager.switch_to(state=MainMenu.DEVICES)
|
||||
|
||||
|
||||
@inject
|
||||
|
||||
@@ -6,6 +6,9 @@ from aiogram.fsm.state import State, StatesGroup
|
||||
class MainMenu(StatesGroup):
|
||||
MAIN = State()
|
||||
DEVICES = State()
|
||||
DEVICE_CONFIRM_DELETE = State()
|
||||
DEVICE_CONFIRM_DELETE_ALL = State()
|
||||
DEVICE_CONFIRM_REISSUE = State()
|
||||
INVITE = State()
|
||||
INVITE_ABOUT = State()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user