mirror of
https://github.com/PaiGramTeam/SIMNet.git
synced 2024-11-21 13:48:20 +00:00
✨ Support ZZZClient
This commit is contained in:
parent
05fcb568d6
commit
074939d881
@ -8,7 +8,7 @@ readme = "README.md"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.8"
|
||||
httpx = "^0.25.0"
|
||||
httpx = ">=0.25.0"
|
||||
pydantic = "<2.0.0,>=1.10.7"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
|
@ -1,5 +1,6 @@
|
||||
from simnet.client.genshin import GenshinClient
|
||||
from simnet.client.starrail import StarRailClient
|
||||
from simnet.client.zzz import ZZZClient
|
||||
from simnet.utils.enums import Game, Region
|
||||
|
||||
__all__ = ("StarRailClient", "GenshinClient", "Game", "Region")
|
||||
__all__ = ("StarRailClient", "GenshinClient", "ZZZClient", "Game", "Region")
|
||||
|
@ -217,6 +217,8 @@ class BaseClient(AsyncContextManager["BaseClient"]):
|
||||
headers["x-rpc-device_id"] = self.get_device_id()
|
||||
headers["x-rpc-device_fp"] = self.get_device_fp()
|
||||
if self.region == Region.OVERSEAS:
|
||||
if self.game == Game.ZZZ:
|
||||
headers["x-rpc-lang"] = self.lang or lang
|
||||
headers["x-rpc-language"] = self.lang or lang
|
||||
if ds is None:
|
||||
app_version, client_type, ds = generate_dynamic_secret(self.region, ds_type, new_ds, data, params)
|
||||
|
@ -50,10 +50,13 @@ class BaseChronicleClient(BaseClient):
|
||||
TimedOut: If the request times out.
|
||||
BadRequest: If the response contains an error.
|
||||
"""
|
||||
base_url = RECORD_URL.get_url(region or self.region)
|
||||
base_url = RECORD_URL.get_url(region or self.region, game or Game.GENSHIN)
|
||||
|
||||
if game:
|
||||
base_url = base_url / game.value / endpoint_type
|
||||
if game == Game.ZZZ:
|
||||
base_url = base_url / endpoint_type / "zzz"
|
||||
else:
|
||||
base_url = base_url / game.value / endpoint_type
|
||||
|
||||
url = base_url / endpoint
|
||||
new_ds = self.region == Region.CHINESE
|
||||
|
@ -312,7 +312,7 @@ class GenshinBattleChronicleClient(BaseChronicleClient):
|
||||
if self.account_id is not None and stuid is None:
|
||||
self.cookies.set("stuid", str(self.account_id))
|
||||
if self.region == Region.OVERSEAS:
|
||||
route = RECORD_URL.get_url(self.region) / "../community/apihub/api/widget/data"
|
||||
route = RECORD_URL.get_url(self.region, self.game) / "../community/apihub/api/widget/data"
|
||||
params = {"game_id": "2"}
|
||||
data = await self.request_lab(route, params=params, lang=lang)
|
||||
model = NotesOverseaWidget
|
||||
|
@ -412,7 +412,7 @@ class StarRailBattleChronicleClient(BaseChronicleClient):
|
||||
if self.account_id is not None and stuid is None:
|
||||
self.cookies.set("stuid", str(self.account_id))
|
||||
if self.region == Region.OVERSEAS:
|
||||
route = RECORD_URL.get_url(self.region) / "../community/apihub/api/hsr_widget"
|
||||
route = RECORD_URL.get_url(self.region, self.game) / "../community/apihub/api/hsr_widget"
|
||||
data = await self.request_lab(route, lang=lang)
|
||||
model = StarRailNoteOverseaWidget
|
||||
else:
|
||||
|
218
simnet/client/components/chronicle/zzz.py
Normal file
218
simnet/client/components/chronicle/zzz.py
Normal file
@ -0,0 +1,218 @@
|
||||
from typing import Optional, Mapping, Dict, Any, List
|
||||
|
||||
from simnet.client.components.chronicle.base import BaseChronicleClient
|
||||
from simnet.errors import BadRequest, DataNotPublic
|
||||
from simnet.models.lab.record import RecordCard
|
||||
from simnet.models.zzz.calculator import ZZZCalculatorCharacterDetails
|
||||
from simnet.models.zzz.chronicle.notes import ZZZNote
|
||||
from simnet.models.zzz.chronicle.stats import ZZZUserStats, ZZZAvatarBasic, ZZZBuddyBasic
|
||||
from simnet.utils.enums import Game
|
||||
from simnet.utils.player import recognize_region, recognize_zzz_server
|
||||
|
||||
__all__ = ("ZZZBattleChronicleClient",)
|
||||
|
||||
|
||||
class ZZZBattleChronicleClient(BaseChronicleClient):
|
||||
"""A client for retrieving data from ZZZ's battle chronicle component.
|
||||
|
||||
This class is used to retrieve various data objects from StarRail's battle chronicle component,
|
||||
including real-time notes, user statistics, and character information.
|
||||
"""
|
||||
|
||||
async def _request_zzz_record(
|
||||
self,
|
||||
endpoint: str,
|
||||
player_id: Optional[int] = None,
|
||||
endpoint_type: str = "api",
|
||||
method: str = "GET",
|
||||
lang: Optional[str] = None,
|
||||
payload: Optional[Dict[str, Any]] = None,
|
||||
) -> Mapping[str, Any]:
|
||||
"""Get an arbitrary object from ZZZ's battle chronicle.
|
||||
|
||||
Args:
|
||||
endpoint (str): The endpoint of the object to retrieve.
|
||||
player_id (Optional[int], optional): The player ID. Defaults to None.
|
||||
method (str, optional): The HTTP method to use. Defaults to "GET".
|
||||
lang (Optional[str], optional): The language of the data. Defaults to None.
|
||||
payload (Optional[Dict[str, Any]], optional): The request payload. Defaults to None.
|
||||
|
||||
Returns:
|
||||
Mapping[str, Any]: The requested object.
|
||||
|
||||
Raises:
|
||||
BadRequest: If the request is invalid.
|
||||
DataNotPublic: If the requested data is not public.
|
||||
"""
|
||||
payload = dict(payload or {})
|
||||
|
||||
player_id = player_id or self.player_id
|
||||
payload = dict(role_id=player_id, server=recognize_zzz_server(player_id), **payload)
|
||||
|
||||
data, params = None, None
|
||||
if method == "POST":
|
||||
data = payload
|
||||
else:
|
||||
params = payload
|
||||
|
||||
return await self.request_game_record(
|
||||
endpoint,
|
||||
endpoint_type=endpoint_type,
|
||||
lang=lang,
|
||||
game=Game.ZZZ,
|
||||
region=recognize_region(player_id, game=Game.ZZZ),
|
||||
params=params,
|
||||
data=data,
|
||||
)
|
||||
|
||||
async def get_zzz_notes(
|
||||
self,
|
||||
player_id: Optional[int] = None,
|
||||
lang: Optional[str] = None,
|
||||
autoauth: bool = True,
|
||||
) -> ZZZNote:
|
||||
"""Get ZZZ's real-time notes.
|
||||
|
||||
Args:
|
||||
player_id (Optional[int], optional): The player ID. Defaults to None.
|
||||
lang (Optional[str], optional): The language of the data. Defaults to None.
|
||||
autoauth (bool, optional): Whether to automatically authenticate the user. Defaults to True.
|
||||
|
||||
Returns:
|
||||
ZZZNote: The requested real-time notes.
|
||||
|
||||
Raises:
|
||||
BadRequest: If the request is invalid.
|
||||
DataNotPublic: If the requested data is not public.
|
||||
"""
|
||||
try:
|
||||
data = await self._request_zzz_record("note", player_id, lang=lang)
|
||||
except DataNotPublic as e:
|
||||
# error raised only when real-time notes are not enabled
|
||||
if player_id and self.player_id != player_id:
|
||||
raise BadRequest(e.response, "Cannot view real-time notes of other users.") from e
|
||||
if not autoauth:
|
||||
raise BadRequest(e.response, "Real-time notes are not enabled.") from e
|
||||
await self.update_settings(3, True, game=Game.ZZZ)
|
||||
data = await self._request_zzz_record("note", player_id, lang=lang)
|
||||
|
||||
return ZZZNote(**data)
|
||||
|
||||
async def get_zzz_user(
|
||||
self,
|
||||
player_id: Optional[int] = None,
|
||||
*,
|
||||
lang: Optional[str] = None,
|
||||
) -> "ZZZUserStats":
|
||||
"""Get ZZZ user statistics.
|
||||
|
||||
Args:
|
||||
player_id (Optional[int], optional): The player ID. Defaults to None.
|
||||
lang (Optional[str], optional): The language of the data. Defaults to None.
|
||||
|
||||
Returns:
|
||||
ZZZUserStats: The requested user statistics.
|
||||
|
||||
Raises:
|
||||
BadRequest: If the request is invalid.
|
||||
DataNotPublic: If the requested data is not public.
|
||||
"""
|
||||
data = await self._request_zzz_record("index", player_id, lang=lang)
|
||||
return ZZZUserStats(**data)
|
||||
|
||||
async def get_zzz_characters(
|
||||
self,
|
||||
player_id: Optional[int] = None,
|
||||
lang: Optional[str] = None,
|
||||
) -> "ZZZAvatarBasic":
|
||||
"""Get ZZZ character basic information.
|
||||
|
||||
Args:
|
||||
player_id (Optional[int], optional): The player ID. Defaults to None.
|
||||
lang (Optional[str], optional): The language of the data. Defaults to None.
|
||||
|
||||
Returns:
|
||||
ZZZAvatarBasic: The requested character information.
|
||||
|
||||
Raises:
|
||||
BadRequest: If the request is invalid.
|
||||
DataNotPublic: If the requested data is not public.
|
||||
"""
|
||||
data = await self._request_zzz_record("avatar/basic", player_id, lang=lang)
|
||||
return ZZZAvatarBasic(**data)
|
||||
|
||||
async def get_zzz_character_info(
|
||||
self,
|
||||
characters: List[int],
|
||||
player_id: Optional[int] = None,
|
||||
lang: Optional[str] = None,
|
||||
) -> "ZZZCalculatorCharacterDetails":
|
||||
"""Get ZZZ character detail information.
|
||||
|
||||
Args:
|
||||
characters (List[int]): A list of character IDs.
|
||||
player_id (Optional[int], optional): The player ID. Defaults to None.
|
||||
lang (Optional[str], optional): The language of the data. Defaults to None.
|
||||
|
||||
Returns:
|
||||
ZZZCalculatorCharacterDetails: The requested character information.
|
||||
|
||||
Raises:
|
||||
BadRequest: If the request is invalid.
|
||||
DataNotPublic: If the requested data is not public.
|
||||
"""
|
||||
ch = characters
|
||||
if isinstance(characters, int):
|
||||
ch = [characters]
|
||||
payload = {"need_wiki": "true", "id_list[]": ch}
|
||||
data = await self._request_zzz_record("avatar/info", player_id, lang=lang, payload=payload)
|
||||
return ZZZCalculatorCharacterDetails(**data)
|
||||
|
||||
async def get_zzz_buddy_list(
|
||||
self,
|
||||
player_id: Optional[int] = None,
|
||||
lang: Optional[str] = None,
|
||||
) -> "ZZZBuddyBasic":
|
||||
"""Get ZZZ buddy basic information.
|
||||
|
||||
Args:
|
||||
player_id (Optional[int], optional): The player ID. Defaults to None.
|
||||
lang (Optional[str], optional): The language of the data. Defaults to None.
|
||||
|
||||
Returns:
|
||||
ZZZBuddyBasic: The requested buddy information.
|
||||
|
||||
Raises:
|
||||
BadRequest: If the request is invalid.
|
||||
DataNotPublic: If the requested data is not public.
|
||||
"""
|
||||
data = await self._request_zzz_record("buddy/info", player_id, lang=lang)
|
||||
return ZZZBuddyBasic(**data)
|
||||
|
||||
async def get_record_card(
|
||||
self,
|
||||
account_id: Optional[int] = None,
|
||||
*,
|
||||
lang: Optional[str] = None,
|
||||
) -> Optional[RecordCard]:
|
||||
"""Get a zzz player record cards.
|
||||
|
||||
Args:
|
||||
account_id: Optional[int], the user's account ID, defaults to None
|
||||
lang: Optional[str], the language version of the request, defaults to None
|
||||
|
||||
Returns:
|
||||
Starrail user record cards.
|
||||
|
||||
Returns:
|
||||
Optional[RecordCard]: RecordCard objects.
|
||||
"""
|
||||
account_id = account_id or self.account_id
|
||||
|
||||
record_cards = await self.get_record_cards(account_id, lang=lang)
|
||||
|
||||
for record_card in record_cards:
|
||||
if record_card.game == Game.ZZZ:
|
||||
return record_card
|
||||
|
||||
return None
|
@ -10,7 +10,7 @@ from simnet.client.routes import REWARD_URL
|
||||
from simnet.errors import GeetestTriggered
|
||||
from simnet.models.lab.daily import DailyRewardInfo, DailyReward, ClaimedDailyReward
|
||||
from simnet.utils.enums import Game, Region
|
||||
from simnet.utils.player import recognize_genshin_server, recognize_starrail_server
|
||||
from simnet.utils.player import recognize_genshin_server, recognize_starrail_server, recognize_zzz_server
|
||||
|
||||
__all__ = ("DailyRewardClient",)
|
||||
|
||||
@ -82,6 +82,14 @@ class DailyRewardClient(BaseClient):
|
||||
)
|
||||
params = params.set("uid", self.player_id)
|
||||
params = params.set("region", recognize_starrail_server(self.player_id))
|
||||
if self.game == Game.ZZZ:
|
||||
headers["referer"] = (
|
||||
"https://act.mihoyo.com/bbs/event/signin/zzz/e202406242138391.html?"
|
||||
"act_id=e202406242138391&mhy_auth_required=true&mhy_presentation_style=fullscreen&"
|
||||
"utm_source=bbs&utm_medium=zzz&utm_campaign=icon"
|
||||
)
|
||||
params = params.set("uid", self.player_id)
|
||||
params = params.set("region", recognize_zzz_server(self.player_id))
|
||||
|
||||
url = base_url / endpoint
|
||||
|
||||
|
@ -261,3 +261,12 @@ class LabClient(BaseClient):
|
||||
"""
|
||||
accounts = await self.get_game_accounts(lang=lang)
|
||||
return [account for account in accounts if account.game == Game.STARRAIL]
|
||||
|
||||
async def get_zzz_accounts(self, *, lang: Optional[str] = None) -> List[Account]:
|
||||
"""Get the zzz accounts of the currently logged-in user.
|
||||
|
||||
Returns:
|
||||
List[Account]: A list of account info objects of zzz accounts.
|
||||
"""
|
||||
accounts = await self.get_game_accounts(lang=lang)
|
||||
return [account for account in accounts if account.game == Game.ZZZ]
|
||||
|
@ -47,6 +47,9 @@ class BaseWishClient(BaseClient):
|
||||
params["authkey"] = unquote(authkey)
|
||||
params["lang"] = create_short_lang_code(lang or self.lang)
|
||||
|
||||
if game == Game.ZZZ:
|
||||
params["real_gacha_type"] = params.get("gacha_type", "")
|
||||
|
||||
return await self.request_api("GET", url, params=params)
|
||||
|
||||
async def wish_history(
|
||||
|
54
simnet/client/components/wish/zzz.py
Normal file
54
simnet/client/components/wish/zzz.py
Normal file
@ -0,0 +1,54 @@
|
||||
from functools import partial
|
||||
from typing import Optional, List
|
||||
|
||||
from simnet.client.components.wish.base import BaseWishClient
|
||||
from simnet.models.zzz.wish import ZZZWish
|
||||
from simnet.utils.enums import Game
|
||||
from simnet.utils.paginator import WishPaginator
|
||||
|
||||
__all__ = ("ZZZWishClient",)
|
||||
|
||||
|
||||
class ZZZWishClient(BaseWishClient):
|
||||
"""The ZZZWishClient class for making requests towards the Wish API."""
|
||||
|
||||
async def wish_history(
|
||||
self,
|
||||
banner_types: Optional[List[int]] = None,
|
||||
limit: Optional[int] = None,
|
||||
lang: Optional[str] = None,
|
||||
authkey: Optional[str] = None,
|
||||
end_id: int = 0,
|
||||
) -> List[ZZZWish]:
|
||||
"""
|
||||
Get the wish history for a list of banner types.
|
||||
|
||||
Args:
|
||||
banner_types (Optional[List[int]], optional): The banner types to get the wish history for.
|
||||
limit (Optional[int] , optional): The maximum number of wishes to retrieve.
|
||||
If not provided, all available wishes will be returned.
|
||||
lang (Optional[str], optional): The language code to use for the request.
|
||||
If not provided, the class default will be used.
|
||||
authkey (Optional[str], optional): The authorization key for making the request.
|
||||
end_id (int, optional): The ending ID of the last wish to retrieve.
|
||||
|
||||
Returns:
|
||||
List[ZZZWish]: A list of ZZZWish objects representing the retrieved wishes.
|
||||
"""
|
||||
banner_types = banner_types or [1, 2, 3, 5]
|
||||
if isinstance(banner_types, int):
|
||||
banner_types = [banner_types]
|
||||
wishes = []
|
||||
for banner_type in banner_types:
|
||||
paginator = WishPaginator(
|
||||
end_id,
|
||||
partial(
|
||||
self.get_wish_page,
|
||||
banner_type=banner_type,
|
||||
game=Game.ZZZ,
|
||||
authkey=authkey,
|
||||
),
|
||||
)
|
||||
items = await paginator.get(limit)
|
||||
wishes.extend([ZZZWish(**i) for i in items])
|
||||
return sorted(wishes, key=lambda wish: wish.time.timestamp())
|
@ -220,9 +220,17 @@ class GameRoute(BaseRoute):
|
||||
return self.urls[region][game]
|
||||
|
||||
|
||||
RECORD_URL = InternationalRoute(
|
||||
overseas="https://bbs-api-os.hoyolab.com/game_record",
|
||||
chinese="https://api-takumi-record.mihoyo.com/game_record/app",
|
||||
RECORD_URL = GameRoute(
|
||||
overseas=dict(
|
||||
genshin="https://bbs-api-os.hoyolab.com/game_record",
|
||||
hkrpg="https://bbs-api-os.hoyolab.com/game_record",
|
||||
nap="https://sg-act-nap-api.hoyolab.com/event/game_record_zzz",
|
||||
),
|
||||
chinese=dict(
|
||||
genshin="https://api-takumi-record.mihoyo.com/game_record/app",
|
||||
hkrpg="https://api-takumi-record.mihoyo.com/game_record/app",
|
||||
nap="https://api-takumi-record.mihoyo.com/event/game_record_zzz",
|
||||
),
|
||||
)
|
||||
|
||||
GACHA_INFO_URL = GameRoute(
|
||||
@ -233,6 +241,7 @@ GACHA_INFO_URL = GameRoute(
|
||||
chinese=dict(
|
||||
genshin="https://public-operation-hk4e.mihoyo.com/gacha_info/api",
|
||||
hkrpg="https://api-takumi.mihoyo.com/common/gacha_record/api",
|
||||
nap="https://public-operation-nap.mihoyo.com/common/gacha_record/api",
|
||||
),
|
||||
)
|
||||
|
||||
@ -264,11 +273,13 @@ REWARD_URL = GameRoute(
|
||||
genshin="https://sg-hk4e-api.hoyolab.com/event/sol/?act_id=e202102251931481",
|
||||
honkai3rd="https://sg-public-api.hoyolab.com/event/mani/?act_id=e202110291205111",
|
||||
hkrpg="https://sg-public-api.hoyolab.com/event/luna/os/?act_id=e202303301540311",
|
||||
nap="https://sg-act-nap-api.hoyolab.com/event/luna/zzz/os/?act_id=e202406031448091",
|
||||
),
|
||||
chinese=dict(
|
||||
genshin="https://api-takumi.mihoyo.com/event/luna/?act_id=e202311201442471",
|
||||
honkai3rd="https://api-takumi.mihoyo.com/event/luna/?act_id=e202207181446311",
|
||||
hkrpg="https://api-takumi.mihoyo.com/event/luna/?act_id=e202304121516551",
|
||||
nap="https://act-nap-api.mihoyo.com/event/luna/zzz/?act_id=e202406242138391",
|
||||
),
|
||||
)
|
||||
TAKUMI_URL = InternationalRoute(
|
||||
@ -321,6 +332,7 @@ CODE_HOYOLAB_URL = GameRoute(
|
||||
overseas=dict(
|
||||
genshin="https://sg-hk4e-api.hoyolab.com/common/apicdkey/api/webExchangeCdkeyHyl",
|
||||
hkrpg="https://sg-hkrpg-api.hoyolab.com/common/apicdkey/api/webExchangeCdkeyHyl",
|
||||
nap="https://public-operation-nap.hoyoverse.com/common/apicdkey/api/webExchangeCdkey",
|
||||
),
|
||||
chinese={},
|
||||
)
|
||||
|
24
simnet/client/zzz.py
Normal file
24
simnet/client/zzz.py
Normal file
@ -0,0 +1,24 @@
|
||||
from typing import Optional
|
||||
|
||||
from simnet.client.components.auth import AuthClient
|
||||
from simnet.client.components.chronicle.zzz import ZZZBattleChronicleClient
|
||||
from simnet.client.components.daily import DailyRewardClient
|
||||
from simnet.client.components.lab import LabClient
|
||||
from simnet.client.components.verify import VerifyClient
|
||||
from simnet.client.components.wish.zzz import ZZZWishClient
|
||||
from simnet.utils.enums import Game
|
||||
|
||||
__all__ = ("ZZZClient",)
|
||||
|
||||
|
||||
class ZZZClient(
|
||||
ZZZBattleChronicleClient,
|
||||
ZZZWishClient,
|
||||
DailyRewardClient,
|
||||
AuthClient,
|
||||
LabClient,
|
||||
VerifyClient,
|
||||
):
|
||||
"""A simple http client for StarRail endpoints."""
|
||||
|
||||
game: Optional[Game] = Game.ZZZ
|
@ -58,6 +58,8 @@ class Account(APIModel):
|
||||
return Game.HONKAI
|
||||
if "hkrpg" in self.game_biz:
|
||||
return Game.STARRAIL
|
||||
if "nap" in self.game_biz:
|
||||
return Game.ZZZ
|
||||
try:
|
||||
return Game(self.game_biz)
|
||||
except ValueError:
|
||||
@ -420,6 +422,57 @@ class StarRailRecodeCard(RecordCard):
|
||||
return int(self.data[3].value)
|
||||
|
||||
|
||||
class ZZZRecodeCard(RecordCard):
|
||||
"""ZZZ record card."""
|
||||
|
||||
@property
|
||||
def game(self) -> Game:
|
||||
"""Returns the game associated with the record card.
|
||||
|
||||
Returns:
|
||||
Game: The game associated with the record card.
|
||||
"""
|
||||
return Game.ZZZ
|
||||
|
||||
@property
|
||||
def days_active(self) -> int:
|
||||
"""Returns the number of days the user has been active.
|
||||
|
||||
Returns:
|
||||
int: The number of days the user has been active.
|
||||
"""
|
||||
return int(self.data[0].value)
|
||||
|
||||
@property
|
||||
def world_level_name(self) -> str:
|
||||
"""
|
||||
Returns the user's world level name.
|
||||
|
||||
Returns:
|
||||
str: The user's world level name.
|
||||
"""
|
||||
return self.data[1].value
|
||||
|
||||
@property
|
||||
def characters(self) -> int:
|
||||
"""Returns the number of characters the user has.
|
||||
|
||||
Returns:
|
||||
int: The number of characters the user has.
|
||||
"""
|
||||
return int(self.data[2].value)
|
||||
|
||||
@property
|
||||
def buddies(self) -> int:
|
||||
"""Returns the number of buddies the user has found.
|
||||
|
||||
Returns:
|
||||
int: The number of buddies the user has found.
|
||||
"""
|
||||
return int(self.data[3].value)
|
||||
|
||||
|
||||
RECORD_CARD_MAP.setdefault(1, HonkaiRecordCard)
|
||||
RECORD_CARD_MAP.setdefault(2, GenshinRecordCard)
|
||||
RECORD_CARD_MAP.setdefault(6, StarRailRecodeCard)
|
||||
RECORD_CARD_MAP.setdefault(8, ZZZRecodeCard)
|
||||
|
0
simnet/models/zzz/__init__.py
Normal file
0
simnet/models/zzz/__init__.py
Normal file
69
simnet/models/zzz/calculator.py
Normal file
69
simnet/models/zzz/calculator.py
Normal file
@ -0,0 +1,69 @@
|
||||
from typing import List
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from simnet.models.base import APIModel
|
||||
from simnet.models.zzz.character import ZZZPartialCharacter
|
||||
|
||||
|
||||
class ZZZCalculatorWeaponProperty(APIModel):
|
||||
|
||||
property_name: str
|
||||
property_id: int
|
||||
base: str
|
||||
|
||||
|
||||
class ZZZCalculatorWeapon(APIModel):
|
||||
|
||||
id: int
|
||||
level: int
|
||||
name: str
|
||||
star: int
|
||||
icon: str
|
||||
rarity: str
|
||||
properties: List[ZZZCalculatorWeaponProperty]
|
||||
main_properties: List[ZZZCalculatorWeaponProperty]
|
||||
talent_title: str
|
||||
talent_content: str
|
||||
profession: int
|
||||
|
||||
|
||||
class ZZZCalculatorAvatarProperty(ZZZCalculatorWeaponProperty):
|
||||
|
||||
add: str
|
||||
final: str
|
||||
|
||||
|
||||
class ZZZCalculatorAvatarSkillItem(APIModel):
|
||||
title: str
|
||||
text: str
|
||||
|
||||
|
||||
class ZZZCalculatorAvatarSkill(APIModel):
|
||||
|
||||
level: int
|
||||
skill_type: int
|
||||
items: List[ZZZCalculatorAvatarSkillItem]
|
||||
|
||||
|
||||
class ZZZCalculatorAvatarRank(APIModel):
|
||||
|
||||
id: int
|
||||
name: str
|
||||
desc: str
|
||||
pos: int
|
||||
is_unlocked: bool
|
||||
|
||||
|
||||
class ZZZCalculatorCharacter(ZZZPartialCharacter):
|
||||
|
||||
equip: List
|
||||
weapon: ZZZCalculatorWeapon
|
||||
properties: List[ZZZCalculatorAvatarProperty]
|
||||
skills: List[ZZZCalculatorAvatarSkill]
|
||||
ranks: List[ZZZCalculatorAvatarRank]
|
||||
|
||||
|
||||
class ZZZCalculatorCharacterDetails(APIModel):
|
||||
|
||||
characters: List[ZZZCalculatorCharacter] = Field(alias="avatar_list")
|
36
simnet/models/zzz/character.py
Normal file
36
simnet/models/zzz/character.py
Normal file
@ -0,0 +1,36 @@
|
||||
"""Starrail base character model."""
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from simnet.models.base import APIModel
|
||||
|
||||
|
||||
class ZZZBaseCharacter(APIModel):
|
||||
"""Base character model."""
|
||||
|
||||
id: int
|
||||
element_type: int
|
||||
rarity: str
|
||||
group_icon_path: str
|
||||
hollow_icon_path: str
|
||||
|
||||
|
||||
class ZZZPartialCharacter(ZZZBaseCharacter):
|
||||
"""Character without any equipment."""
|
||||
|
||||
name: str = Field(alias="name_mi18n")
|
||||
full_name: str = Field(alias="full_name_mi18n")
|
||||
camp_name: str = Field(alias="camp_name_mi18n")
|
||||
avatar_profession: int
|
||||
level: int
|
||||
rank: int
|
||||
|
||||
|
||||
class ZZZBaseBuddy(APIModel):
|
||||
"""Base Buddy model."""
|
||||
|
||||
id: int
|
||||
name: str
|
||||
rarity: str
|
||||
level: int
|
||||
star: int
|
0
simnet/models/zzz/chronicle/__init__.py
Normal file
0
simnet/models/zzz/chronicle/__init__.py
Normal file
110
simnet/models/zzz/chronicle/notes.py
Normal file
110
simnet/models/zzz/chronicle/notes.py
Normal file
@ -0,0 +1,110 @@
|
||||
import datetime
|
||||
import enum
|
||||
|
||||
from simnet.models.base import APIModel
|
||||
|
||||
|
||||
class ZZZNoteProgress(APIModel):
|
||||
"""
|
||||
Represents the progress of the user.
|
||||
|
||||
Attributes:
|
||||
max (int): The maximum progress of the user.
|
||||
current (int): The current progress of the user.
|
||||
"""
|
||||
|
||||
max: int
|
||||
current: int
|
||||
|
||||
|
||||
class ZZZNoteEnergy(APIModel):
|
||||
"""
|
||||
Represents the energy of the user.
|
||||
|
||||
Attributes:
|
||||
progress (ZZZNoteProgress): The progress of the progress
|
||||
restore (int): The restore of the progress
|
||||
"""
|
||||
|
||||
progress: ZZZNoteProgress
|
||||
restore: datetime.timedelta
|
||||
|
||||
|
||||
class ZZZNoteVitality(APIModel):
|
||||
"""
|
||||
Represents the vitality of the user.
|
||||
|
||||
Attributes:
|
||||
max (int): The maximum vitality of the user.
|
||||
current (int): The current vitality of the user.
|
||||
"""
|
||||
|
||||
max: int
|
||||
current: int
|
||||
|
||||
|
||||
class ZZZNoteVhsSaleState(str, enum.Enum):
|
||||
"""
|
||||
Represents the state of the vhs sale of the user.
|
||||
"""
|
||||
|
||||
FREE = "SaleStateNo"
|
||||
DOING = "SaleStateDoing"
|
||||
DONE = "SaleStateDone"
|
||||
|
||||
|
||||
class ZZZNoteVhsSale(APIModel):
|
||||
"""
|
||||
Represents the vhs sale of the user.
|
||||
|
||||
Attributes:
|
||||
sale_state (ZZZNoteVhsSaleState): The state of the vhs sale of the user.
|
||||
"""
|
||||
|
||||
sale_state: ZZZNoteVhsSaleState
|
||||
|
||||
|
||||
class ZZZNoteCardSignState(str, enum.Enum):
|
||||
"""
|
||||
Represents the state of the card sign of the user.
|
||||
"""
|
||||
|
||||
FREE = "CardSignNo"
|
||||
DONE = "CardSignDone"
|
||||
|
||||
|
||||
class ZZZNote(APIModel):
|
||||
"""Represents a ZZZ Note.
|
||||
|
||||
Attributes:
|
||||
energy (ZZZNoteEnergy): The energy of the user.
|
||||
vitality (ZZZNoteVitality): The vitality of the user.
|
||||
vhs_sale (ZZZNoteVhsSale): The vhs sale of the user.
|
||||
card_sign (ZZZNoteCardSignState): The card sign of the user.
|
||||
"""
|
||||
|
||||
energy: ZZZNoteEnergy
|
||||
vitality: ZZZNoteVitality
|
||||
vhs_sale: ZZZNoteVhsSale
|
||||
card_sign: ZZZNoteCardSignState
|
||||
|
||||
@property
|
||||
def current_stamina(self) -> int:
|
||||
return self.energy.progress.current
|
||||
|
||||
@property
|
||||
def max_stamina(self) -> int:
|
||||
return self.energy.progress.max
|
||||
|
||||
@property
|
||||
def stamina_recover_time(self) -> datetime:
|
||||
"""A property that returns the time when resin will be fully recovered."""
|
||||
return datetime.datetime.now().astimezone() + self.energy.restore
|
||||
|
||||
@property
|
||||
def current_train_score(self) -> int:
|
||||
return self.vitality.current
|
||||
|
||||
@property
|
||||
def max_train_score(self) -> int:
|
||||
return self.vitality.max
|
38
simnet/models/zzz/chronicle/stats.py
Normal file
38
simnet/models/zzz/chronicle/stats.py
Normal file
@ -0,0 +1,38 @@
|
||||
"""Starrail chronicle stats."""
|
||||
|
||||
import typing
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from simnet.models.base import APIModel
|
||||
from simnet.models.zzz import character
|
||||
|
||||
|
||||
class ZZZStats(APIModel):
|
||||
"""Overall user stats."""
|
||||
|
||||
active_days: int
|
||||
avatar_num: int
|
||||
world_level_name: str
|
||||
cur_period_zone_layer_count: int
|
||||
buddy_num: int
|
||||
|
||||
|
||||
class ZZZAvatarBasic(APIModel):
|
||||
"""Basic avatar"""
|
||||
|
||||
characters: typing.Sequence[character.ZZZPartialCharacter] = Field(alias="avatar_list")
|
||||
|
||||
|
||||
class ZZZBuddyBasic(APIModel):
|
||||
"""Basic buddy"""
|
||||
|
||||
buddy_list: typing.Sequence[character.ZZZBaseBuddy] = Field(alias="list")
|
||||
|
||||
|
||||
class ZZZUserStats(ZZZAvatarBasic):
|
||||
"""User stats with characters without equipment."""
|
||||
|
||||
stats: ZZZStats
|
||||
cur_head_icon_url: str
|
||||
buddy_list: typing.Sequence[character.ZZZBaseBuddy]
|
64
simnet/models/zzz/wish.py
Normal file
64
simnet/models/zzz/wish.py
Normal file
@ -0,0 +1,64 @@
|
||||
from datetime import datetime
|
||||
from enum import IntEnum
|
||||
from typing import Any
|
||||
|
||||
from pydantic import Field, validator
|
||||
|
||||
from simnet.models.base import APIModel
|
||||
|
||||
|
||||
class ZZZBannerType(IntEnum):
|
||||
"""Banner types in wish histories."""
|
||||
|
||||
STANDARD = PERMANENT = NOVICE = 1
|
||||
"""Permanent standard banner."""
|
||||
|
||||
CHARACTER = 2
|
||||
"""Rotating character banner."""
|
||||
|
||||
WEAPON = 3
|
||||
"""Rotating weapon banner."""
|
||||
|
||||
BANGBOO = 5
|
||||
"""BangBoo banner."""
|
||||
|
||||
|
||||
class ZZZWish(APIModel):
|
||||
"""Wish made on any banner."""
|
||||
|
||||
uid: int
|
||||
"""User ID of the wish maker."""
|
||||
|
||||
id: int
|
||||
"""ID of the wished item."""
|
||||
|
||||
type: str = Field(alias="item_type")
|
||||
"""Type of the wished item."""
|
||||
|
||||
item_id: int = Field(alias="item_id")
|
||||
"""ID of the wished item."""
|
||||
|
||||
name: str
|
||||
"""Name of the wished item."""
|
||||
|
||||
rarity: int = Field(alias="rank_type")
|
||||
"""Rarity of the wished item."""
|
||||
|
||||
time: datetime
|
||||
"""Time when the wish was made."""
|
||||
|
||||
banner_id: int = Field(alias="gacha_id")
|
||||
"""ID of the banner the wish was made on."""
|
||||
|
||||
banner_type: ZZZBannerType = Field(alias="gacha_type")
|
||||
"""Type of the banner the wish was made on."""
|
||||
|
||||
@validator("banner_type", pre=True)
|
||||
def cast_banner_type(cls, v: Any) -> int:
|
||||
"""Converts the banner type from any type to int."""
|
||||
return int(v)
|
||||
|
||||
@validator("rarity")
|
||||
def add_rarity(cls, v: int) -> int:
|
||||
"""Add rarity 1."""
|
||||
return v + 1
|
@ -24,8 +24,10 @@ class Game(str, _enum.Enum):
|
||||
GENSHIN (Game): Represents the game "Genshin Impact".
|
||||
HONKAI (Game): Represents the game "Honkai Impact 3rd".
|
||||
STARRAIL (Game): Represents the game "Honkai Impact 3rd RPG".
|
||||
ZZZ (Game): Represents the game "Zenless Zone Zero".
|
||||
"""
|
||||
|
||||
GENSHIN = "genshin"
|
||||
HONKAI = "honkai3rd"
|
||||
STARRAIL = "hkrpg"
|
||||
ZZZ = "nap"
|
||||
|
@ -50,7 +50,7 @@ class WishPaginator:
|
||||
if limit and len(all_items) >= limit:
|
||||
break
|
||||
|
||||
await asyncio.sleep(0.5)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# Return up to the specified limit.
|
||||
return all_items[: min(len(all_items), limit)] if limit else all_items
|
||||
|
@ -8,6 +8,7 @@ UID_LENGTH: Mapping[Game, int] = {
|
||||
Game.GENSHIN: 9,
|
||||
Game.STARRAIL: 9,
|
||||
Game.HONKAI: 8,
|
||||
Game.ZZZ: 9,
|
||||
}
|
||||
UID_RANGE: Mapping[Game, Mapping[Region, Sequence[int]]] = {
|
||||
Game.GENSHIN: {
|
||||
@ -22,6 +23,9 @@ UID_RANGE: Mapping[Game, Mapping[Region, Sequence[int]]] = {
|
||||
Region.OVERSEAS: (1, 2),
|
||||
Region.CHINESE: (3, 4),
|
||||
},
|
||||
Game.ZZZ: {
|
||||
Region.OVERSEAS: (13,),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -104,6 +108,33 @@ def recognize_starrail_server(player_id: int) -> str:
|
||||
raise ValueError(f"player id {player_id} isn't associated with any server")
|
||||
|
||||
|
||||
def recognize_zzz_server(player_id: int) -> str:
|
||||
"""Recognize which server a ZZZ UID is from.
|
||||
|
||||
Args:
|
||||
player_id (int): The player ID to recognize the server for.
|
||||
|
||||
Returns:
|
||||
str: The name of the server associated with the given player ID.
|
||||
|
||||
Raises:
|
||||
ValueError: If the player ID is not associated with any server.
|
||||
"""
|
||||
if len(str(player_id)) == 8:
|
||||
return "prod_gf_cn"
|
||||
server = {
|
||||
10: "prod_gf_us",
|
||||
15: "prod_gf_eu",
|
||||
13: "prod_gf_jp",
|
||||
17: "prod_gf_sg",
|
||||
}.get(recognize_game_uid_first_digit(player_id, Game.ZZZ))
|
||||
|
||||
if server:
|
||||
return server
|
||||
|
||||
raise ValueError(f"player id {player_id} isn't associated with any server")
|
||||
|
||||
|
||||
def recognize_region(player_id: int, game: Game) -> Optional[Region]:
|
||||
"""
|
||||
Recognizes the region of a player ID for a given game.
|
||||
@ -115,6 +146,9 @@ def recognize_region(player_id: int, game: Game) -> Optional[Region]:
|
||||
Returns:
|
||||
Optional[Region]: The region the player ID belongs to if it can be recognized, None otherwise.
|
||||
"""
|
||||
if game == Game.ZZZ and len(str(player_id)) == 8:
|
||||
return Region.CHINESE
|
||||
|
||||
for region, digits in UID_RANGE[game].items():
|
||||
first = recognize_game_uid_first_digit(player_id, game)
|
||||
if first in digits:
|
||||
@ -141,6 +175,8 @@ def recognize_server(player_id: int, game: Game) -> str:
|
||||
return recognize_genshin_server(player_id)
|
||||
if game == Game.STARRAIL:
|
||||
return recognize_starrail_server(player_id)
|
||||
if game == Game.ZZZ:
|
||||
return recognize_zzz_server(player_id)
|
||||
raise ValueError(f"{game} is not a valid game")
|
||||
|
||||
|
||||
@ -162,6 +198,15 @@ def recognize_starrail_game_biz(game_uid: int) -> str:
|
||||
return "hkrpg_cn" if game_uid < 600000000 else "hkrpg_global"
|
||||
|
||||
|
||||
def recognize_zzz_game_biz(game_uid: int) -> str:
|
||||
"""Recognizes the game biz of a player ID for a game biz.
|
||||
|
||||
Returns:
|
||||
str: The game biz the player ID belongs to.
|
||||
"""
|
||||
return "nap_cn" if len(str(game_uid)) == 8 else "nap_global"
|
||||
|
||||
|
||||
def recognize_game_biz(player_id: int, game: Game) -> str:
|
||||
"""
|
||||
Recognizes the game biz of a player ID for a given game.
|
||||
@ -180,4 +225,6 @@ def recognize_game_biz(player_id: int, game: Game) -> str:
|
||||
return recognize_genshin_game_biz(player_id)
|
||||
if game == Game.STARRAIL:
|
||||
return recognize_starrail_game_biz(player_id)
|
||||
if game == Game.ZZZ:
|
||||
return recognize_zzz_game_biz(player_id)
|
||||
raise ValueError(f"{game} is not a valid game")
|
||||
|
Loading…
Reference in New Issue
Block a user