Support ZZZClient

This commit is contained in:
omg-xtao 2024-07-05 21:58:00 +08:00 committed by GitHub
parent 05fcb568d6
commit 074939d881
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 764 additions and 11 deletions

View File

@ -8,7 +8,7 @@ readme = "README.md"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.8" python = "^3.8"
httpx = "^0.25.0" httpx = ">=0.25.0"
pydantic = "<2.0.0,>=1.10.7" pydantic = "<2.0.0,>=1.10.7"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]

View File

@ -1,5 +1,6 @@
from simnet.client.genshin import GenshinClient from simnet.client.genshin import GenshinClient
from simnet.client.starrail import StarRailClient from simnet.client.starrail import StarRailClient
from simnet.client.zzz import ZZZClient
from simnet.utils.enums import Game, Region from simnet.utils.enums import Game, Region
__all__ = ("StarRailClient", "GenshinClient", "Game", "Region") __all__ = ("StarRailClient", "GenshinClient", "ZZZClient", "Game", "Region")

View File

@ -217,6 +217,8 @@ class BaseClient(AsyncContextManager["BaseClient"]):
headers["x-rpc-device_id"] = self.get_device_id() headers["x-rpc-device_id"] = self.get_device_id()
headers["x-rpc-device_fp"] = self.get_device_fp() headers["x-rpc-device_fp"] = self.get_device_fp()
if self.region == Region.OVERSEAS: 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 headers["x-rpc-language"] = self.lang or lang
if ds is None: if ds is None:
app_version, client_type, ds = generate_dynamic_secret(self.region, ds_type, new_ds, data, params) app_version, client_type, ds = generate_dynamic_secret(self.region, ds_type, new_ds, data, params)

View File

@ -50,10 +50,13 @@ class BaseChronicleClient(BaseClient):
TimedOut: If the request times out. TimedOut: If the request times out.
BadRequest: If the response contains an error. 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: 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 url = base_url / endpoint
new_ds = self.region == Region.CHINESE new_ds = self.region == Region.CHINESE

View File

@ -312,7 +312,7 @@ class GenshinBattleChronicleClient(BaseChronicleClient):
if self.account_id is not None and stuid is None: if self.account_id is not None and stuid is None:
self.cookies.set("stuid", str(self.account_id)) self.cookies.set("stuid", str(self.account_id))
if self.region == Region.OVERSEAS: 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"} params = {"game_id": "2"}
data = await self.request_lab(route, params=params, lang=lang) data = await self.request_lab(route, params=params, lang=lang)
model = NotesOverseaWidget model = NotesOverseaWidget

View File

@ -412,7 +412,7 @@ class StarRailBattleChronicleClient(BaseChronicleClient):
if self.account_id is not None and stuid is None: if self.account_id is not None and stuid is None:
self.cookies.set("stuid", str(self.account_id)) self.cookies.set("stuid", str(self.account_id))
if self.region == Region.OVERSEAS: 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) data = await self.request_lab(route, lang=lang)
model = StarRailNoteOverseaWidget model = StarRailNoteOverseaWidget
else: else:

View 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

View File

@ -10,7 +10,7 @@ from simnet.client.routes import REWARD_URL
from simnet.errors import GeetestTriggered from simnet.errors import GeetestTriggered
from simnet.models.lab.daily import DailyRewardInfo, DailyReward, ClaimedDailyReward from simnet.models.lab.daily import DailyRewardInfo, DailyReward, ClaimedDailyReward
from simnet.utils.enums import Game, Region 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",) __all__ = ("DailyRewardClient",)
@ -82,6 +82,14 @@ class DailyRewardClient(BaseClient):
) )
params = params.set("uid", self.player_id) params = params.set("uid", self.player_id)
params = params.set("region", recognize_starrail_server(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 url = base_url / endpoint

View File

@ -261,3 +261,12 @@ class LabClient(BaseClient):
""" """
accounts = await self.get_game_accounts(lang=lang) accounts = await self.get_game_accounts(lang=lang)
return [account for account in accounts if account.game == Game.STARRAIL] 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]

View File

@ -47,6 +47,9 @@ class BaseWishClient(BaseClient):
params["authkey"] = unquote(authkey) params["authkey"] = unquote(authkey)
params["lang"] = create_short_lang_code(lang or self.lang) 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) return await self.request_api("GET", url, params=params)
async def wish_history( async def wish_history(

View 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())

View File

@ -220,9 +220,17 @@ class GameRoute(BaseRoute):
return self.urls[region][game] return self.urls[region][game]
RECORD_URL = InternationalRoute( RECORD_URL = GameRoute(
overseas="https://bbs-api-os.hoyolab.com/game_record", overseas=dict(
chinese="https://api-takumi-record.mihoyo.com/game_record/app", 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( GACHA_INFO_URL = GameRoute(
@ -233,6 +241,7 @@ GACHA_INFO_URL = GameRoute(
chinese=dict( chinese=dict(
genshin="https://public-operation-hk4e.mihoyo.com/gacha_info/api", genshin="https://public-operation-hk4e.mihoyo.com/gacha_info/api",
hkrpg="https://api-takumi.mihoyo.com/common/gacha_record/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", genshin="https://sg-hk4e-api.hoyolab.com/event/sol/?act_id=e202102251931481",
honkai3rd="https://sg-public-api.hoyolab.com/event/mani/?act_id=e202110291205111", 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", 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( chinese=dict(
genshin="https://api-takumi.mihoyo.com/event/luna/?act_id=e202311201442471", genshin="https://api-takumi.mihoyo.com/event/luna/?act_id=e202311201442471",
honkai3rd="https://api-takumi.mihoyo.com/event/luna/?act_id=e202207181446311", honkai3rd="https://api-takumi.mihoyo.com/event/luna/?act_id=e202207181446311",
hkrpg="https://api-takumi.mihoyo.com/event/luna/?act_id=e202304121516551", 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( TAKUMI_URL = InternationalRoute(
@ -321,6 +332,7 @@ CODE_HOYOLAB_URL = GameRoute(
overseas=dict( overseas=dict(
genshin="https://sg-hk4e-api.hoyolab.com/common/apicdkey/api/webExchangeCdkeyHyl", genshin="https://sg-hk4e-api.hoyolab.com/common/apicdkey/api/webExchangeCdkeyHyl",
hkrpg="https://sg-hkrpg-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={}, chinese={},
) )

24
simnet/client/zzz.py Normal file
View 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

View File

@ -58,6 +58,8 @@ class Account(APIModel):
return Game.HONKAI return Game.HONKAI
if "hkrpg" in self.game_biz: if "hkrpg" in self.game_biz:
return Game.STARRAIL return Game.STARRAIL
if "nap" in self.game_biz:
return Game.ZZZ
try: try:
return Game(self.game_biz) return Game(self.game_biz)
except ValueError: except ValueError:
@ -420,6 +422,57 @@ class StarRailRecodeCard(RecordCard):
return int(self.data[3].value) 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(1, HonkaiRecordCard)
RECORD_CARD_MAP.setdefault(2, GenshinRecordCard) RECORD_CARD_MAP.setdefault(2, GenshinRecordCard)
RECORD_CARD_MAP.setdefault(6, StarRailRecodeCard) RECORD_CARD_MAP.setdefault(6, StarRailRecodeCard)
RECORD_CARD_MAP.setdefault(8, ZZZRecodeCard)

View File

View 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")

View 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

View File

View 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

View 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
View 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

View File

@ -24,8 +24,10 @@ class Game(str, _enum.Enum):
GENSHIN (Game): Represents the game "Genshin Impact". GENSHIN (Game): Represents the game "Genshin Impact".
HONKAI (Game): Represents the game "Honkai Impact 3rd". HONKAI (Game): Represents the game "Honkai Impact 3rd".
STARRAIL (Game): Represents the game "Honkai Impact 3rd RPG". STARRAIL (Game): Represents the game "Honkai Impact 3rd RPG".
ZZZ (Game): Represents the game "Zenless Zone Zero".
""" """
GENSHIN = "genshin" GENSHIN = "genshin"
HONKAI = "honkai3rd" HONKAI = "honkai3rd"
STARRAIL = "hkrpg" STARRAIL = "hkrpg"
ZZZ = "nap"

View File

@ -50,7 +50,7 @@ class WishPaginator:
if limit and len(all_items) >= limit: if limit and len(all_items) >= limit:
break break
await asyncio.sleep(0.5) await asyncio.sleep(1)
# Return up to the specified limit. # Return up to the specified limit.
return all_items[: min(len(all_items), limit)] if limit else all_items return all_items[: min(len(all_items), limit)] if limit else all_items

View File

@ -8,6 +8,7 @@ UID_LENGTH: Mapping[Game, int] = {
Game.GENSHIN: 9, Game.GENSHIN: 9,
Game.STARRAIL: 9, Game.STARRAIL: 9,
Game.HONKAI: 8, Game.HONKAI: 8,
Game.ZZZ: 9,
} }
UID_RANGE: Mapping[Game, Mapping[Region, Sequence[int]]] = { UID_RANGE: Mapping[Game, Mapping[Region, Sequence[int]]] = {
Game.GENSHIN: { Game.GENSHIN: {
@ -22,6 +23,9 @@ UID_RANGE: Mapping[Game, Mapping[Region, Sequence[int]]] = {
Region.OVERSEAS: (1, 2), Region.OVERSEAS: (1, 2),
Region.CHINESE: (3, 4), 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") 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]: def recognize_region(player_id: int, game: Game) -> Optional[Region]:
""" """
Recognizes the region of a player ID for a given game. 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: Returns:
Optional[Region]: The region the player ID belongs to if it can be recognized, None otherwise. 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(): for region, digits in UID_RANGE[game].items():
first = recognize_game_uid_first_digit(player_id, game) first = recognize_game_uid_first_digit(player_id, game)
if first in digits: if first in digits:
@ -141,6 +175,8 @@ def recognize_server(player_id: int, game: Game) -> str:
return recognize_genshin_server(player_id) return recognize_genshin_server(player_id)
if game == Game.STARRAIL: if game == Game.STARRAIL:
return recognize_starrail_server(player_id) 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") 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" 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: def recognize_game_biz(player_id: int, game: Game) -> str:
""" """
Recognizes the game biz of a player ID for a given game. 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) return recognize_genshin_game_biz(player_id)
if game == Game.STARRAIL: if game == Game.STARRAIL:
return recognize_starrail_game_biz(player_id) 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") raise ValueError(f"{game} is not a valid game")