| import asyncio |
| from base64 import urlsafe_b64decode |
| from datetime import datetime, timezone |
| from functools import lru_cache |
| from typing import TYPE_CHECKING, Any, Literal, Optional |
|
|
| from pydantic import BaseModel, Field |
|
|
| from hibiapi.api.bika.constants import BikaConstants |
| from hibiapi.utils.net import BaseNetClient |
|
|
| if TYPE_CHECKING: |
| from .api import BikaEndpoints |
|
|
|
|
| class BikaLogin(BaseModel): |
| email: str |
| password: str |
|
|
|
|
| class JWTHeader(BaseModel): |
| alg: str |
| typ: Literal["JWT"] |
|
|
|
|
| class JWTBody(BaseModel): |
| id: str = Field(alias="_id") |
| iat: datetime |
| exp: datetime |
|
|
|
|
| @lru_cache(maxsize=4) |
| def load_jwt(token: str): |
| def b64pad(data: str): |
| return data + "=" * (-len(data) % 4) |
|
|
| head, body, _ = token.split(".") |
| head_data = JWTHeader.parse_raw(urlsafe_b64decode(b64pad(head))) |
| body_data = JWTBody.parse_raw(urlsafe_b64decode(b64pad(body))) |
| return head_data, body_data |
|
|
|
|
| class NetRequest(BaseNetClient): |
| _token: Optional[str] = None |
|
|
| def __init__(self): |
| super().__init__( |
| headers=BikaConstants.DEFAULT_HEADERS.copy(), |
| proxies=BikaConstants.CONFIG["proxy"].as_dict(), |
| ) |
| self.auth_lock = asyncio.Lock() |
|
|
| @property |
| def token(self) -> Optional[str]: |
| if self._token is None: |
| return None |
| _, body = load_jwt(self._token) |
| return None if body.exp < datetime.now(timezone.utc) else self._token |
|
|
| async def login(self, endpoint: "BikaEndpoints"): |
| login_data = BikaConstants.CONFIG["account"].get(BikaLogin) |
| login_result: dict[str, Any] = await endpoint.request( |
| "auth/sign-in", |
| body=login_data.dict(), |
| no_token=True, |
| ) |
| assert login_result["code"] == 200, login_result["message"] |
| if not ( |
| isinstance(login_data := login_result.get("data"), dict) |
| and "token" in login_data |
| ): |
| raise ValueError("failed to read Bika account token.") |
| self._token = login_data["token"] |
|
|