# ruff: noqa: F401,F403,F405,I001 from ._runtime import * # noqa: F403,F405 from bot.app.web.webapp.cache_helpers import webapp_cached_user_payload async def devices_route(request: web.Request) -> web.Response: user_id = _require_user_id(request) settings: Settings = request.app["settings"] if not settings.MY_DEVICES_SECTION_ENABLED: return _json_error(404, "devices_disabled", "Devices section is disabled") async_session_factory: sessionmaker = request.app["async_session_factory"] subscription_service: SubscriptionService = request.app["subscription_service"] async with async_session_factory() as session: db_user = await user_dal.get_user_by_id(session, user_id) if not db_user or db_user.is_banned: return _json_error(403, "access_denied", "Access denied") result = await webapp_cached_user_payload( settings, "devices", user_id, int(getattr(settings, "WEBAPP_DEVICES_CACHE_TTL_SECONDS", 5) or 0), lambda: _load_devices_payload( subscription_service, session, user_id, fallback_panel_user_uuid=str(getattr(db_user, "panel_user_uuid", "") or "").strip() or None, ), ) if isinstance(result, dict) and result.get("ok") is True: return web.json_response({"ok": True, **(result.get("payload") or {})}) if isinstance(result, dict) and not result.get("error"): # Backward-compatible with payloads written by older versions under # the same Redis cache key. return web.json_response({"ok": True, **result}) if not isinstance(result, dict): result = {} if not result.get("ok"): return _json_error( int(result.get("status") or 500), str(result.get("error") or "devices_load_failed"), str(result.get("message") or "Failed to load devices"), ) return web.json_response({"ok": True, **(result.get("payload") or {})}) async def _load_devices_payload( subscription_service: SubscriptionService, session: AsyncSession, user_id: int, fallback_panel_user_uuid: Optional[str] = None, ) -> Dict[str, Any]: active = await subscription_service.get_active_subscription_details(session, user_id) panel_user_uuid = str((active or {}).get("user_id") or fallback_panel_user_uuid or "").strip() if not panel_user_uuid: return _empty_inactive_devices_payload() panel_service = getattr(subscription_service, "panel_service", None) if not panel_service: return { "ok": False, "status": 503, "error": "panel_unavailable", "message": "Panel service unavailable", } try: devices_response = await panel_service.get_user_devices(panel_user_uuid) except Exception: logger.exception("Failed to load WebApp devices for user %s", user_id) return { "ok": False, "status": 502, "error": "devices_load_failed", "message": "Failed to load devices", } devices = _normalize_devices_response(devices_response) max_devices = _coerce_int_or_none(active.get("max_devices")) if active else None return { "ok": True, "payload": { "enabled": True, "subscription_active": _devices_subscription_is_active(active), "current_devices": len(devices), "max_devices": max_devices, "max_devices_label": _format_devices_limit(max_devices), "devices": [ _serialize_device(device, index) for index, device in enumerate(devices, start=1) ], }, } def _empty_inactive_devices_payload() -> Dict[str, Any]: return { "ok": True, "payload": { "enabled": True, "subscription_active": False, "current_devices": 0, "max_devices": None, "max_devices_label": _format_devices_limit(None), "devices": [], }, } def _devices_subscription_is_active(active: Optional[Dict[str, Any]]) -> bool: if not active: return False end_date = active.get("end_date") if not isinstance(end_date, datetime): return False if end_date.tzinfo is None: end_date = end_date.replace(tzinfo=timezone.utc) return end_date > datetime.now(timezone.utc) async def disconnect_device_route(request: web.Request) -> web.Response: user_id = _require_user_id(request) rate_limit_response = await _enforce_webapp_rate_limit( request, user_id=user_id, action="devices_disconnect", ) if rate_limit_response: return rate_limit_response settings: Settings = request.app["settings"] if not settings.MY_DEVICES_SECTION_ENABLED: return _json_error(404, "devices_disabled", "Devices section is disabled") payload = await _read_json(request) disconnect_payload, validation_error = _validate_model_payload( WebAppDeviceDisconnectPayload, payload ) if validation_error: return validation_error token = str(disconnect_payload.token or "").strip() async_session_factory: sessionmaker = request.app["async_session_factory"] subscription_service: SubscriptionService = request.app["subscription_service"] async with async_session_factory() as session: db_user = await user_dal.get_user_by_id(session, user_id) if not db_user or db_user.is_banned: return _json_error(403, "access_denied", "Access denied") active = await subscription_service.get_active_subscription_details(session, user_id) panel_user_uuid = active.get("user_id") if active else None if not panel_user_uuid: return _json_error(400, "subscription_not_active", "Subscription is not active") panel_service = getattr(subscription_service, "panel_service", None) if not panel_service: return _json_error(503, "panel_unavailable", "Panel service unavailable") try: devices_response = await panel_service.get_user_devices(panel_user_uuid) except Exception: logger.exception("Failed to load WebApp devices before disconnect for user %s", user_id) return _json_error(502, "devices_load_failed", "Failed to load devices") target_hwid = None for device in _normalize_devices_response(devices_response): hwid = str(device.get("hwid") or "").strip() if hwid and hmac.compare_digest(_device_hwid_token(hwid), token): target_hwid = hwid break if not target_hwid: return _json_error(404, "device_not_found", "Device not found") success = await panel_service.disconnect_device(panel_user_uuid, target_hwid) if not success: return _json_error(502, "device_disconnect_failed", "Failed to disconnect device") await cache_delete(settings, redis_key(settings, "cache", "webapp", "devices", user_id)) await session.commit() return web.json_response({"ok": True}) def _device_hwid_token(hwid: str) -> str: return hashlib.sha256(str(hwid or "").encode()).hexdigest()[:32] def _shorten_hwid_for_display(hwid: Optional[str], max_length: int = 24) -> str: value = str(hwid or "").strip() if len(value) <= max_length: return value return f"{value[:8]}...{value[-6:]}" def _normalize_devices_response(devices_response: Any) -> List[Dict[str, Any]]: if isinstance(devices_response, dict): devices = devices_response.get("devices") or [] else: devices = devices_response or [] if not isinstance(devices, list): return [] return [device for device in devices if isinstance(device, dict)] def _format_devices_limit(max_devices: Optional[int]) -> str: if max_devices in (None, 0): return "Unlimited" return str(max_devices) def _format_device_datetime(value: Any) -> str: if not value: return "" text = str(value) try: normalized = datetime.fromisoformat(text.replace("Z", "+00:00")) return normalized.strftime("%d.%m.%Y %H:%M") except Exception: return text def _serialize_device_datetime(value: Any) -> Optional[str]: if not value: return None if isinstance(value, datetime): normalized = value if value.tzinfo else value.replace(tzinfo=timezone.utc) return normalized.isoformat() return str(value) def _serialize_device(device: Dict[str, Any], index: int) -> Dict[str, Any]: hwid = str(device.get("hwid") or "").strip() model = str(device.get("deviceModel") or "").strip() platform = str(device.get("platform") or "").strip() os_version = str(device.get("osVersion") or "").strip() user_agent = str(device.get("userAgent") or "").strip() display_name = model or platform or f"Device {index}" platform_label = " ".join(part for part in (platform, os_version) if part).strip() return { "index": index, "display_name": display_name, "platform": platform, "os_version": os_version, "platform_label": platform_label, "user_agent": user_agent, "created_at": _serialize_device_datetime(device.get("createdAt")), "created_at_text": _format_device_datetime(device.get("createdAt")), "hwid_short": _shorten_hwid_for_display(hwid), "token": _device_hwid_token(hwid) if hwid else "", "can_disconnect": bool(hwid), }