Add 'My Devices' feature to user subscription management
- Implemented a new command handler for displaying user devices. - Added functionality to disconnect devices from the user's account. - Updated subscription service to retrieve and manage device information. - Enhanced inline keyboard to include device management options. - Added new translations for device-related messages in English and Russian locales.
This commit is contained in:
@@ -404,10 +404,13 @@ async def main_action_callback_handler(
|
|||||||
await user_subscription_handlers.display_subscription_options(
|
await user_subscription_handlers.display_subscription_options(
|
||||||
callback, i18n_data, settings, session)
|
callback, i18n_data, settings, session)
|
||||||
elif action == "my_subscription":
|
elif action == "my_subscription":
|
||||||
|
|
||||||
await user_subscription_handlers.my_subscription_command_handler(
|
await user_subscription_handlers.my_subscription_command_handler(
|
||||||
callback, i18n_data, settings, panel_service, subscription_service,
|
callback, i18n_data, settings, panel_service, subscription_service,
|
||||||
session, bot)
|
session, bot)
|
||||||
|
elif action == "my_devices":
|
||||||
|
await user_subscription_handlers.my_devices_command_handler(
|
||||||
|
callback, i18n_data, settings, panel_service, subscription_service,
|
||||||
|
session, bot)
|
||||||
elif action == "referral":
|
elif action == "referral":
|
||||||
await user_referral_handlers.referral_command_handler(
|
await user_referral_handlers.referral_command_handler(
|
||||||
callback, settings, i18n_data, referral_service, bot, session)
|
callback, settings, i18n_data, referral_service, bot, session)
|
||||||
|
|||||||
@@ -12,6 +12,6 @@ router.include_router(payments.router)
|
|||||||
router.include_router(payment_methods.router)
|
router.include_router(payment_methods.router)
|
||||||
|
|
||||||
# Re-export commonly used entrypoints for backward compatibility
|
# Re-export commonly used entrypoints for backward compatibility
|
||||||
from .core import display_subscription_options, my_subscription_command_handler # noqa: E402,F401
|
from .core import display_subscription_options, my_subscription_command_handler, my_devices_command_handler # noqa: E402,F401
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -173,6 +173,13 @@ async def my_subscription_command_handler(
|
|||||||
)
|
)
|
||||||
])
|
])
|
||||||
|
|
||||||
|
prepend_rows.append([
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=get_text("devices_button"),
|
||||||
|
callback_data="main_action:my_devices",
|
||||||
|
)
|
||||||
|
])
|
||||||
|
|
||||||
# 2) Auto-renew toggle (if supported and not tribute)
|
# 2) Auto-renew toggle (if supported and not tribute)
|
||||||
if local_sub and local_sub.provider != "tribute" and getattr(settings, 'YOOKASSA_AUTOPAYMENTS_ENABLED', False):
|
if local_sub and local_sub.provider != "tribute" and getattr(settings, 'YOOKASSA_AUTOPAYMENTS_ENABLED', False):
|
||||||
toggle_text = (
|
toggle_text = (
|
||||||
@@ -216,6 +223,120 @@ async def my_subscription_command_handler(
|
|||||||
await target.answer(text + tribute_hint, reply_markup=markup, parse_mode="HTML", disable_web_page_preview=True)
|
await target.answer(text + tribute_hint, reply_markup=markup, parse_mode="HTML", disable_web_page_preview=True)
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data == "main_action:my_devices")
|
||||||
|
async def my_devices_command_handler(
|
||||||
|
event: Union[types.Message, types.CallbackQuery],
|
||||||
|
i18n_data: dict,
|
||||||
|
settings: Settings,
|
||||||
|
panel_service: PanelApiService,
|
||||||
|
subscription_service: SubscriptionService,
|
||||||
|
session: AsyncSession,
|
||||||
|
bot: Bot,
|
||||||
|
):
|
||||||
|
target = event.message if isinstance(event, types.CallbackQuery) else event
|
||||||
|
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||||
|
i18n: JsonI18n = i18n_data.get("i18n_instance")
|
||||||
|
get_text = lambda key, **kw: i18n.gettext(current_lang, key, **kw)
|
||||||
|
|
||||||
|
if not i18n or not target:
|
||||||
|
if isinstance(event, types.Message):
|
||||||
|
await event.answer(get_text("error_occurred_try_again"))
|
||||||
|
return
|
||||||
|
|
||||||
|
# TODO: context?
|
||||||
|
active = await subscription_service.get_active_subscription_details(session, event.from_user.id)
|
||||||
|
|
||||||
|
devices = await panel_service.get_user_devices(active.get("user_id")) if active else None
|
||||||
|
if not devices:
|
||||||
|
await target.answer(get_text("no_devices_found"))
|
||||||
|
return
|
||||||
|
|
||||||
|
max_devices = active.get("max_devices")
|
||||||
|
|
||||||
|
if not devices or not devices.get('devices') or len(devices.get('devices')) == 0:
|
||||||
|
text = get_text("no_devices_details_found_message", max_devices=max_devices)
|
||||||
|
else:
|
||||||
|
devices_list = []
|
||||||
|
current_devices = len(devices.get('devices') or [])
|
||||||
|
for index, device in enumerate(devices.get('devices') or [], start=1):
|
||||||
|
device_model = device.get('deviceModel') or None
|
||||||
|
platform = device.get('platform') or None
|
||||||
|
user_agent = device.get('userAgent') or None
|
||||||
|
os_version = device.get('osVersion') or None
|
||||||
|
created_at = device.get('createdAt')
|
||||||
|
hwid = device.get('hwid')
|
||||||
|
created_at_str = datetime.fromisoformat(created_at).strftime("%d.%m.%Y %H:%M")
|
||||||
|
|
||||||
|
device_details = get_text("device_details", index=index, device_model=device_model, platform=platform, os_version=os_version, created_at_str=created_at_str, user_agent=user_agent, hwid=hwid)
|
||||||
|
devices_list.append(device_details)
|
||||||
|
|
||||||
|
text = get_text("my_devices_details", devices="\n\n".join(devices_list), current_devices=current_devices, max_devices=max_devices)
|
||||||
|
|
||||||
|
base_markup = get_back_to_main_menu_markup(current_lang, i18n, callback_data="main_action:my_subscription")
|
||||||
|
kb = base_markup.inline_keyboard
|
||||||
|
|
||||||
|
devices_kb = []
|
||||||
|
for index, device in enumerate(devices.get('devices') or [], start=1):
|
||||||
|
hwid = device.get('hwid')
|
||||||
|
device_button_text = get_text("disconnect_device_button", hwid=hwid, index=index)
|
||||||
|
|
||||||
|
devices_kb.append([InlineKeyboardButton(text=device_button_text, callback_data=f"disconnect_device:{hwid}")])
|
||||||
|
kb = devices_kb + kb
|
||||||
|
markup = InlineKeyboardMarkup(inline_keyboard=kb)
|
||||||
|
|
||||||
|
if isinstance(event, types.CallbackQuery):
|
||||||
|
try:
|
||||||
|
await event.answer()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
await event.message.edit_text(text, reply_markup=markup)
|
||||||
|
except Exception:
|
||||||
|
await event.message.answer(text, reply_markup=markup)
|
||||||
|
else:
|
||||||
|
await target.answer(text, reply_markup=markup)
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data.startswith("disconnect_device:"))
|
||||||
|
async def disconnect_device_handler(
|
||||||
|
callback: types.CallbackQuery,
|
||||||
|
settings: Settings,
|
||||||
|
i18n_data: dict,
|
||||||
|
session: AsyncSession,
|
||||||
|
subscription_service: SubscriptionService,
|
||||||
|
panel_service: PanelApiService,
|
||||||
|
bot: Bot,
|
||||||
|
):
|
||||||
|
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||||
|
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||||
|
get_text = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) if i18n else key
|
||||||
|
|
||||||
|
try:
|
||||||
|
_, hwid = callback.data.split(":", 1)
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
await callback.answer(get_text("error_try_again"), show_alert=True)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
|
||||||
|
active = await subscription_service.get_active_subscription_details(session, callback.from_user.id)
|
||||||
|
if not active:
|
||||||
|
await callback.answer(get_text("subscription_not_active"), show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
success = await panel_service.disconnect_device(active.get("user_id"), hwid)
|
||||||
|
if not success:
|
||||||
|
await callback.answer(get_text("error_try_again"), show_alert=True)
|
||||||
|
return
|
||||||
|
await session.commit()
|
||||||
|
try:
|
||||||
|
await callback.answer(get_text("device_disconnected"))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
await my_devices_command_handler(callback, i18n_data, settings, panel_service, subscription_service, session, bot)
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(F.data.startswith("toggle_autorenew:"))
|
@router.callback_query(F.data.startswith("toggle_autorenew:"))
|
||||||
async def toggle_autorenew_handler(
|
async def toggle_autorenew_handler(
|
||||||
callback: types.CallbackQuery,
|
callback: types.CallbackQuery,
|
||||||
|
|||||||
@@ -159,9 +159,14 @@ def get_referral_link_keyboard(lang: str,
|
|||||||
|
|
||||||
|
|
||||||
def get_back_to_main_menu_markup(lang: str,
|
def get_back_to_main_menu_markup(lang: str,
|
||||||
i18n_instance) -> InlineKeyboardMarkup:
|
i18n_instance,
|
||||||
|
callback_data: Optional[str] = None) -> InlineKeyboardMarkup:
|
||||||
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
||||||
builder = InlineKeyboardBuilder()
|
builder = InlineKeyboardBuilder()
|
||||||
|
if callback_data:
|
||||||
|
builder.button(text=_(key="back_to_main_menu_button"),
|
||||||
|
callback_data=callback_data)
|
||||||
|
else:
|
||||||
builder.button(text=_(key="back_to_main_menu_button"),
|
builder.button(text=_(key="back_to_main_menu_button"),
|
||||||
callback_data="main_action:back_to_main")
|
callback_data="main_action:back_to_main")
|
||||||
return builder.as_markup()
|
return builder.as_markup()
|
||||||
|
|||||||
@@ -455,6 +455,30 @@ class PanelApiService:
|
|||||||
return f"{base_sub_url}/{client_type.lower()}"
|
return f"{base_sub_url}/{client_type.lower()}"
|
||||||
return base_sub_url
|
return base_sub_url
|
||||||
|
|
||||||
|
async def get_user_devices(self, user_uuid: str) -> Optional[List[Dict[str, Any]]]:
|
||||||
|
endpoint = f"/hwid/devices/{user_uuid}"
|
||||||
|
response_data = await self._request("GET", endpoint, log_full_response=False)
|
||||||
|
if response_data and not response_data.get("error") and "response" in response_data:
|
||||||
|
return response_data.get("response")
|
||||||
|
logging.error(
|
||||||
|
f"Failed to get user devices for user {user_uuid}. Response: {response_data}"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def disconnect_device(self, user_uuid: str, hwid: str) -> bool:
|
||||||
|
endpoint = f"/hwid/devices/delete"
|
||||||
|
payload = {
|
||||||
|
"userUuid": user_uuid,
|
||||||
|
"hwid": hwid
|
||||||
|
}
|
||||||
|
response_data = await self._request("POST", endpoint, json=payload, log_full_response=False)
|
||||||
|
if response_data and not response_data.get("error") and "response" in response_data:
|
||||||
|
return True
|
||||||
|
logging.error(
|
||||||
|
f"Failed to disconnect device {hwid} for user {user_uuid}. Payload: {payload}, Response: {response_data}"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
async def update_bot_db_sync_status(self,
|
async def update_bot_db_sync_status(self,
|
||||||
session: AsyncSession,
|
session: AsyncSession,
|
||||||
status: str,
|
status: str,
|
||||||
@@ -469,7 +493,6 @@ class PanelApiService:
|
|||||||
self, session: AsyncSession) -> Optional[PanelSyncStatus]:
|
self, session: AsyncSession) -> Optional[PanelSyncStatus]:
|
||||||
return await panel_sync_dal.get_panel_sync_status(session)
|
return await panel_sync_dal.get_panel_sync_status(session)
|
||||||
|
|
||||||
|
|
||||||
async def get_system_stats(self) -> Optional[Dict[str, Any]]:
|
async def get_system_stats(self) -> Optional[Dict[str, Any]]:
|
||||||
"""Get system statistics (CPU, memory, users counts)"""
|
"""Get system statistics (CPU, memory, users counts)"""
|
||||||
response_data = await self._request("GET", "/system/stats", log_full_response=False)
|
response_data = await self._request("GET", "/system/stats", log_full_response=False)
|
||||||
|
|||||||
@@ -741,6 +741,7 @@ class SubscriptionService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
"user_id": panel_user_data.get("uuid"),
|
||||||
"end_date": panel_end_date,
|
"end_date": panel_end_date,
|
||||||
"status_from_panel": panel_user_data.get("status", "UNKNOWN").upper(),
|
"status_from_panel": panel_user_data.get("status", "UNKNOWN").upper(),
|
||||||
"config_link": panel_user_data.get("subscriptionUrl"),
|
"config_link": panel_user_data.get("subscriptionUrl"),
|
||||||
@@ -748,6 +749,7 @@ class SubscriptionService:
|
|||||||
"traffic_used_bytes": panel_user_data.get("usedTrafficBytes"),
|
"traffic_used_bytes": panel_user_data.get("usedTrafficBytes"),
|
||||||
"user_bot_username": db_user.username,
|
"user_bot_username": db_user.username,
|
||||||
"is_panel_data": True,
|
"is_panel_data": True,
|
||||||
|
"max_devices": panel_user_data.get("hwidDeviceLimit"),
|
||||||
}
|
}
|
||||||
|
|
||||||
async def get_subscriptions_ending_soon(
|
async def get_subscriptions_ending_soon(
|
||||||
|
|||||||
@@ -35,6 +35,15 @@
|
|||||||
"pay_with_stars_button": "🌟 Telegram Stars",
|
"pay_with_stars_button": "🌟 Telegram Stars",
|
||||||
"connect_button": "🔗 Connect",
|
"connect_button": "🔗 Connect",
|
||||||
"cancel_button": "❌ Cancel",
|
"cancel_button": "❌ Cancel",
|
||||||
|
|
||||||
|
"devices_button": "📱 My Devices ({current_devices}/{max_devices})",
|
||||||
|
"my_devices_details": "📱 <b>My Devices ({current_devices}/{max_devices})</b>\n\n{devices}\n\nYou can disconnect a device by selecting it from the list below.\n<blockquote><i>Note: If you disconnect a device, it will be automatically connected again when you use it next. Before deleting, make sure you have deleted the subscription from the application.</i></blockquote>",
|
||||||
|
"no_devices_details_found_message": "📱 <b>My Devices</b>\n\nYou don't have any devices yet.\nYou have {max_devices} devices available. You can connect them by clicking the \"🔗 Connect\" button in the subscription menu.",
|
||||||
|
"device_details": "{index}) <b>{device_model} ({platform} {os_version})</b>\nDate connected: <b>{created_at_str}</b>\nUser Agent: <b>{user_agent}</b>\nHWID: <code>{hwid}</code>",
|
||||||
|
"disconnect_device_button": "🚫 Disconnect {hwid} ({index})",
|
||||||
|
"device_disconnected": "Device disconnected successfully.",
|
||||||
|
"no_devices_found": "Error loading device list.",
|
||||||
|
|
||||||
"payment_description_subscription": "Subscription payment for {months} mo.",
|
"payment_description_subscription": "Subscription payment for {months} mo.",
|
||||||
"payment_link_message": "To pay for {months} mo. subscription, click the button below:",
|
"payment_link_message": "To pay for {months} mo. subscription, click the button below:",
|
||||||
"payment_successful_error_details": "✅ Payment succeeded, but an error occurred displaying details. Your subscription is active. Contact support if anything is wrong.",
|
"payment_successful_error_details": "✅ Payment succeeded, but an error occurred displaying details. Your subscription is active. Contact support if anything is wrong.",
|
||||||
|
|||||||
@@ -34,6 +34,15 @@
|
|||||||
"pay_with_tribute_button": "❤️ Tribute",
|
"pay_with_tribute_button": "❤️ Tribute",
|
||||||
"pay_with_stars_button": "🌟 Звезды Telegram",
|
"pay_with_stars_button": "🌟 Звезды Telegram",
|
||||||
"connect_button": "🔗 Подключиться",
|
"connect_button": "🔗 Подключиться",
|
||||||
|
|
||||||
|
"devices_button": "📱 Мои устройства",
|
||||||
|
"my_devices_details": "📱 <b>Список ваших устройств ({current_devices}/{max_devices})</b>\n\n{devices}\n\nВы можете отключить устройство, выбрав его в списке ниже.\n<blockquote><i>Примечание: Если вы отключили устройство, оно будет автоматически подключено заново при следующем использовании. Перед удалением убедитесь, что вы удалили подписку из приложения.</i></blockquote>",
|
||||||
|
"no_devices_details_found_message": "📱 <b>Список ваших устройств</b>\n\nУ вас пока нет устройств.\nВам доступно {max_devices} устройств. Подключить их можно через кнопку \"🔗 Подключиться\" в меню подписки.",
|
||||||
|
"device_details": "{index}) <b>{device_model} ({platform} {os_version})</b>\nДата подключения: <b>{created_at_str}</b>\nUser Agent: <b>{user_agent}</b>\nHWID: <code>{hwid}</code>",
|
||||||
|
"disconnect_device_button": "🚫 Отключить {hwid} ({index})",
|
||||||
|
"device_disconnected": "Устройство успешно отключено.",
|
||||||
|
"no_devices_found": "Ошибка загрузки списка устройств.",
|
||||||
|
|
||||||
"cancel_button": "❌ Отмена",
|
"cancel_button": "❌ Отмена",
|
||||||
"payment_description_subscription": "Оплата подписки на {months} мес.",
|
"payment_description_subscription": "Оплата подписки на {months} мес.",
|
||||||
"payment_link_message": "Для оплаты подписки на {months} мес., нажмите кнопку ниже:",
|
"payment_link_message": "Для оплаты подписки на {months} мес., нажмите кнопку ниже:",
|
||||||
|
|||||||
Reference in New Issue
Block a user