From a92a7f84f8a32254fd2adc44591eb885d2f969d7 Mon Sep 17 00:00:00 2001 From: luoshuijs Date: Fri, 9 Jun 2023 19:50:48 +0800 Subject: [PATCH] :sparkles: Add transaction client --- simnet/client/components/transaction.py | 104 ++++++++++++++++++++++++ simnet/client/genshin.py | 4 +- simnet/client/genshin.pyi | 9 +- simnet/client/routes.py | 5 ++ simnet/models/genshin/transaction.py | 54 ++++++++++++ 5 files changed, 174 insertions(+), 2 deletions(-) create mode 100644 simnet/client/components/transaction.py create mode 100644 simnet/models/genshin/transaction.py diff --git a/simnet/client/components/transaction.py b/simnet/client/components/transaction.py new file mode 100644 index 0000000..a166a40 --- /dev/null +++ b/simnet/client/components/transaction.py @@ -0,0 +1,104 @@ +from typing import Dict, Any, Optional, List, Union +from urllib import parse + +from simnet.client.base import BaseClient +from simnet.client.routes import YSULOG_URL +from simnet.models.genshin.transaction import BaseTransaction, TransactionKind, ItemTransaction, Transaction +from simnet.utils.lang import create_short_lang_code + + +class TransactionClient(BaseClient): + """Transaction component.""" + + async def request_transaction( + self, + endpoint: str, + authkey: str, + *, + method: str = "GET", + lang: Optional[str] = None, + params: Optional[Dict[str, Any]] = None, + ) -> Dict[str, Any]: + """Make a request towards the transaction log endpoint. + + Args: + endpoint (str): The endpoint to make the request to. + authkey (str): The authkey to use for the request. + method (str, optional): The HTTP method to use. Defaults to "GET". + lang (str, optional): The language to use for the request. Defaults to None. + params (Dict[str, Any], optional): The parameters to use for the request. Defaults to None. + """ + params = dict(params or {}) + + base_url = YSULOG_URL.get_url(self.region) + url = base_url / endpoint + + params["authkey_ver"] = 1 + params["sign_type"] = 2 + params["authkey"] = parse.unquote(authkey) + params["lang"] = create_short_lang_code(lang or self.lang) + + return await self.request_lab(url, method=method, params=params) + + async def _get_transaction_page( + self, + end_id: int, + kind: str, + authkey: str, + *, + lang: Optional[str] = None, + ) -> List[BaseTransaction]: + """Get a single page of transactions. + + Args: + end_id (int): The ID of the last transaction to get. + kind (str): The kind of transaction to get. + authkey (str): The authkey to use for the request. + lang (str, optional): The language to use for the request. Defaults to None. + """ + kind = TransactionKind(kind) + endpoint = "get" + kind.value.capitalize() + "Log" + + data = await self.request_transaction( + endpoint, + lang=lang, + authkey=authkey, + params=dict(end_id=end_id, size=20), + ) + + transactions: List[BaseTransaction] = [] + for trans in data["list"]: + model = ItemTransaction if "name" in trans else Transaction + transactions.append(model(**trans, kind=kind, lang=lang or self.lang)) + + return transactions + + async def transaction_log( + self, + authkey: str, + kind: Optional[Union[str, List[str]]] = None, + *, + limit: Optional[int] = None, + lang: Optional[str] = None, + end_id: int = 0, + ) -> List[BaseTransaction]: + """Get the transaction log of a user. + + Arg: + authkey (str): The authkey to use for the request. + kind (Union[str, List[str]], optional): The kind of transaction to get. Defaults to None. + limit (int, optional): The maximum number of transactions to get. Defaults to None. + lang (str, optional): The language to use for the request. Defaults to None. + end_id (int, optional): The ID of the last transaction to get. Defaults to 0. + """ + kinds = kind or ["primogem", "crystal", "resin", "artifact", "weapon"] + + if isinstance(kinds, str): + kinds = [kinds] + + iterators: List[BaseTransaction] = [] + for value in kinds: + iterator = await self._get_transaction_page(end_id, value, lang=lang, authkey=authkey) + iterators.extend(iterator) + + return iterators[: min(len(iterators), limit)] if limit else iterators diff --git a/simnet/client/genshin.py b/simnet/client/genshin.py index 88c3b5c..813ae87 100644 --- a/simnet/client/genshin.py +++ b/simnet/client/genshin.py @@ -5,6 +5,7 @@ from simnet.client.components.chronicle.genshin import GenshinBattleChronicleCli from simnet.client.components.daily import DailyRewardClient from simnet.client.components.diary.genshin import GenshinDiaryClient from simnet.client.components.lab import LabClient +from simnet.client.components.transaction import TransactionClient from simnet.client.components.wish.genshin import GenshinWishClient from simnet.utils.enum_ import Game @@ -18,7 +19,8 @@ class GenshinClient( AuthClient, DailyRewardClient, LabClient, + TransactionClient, ): - """A simple http client for StarRail endpoints.""" + """A simple http client for Genshin endpoints.""" game: Optional[Game] = Game.GENSHIN diff --git a/simnet/client/genshin.pyi b/simnet/client/genshin.pyi index cc02e2f..22580ef 100644 --- a/simnet/client/genshin.pyi +++ b/simnet/client/genshin.pyi @@ -5,13 +5,20 @@ from simnet.client.components.chronicle.genshin import GenshinBattleChronicleCli from simnet.client.components.daily import DailyRewardClient from simnet.client.components.diary.genshin import GenshinDiaryClient from simnet.client.components.lab import LabClient +from simnet.client.components.transaction import TransactionClient from simnet.client.components.wish.genshin import GenshinWishClient from simnet.utils.enum_ import Region from simnet.utils.types import CookieTypes, HeaderTypes, TimeoutTypes class GenshinClient( - GenshinBattleChronicleClient, GenshinWishClient, GenshinDiaryClient, AuthClient, DailyRewardClient, LabClient + GenshinBattleChronicleClient, + GenshinWishClient, + GenshinDiaryClient, + AuthClient, + DailyRewardClient, + LabClient, + TransactionClient, ): def __init__( self, diff --git a/simnet/client/routes.py b/simnet/client/routes.py index 1ea68e9..d7c0028 100644 --- a/simnet/client/routes.py +++ b/simnet/client/routes.py @@ -27,6 +27,7 @@ __all__ = ( "INFO_LEDGER_URL", "HK4E_URL", "CODE_URL", + "YSULOG_URL", ) @@ -255,6 +256,10 @@ INFO_LEDGER_URL = GameRoute( ), ) +YSULOG_URL = InternationalRoute( + overseas="https://hk4e-api-os.hoyoverse.com/ysulog/api/", + chinese="https://hk4e-api.mihoyo.com/ysulog/api/", +) HK4E_URL = Route("https://sg-hk4e-api.hoyoverse.com/common/hk4e_global/") diff --git a/simnet/models/genshin/transaction.py b/simnet/models/genshin/transaction.py new file mode 100644 index 0000000..52f8cab --- /dev/null +++ b/simnet/models/genshin/transaction.py @@ -0,0 +1,54 @@ +from datetime import datetime +from enum import Enum +from typing import Literal + +from pydantic import Field + +from simnet.models.base import APIModel + +__all__ = ("BaseTransaction", "ItemTransaction", "Transaction", "TransactionKind") + + +class TransactionKind(str, Enum): + """Possible kind of transaction. + + Attributes: + PRIMOGEM: Primogem currency. + CRYSTAL: Genesis crystal currency. + RESIN: Resin currency. + ARTIFACT: Artifact items from domains. + WEAPON: Weapon items from domains and wishes. + """ + + PRIMOGEM = "primogem" + CRYSTAL = "crystal" + RESIN = "resin" + ARTIFACT = "artifact" + WEAPON = "weapon" + + +class BaseTransaction(APIModel): + """Genshin transaction.""" + + kind: TransactionKind + + id: int + uid: int + time: datetime + amount: int = Field(alias="add_num") + reason_id: int = Field(alias="reason") + + +class Transaction(BaseTransaction): + """Genshin transaction of currency.""" + + kind: Literal[TransactionKind.PRIMOGEM, TransactionKind.CRYSTAL, TransactionKind.RESIN] + + +class ItemTransaction(BaseTransaction): + """Genshin transaction of artifacts or weapons.""" + + kind: Literal[TransactionKind.ARTIFACT, TransactionKind.WEAPON] + + name: str + rarity: int = Field(alias="rank")