🐛 Fix issues and improve functionality

- Fix issue with Daily Reward Client not running
- Update Wish Client to support multiple banner retrieval
- Fix issue with setting salt for dynamic secret
- Improve APIModel to support multiple aliases
- Add asyncio.sleep() to Wish paginator to prevent excessive requests

Co-authored-by: xtaodada <xtao@xtaolink.cn>
This commit is contained in:
洛水居室 2023-05-08 08:53:48 +08:00
parent 5d2d3609bb
commit 4250481727
No known key found for this signature in database
GPG Key ID: C9DE87DA724B88FC
15 changed files with 150 additions and 98 deletions

View File

@ -8,7 +8,7 @@ from httpx import AsyncClient, TimeoutException, Response, HTTPError, Timeout
from simnet.client.cookies import Cookies
from simnet.client.headers import Headers
from simnet.errors import TimedOut, NetworkError, BadRequest, raise_for_ret_code
from simnet.utils.ds import generate_dynamic_secret
from simnet.utils.ds import generate_dynamic_secret, DSType
from simnet.utils.enum_ import Region, Game
from simnet.utils.types import (
RT,
@ -187,7 +187,7 @@ class BaseClient(AsyncContextManager["BaseClient"]):
header (HeaderTypes): The header to use.
lang (Optional[str], optional): The language to use for overseas regions. Defaults to None.
ds (str, optional): The DS string to use. Defaults to None.
ds_type (str, optional): The DS type to use. Defaults to None.
ds_type (Optional[DSType], optional): The DS type to use. Defaults to None.
new_ds (bool, optional): Whether to generate a new DS. Defaults to False.
data (Any, optional): The data to use. Defaults to None.
params (Optional[QueryParamTypes], optional): The query parameters to use. Defaults to None.
@ -309,7 +309,7 @@ class BaseClient(AsyncContextManager["BaseClient"]):
headers: Optional[HeaderTypes] = None,
lang: Optional[str] = None,
new_ds: bool = False,
ds_type: str = None,
ds_type: Optional[DSType] = None,
):
"""Make a request to the lab API and return the data.
@ -325,7 +325,7 @@ class BaseClient(AsyncContextManager["BaseClient"]):
headers (Optional[HeaderTypes]): The headers to include in the request.
lang (Optional[str]): The language of the request (e.g., "en", "zh").
new_ds (bool): Whether to use a new dataset for the request.
ds_type (str): The type of dataset to use for the request (e.g., "news", "qa").
ds_type (Optional[DSType]): The type of dataset to use for the request (e.g., "news", "qa").
Returns:
Any: The data returned by the lab API.

View File

@ -218,15 +218,19 @@ class GenshinBattleChronicleClient(BaseChronicleClient):
Returns:
Character: The requested genshin user with all their possible data.
"""
user, abyss1, abyss2, activities = await asyncio.gather(
self.get_genshin_user(player_id, lang=lang),
index, character, abyss1, abyss2, activities = await asyncio.gather(
self._request_genshin_record("index", player_id, lang=lang),
self._request_genshin_record(
"character", player_id, lang=lang, method="POST"
),
self.get_genshin_spiral_abyss(player_id, lang=lang, previous=False),
self.get_genshin_spiral_abyss(player_id, lang=lang, previous=True),
self.get_genshin_activities(player_id, lang=lang),
)
user = {**index, **character}
abyss = SpiralAbyssPair(current=abyss1, previous=abyss2)
return FullGenshinUserStats(**user.dict(), abyss=abyss, activities=activities)
return FullGenshinUserStats(**user, abyss=abyss, activities=activities)
async def get_genshin_activities(
self, player_id: Optional[int] = None, *, lang: Optional[str] = None

View File

@ -2,11 +2,14 @@
import asyncio
from typing import Optional, Dict, Any, List
from httpx import QueryParams
from simnet.client.base import BaseClient
from simnet.utils.ds import hex_digest
from simnet.client.routes import REWARD_URL
from simnet.models.lab.daily import DailyRewardInfo, DailyReward, ClaimedDailyReward
from simnet.utils.ds import hex_digest
from simnet.utils.enum_ import Game, Region
from simnet.utils.player import recognize_genshin_server, recognize_starrail_server
__all__ = ("DailyRewardClient",)
@ -40,19 +43,18 @@ class DailyRewardClient(BaseClient):
Returns:
A dictionary containing the response data.
"""
new_ds: bool = False
headers: Optional[Dict[str, str]] = None
params = QueryParams(params)
base_url = REWARD_URL.get_url(self.region, self.game or game)
params = params.merge(base_url.params)
if self.region == Region.CHINESE:
headers = {}
if challenge is not None and validate is not None:
headers["x-rpc-challenge"] = challenge
headers["x-rpc-validate"] = validate
headers["x-rpc-seccode"] = f"{validate}|jordan"
headers["Referer"] = (
"https://webstatic.mihoyo.com/bbs/event/signin-ys/index.html?"
"bbs_auth_required=true&act_id=e202009291139501&utm_source=bbs&utm_medium=mys&utm_campaign=icon"
)
headers["x-rpc-device_name"] = "Chrome 20 2023"
headers["x-rpc-channel"] = "chrome"
headers["x-rpc-device_model"] = "Chrome 2023"
@ -61,13 +63,31 @@ class DailyRewardClient(BaseClient):
device_id = self.device_id
hash_value = hex_digest(device_id)
headers["x-rpc-device_fp"] = hash_value[:13]
new_ds = endpoint == "sign"
if self.game == Game.GENSHIN:
headers["referer"] = (
"https://webstatic.mihoyo.com/bbs/event/signin-ys/index.html?"
"bbs_auth_required=true&act_id=e202009291139501&utm_source=bbs&utm_medium=mys&utm_campaign=icon"
)
params = params.set("uid", self.player_id)
params = params.set("region", recognize_genshin_server(self.player_id))
if self.game == Game.STARRAIL:
headers["referer"] = (
"https://webstatic.mihoyo.com/bbs/event/signin/hkrpg/index.html?"
"bbs_auth_required=true&act_id=e202304121516551&"
"bbs_auth_required=true&bbs_presentation_style=fullscreen&"
"utm_source=bbs&utm_medium=mys&utm_campaign=icon"
)
params = params.set("uid", self.player_id)
params = params.set("region", recognize_starrail_server(self.player_id))
base_url = REWARD_URL.get_url(self.region, self.game or game)
url = (base_url / endpoint).update_query(**base_url.query)
url = base_url / endpoint
return await self.request_lab(
method, url, params=params, headers=headers, lang=lang, new_ds=new_ds
url,
method,
params=params,
headers=headers,
lang=lang,
)
async def get_reward_info(
@ -105,7 +125,7 @@ class DailyRewardClient(BaseClient):
"""
data = await self.request_daily_reward(
"home",
game=game,
game=game or self.game,
lang=lang,
)
return [DailyReward(**i) for i in data["awards"]]
@ -129,7 +149,7 @@ class DailyRewardClient(BaseClient):
page.
"""
data = await self.request_daily_reward(
"award", params=dict(current_page=page), game=game, lang=lang
"award", params=dict(current_page=page), game=game or self.game, lang=lang
)
return [ClaimedDailyReward(**i) for i in data["list"]]
@ -159,7 +179,7 @@ class DailyRewardClient(BaseClient):
break
fetched_items = await self._get_claimed_rewards_page(
page, game=game, lang=lang
page, game=game or self.game, lang=lang
)
if not fetched_items:
break
@ -206,7 +226,7 @@ class DailyRewardClient(BaseClient):
await self.request_daily_reward(
"sign",
method="POST",
game=game,
game=game or self.game,
lang=lang,
challenge=challenge,
validate=validate,
@ -216,7 +236,7 @@ class DailyRewardClient(BaseClient):
return None
info, rewards = await asyncio.gather(
self.get_reward_info(game=game, lang=lang),
self.get_monthly_rewards(game=game, lang=lang),
self.get_reward_info(game=game or self.game, lang=lang),
self.get_monthly_rewards(game=game or self.game, lang=lang),
)
return rewards[info.claimed_rewards - 1]

View File

@ -102,7 +102,9 @@ class BaseWishClient(BaseClient):
game=game,
lang=lang,
authkey=authkey,
params=dict(gacha_type=banner_type, size=size, end_id=end_id),
params=dict(
gacha_type=banner_type, size=size, end_id=end_id, game_biz=Game.value
),
)
async def get_banner_names(

View File

@ -6,7 +6,6 @@ from simnet.models.genshin.wish import Wish
from simnet.utils.enum_ import Game
from simnet.utils.paginator import WishPaginator
__all__ = ("GenshinWishClient",)
@ -15,17 +14,16 @@ class GenshinWishClient(BaseWishClient):
async def wish_history(
self,
banner_type: int,
banner_types: Optional[List[int]] = None,
limit: Optional[int] = None,
lang: Optional[str] = None,
authkey: Optional[str] = None,
end_id: int = 0,
) -> List[Wish]:
"""
Get the wish history for a list of banner types.
"""Get the wish history for a list of banner types.
Args:
banner_type (int, optional): The banner types to get the wish history for.
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.
@ -36,19 +34,22 @@ class GenshinWishClient(BaseWishClient):
Returns:
List[Wish]: A list of GenshinWish objects representing the retrieved wishes.
"""
banner_types = banner_types or [100, 200, 301, 302]
banner_names = await self.get_banner_names(
game=Game.GENSHIN, lang=lang, authkey=authkey
)
paginator = WishPaginator(
end_id,
partial(
self.get_wish_page,
banner_type=banner_type,
game=Game.GENSHIN,
authkey=authkey,
),
)
items = await paginator.get(limit)
banner_name = banner_names[banner_type]
wish = [Wish(**i, banner_name=banner_name) for i in items]
return wish
wishes = []
for banner_type in banner_types:
paginator = WishPaginator(
end_id,
partial(
self.get_wish_page,
banner_type=banner_type,
game=Game.GENSHIN,
authkey=authkey,
),
)
items = await paginator.get(limit)
banner_name = banner_names[banner_type]
wishes.extend([Wish(**i, banner_name=banner_name) for i in items])
return sorted(wishes, key=lambda wish: wish.time.timestamp())

View File

@ -1,5 +1,6 @@
from functools import partial
from typing import Optional, List
from simnet.client.components.wish.base import BaseWishClient
from simnet.models.starrail.wish import StarRailWish
from simnet.utils.enum_ import Game
@ -13,7 +14,7 @@ class StarRailWishClient(BaseWishClient):
async def wish_history(
self,
banner_type: int,
banner_types: Optional[List[int]] = None,
limit: Optional[int] = None,
lang: Optional[str] = None,
authkey: Optional[str] = None,
@ -23,7 +24,7 @@ class StarRailWishClient(BaseWishClient):
Get the wish history for a list of banner types.
Args:
banner_type (int, optional): The banner types to get the wish history for.
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.
@ -34,15 +35,18 @@ class StarRailWishClient(BaseWishClient):
Returns:
List[StarRailWish]: A list of StarRailWish objects representing the retrieved wishes.
"""
paginator = WishPaginator(
end_id,
partial(
self.get_wish_page,
banner_type=banner_type,
game=Game.STARRAIL,
authkey=authkey,
),
)
items = await paginator.get(limit)
wish = [StarRailWish(**i) for i in items]
return wish
banner_types = banner_types or [1, 2, 11, 12]
wishes = []
for banner_type in banner_types:
paginator = WishPaginator(
end_id,
partial(
self.get_wish_page,
banner_type=banner_type,
game=Game.STARRAIL,
authkey=authkey,
),
)
items = await paginator.get(limit)
wishes.extend([StarRailWish(**i) for i in items])
return sorted(wishes, key=lambda wish: wish.time.timestamp())

View File

@ -2,13 +2,16 @@ from typing import Optional
from simnet.client.components.auth import AuthClient
from simnet.client.components.chronicle.genshin import GenshinBattleChronicleClient
from simnet.client.components.daily import DailyRewardClient
from simnet.client.components.wish.genshin import GenshinWishClient
from simnet.utils.enum_ import Game
__all__ = ("GenshinClient",)
class GenshinClient(GenshinBattleChronicleClient, GenshinWishClient, AuthClient):
class GenshinClient(
GenshinBattleChronicleClient, GenshinWishClient, AuthClient, DailyRewardClient
):
"""A simple http client for StarRail endpoints."""
game: Optional[Game] = Game.GENSHIN

View File

@ -2,11 +2,13 @@ from typing import Optional
from simnet.client.components.auth import AuthClient
from simnet.client.components.chronicle.genshin import GenshinBattleChronicleClient
from simnet.client.components.daily import DailyRewardClient
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, AuthClient):
class GenshinClient(GenshinBattleChronicleClient, GenshinWishClient, AuthClient, DailyRewardClient):
def __init__(
self,
cookies: Optional[CookieTypes] = None,

View File

@ -1,3 +1,5 @@
from typing import Any
from pydantic import BaseModel
try:
@ -9,6 +11,13 @@ except ImportError:
class APIModel(BaseModel):
"""A Pydantic BaseModel class used for modeling JSON data returned by an API."""
def __init__(self, **data: Any) -> None:
for field_name, field in self.__fields__.items():
aliases = field.field_info.extra.get("aliases")
if aliases and aliases in data:
data[field_name] = data.pop(aliases)
super().__init__(**data)
class Config:
"""A nested class defining configuration options for the APIModel."""

View File

@ -1,3 +1,5 @@
from typing import Optional
from simnet.models.base import APIModel
__all__ = ("BaseCharacter",)
@ -20,10 +22,10 @@ class BaseCharacter(APIModel):
"""
id: int
name: str
element: str
rarity: int
icon: str
name: Optional[str] = None
element: Optional[str] = None
rarity: Optional[str] = None
icon: Optional[str] = None
collab: bool = False

View File

@ -27,6 +27,7 @@ class PartialCharacter(BaseCharacter):
constellation (int): The number of constellations that are currently active for the character.
"""
icon: str = Field(alias="image")
level: int
friendship: int = Field(alias="fetter")
constellation: int = Field(alias="actived_constellation_num")

View File

@ -30,21 +30,21 @@ class Stats(APIModel):
unlocked_domains (int): Number of domains unlocked by the user.
"""
achievements: int = Field(alias="achievement_number")
days_active: int = Field(alias="active_day_number")
characters: int = Field(alias="avatar_number")
spiral_abyss: str = Field(alias="spiral_abyss")
anemoculi: int = Field(alias="anemoculus_number")
geoculi: int = Field(alias="geoculus_number")
dendroculi: int = Field(alias="dendroculus_number")
electroculi: int = Field(alias="electroculus_number")
common_chests: int = Field(alias="common_chest_number")
exquisite_chests: int = Field(alias="exquisite_chest_number")
precious_chests: int = Field(alias="precious_chest_number")
luxurious_chests: int = Field(alias="luxurious_chest_number")
remarkable_chests: int = Field(alias="magic_chest_number")
unlocked_waypoints: int = Field(alias="way_point_number")
unlocked_domains: int = Field(alias="domain_number")
achievements: int = Field(aliases="achievement_number")
days_active: int = Field(aliases="active_day_number")
characters: int = Field(aliases="avatar_number")
spiral_abyss: str = Field(aliases="spiral_abyss")
anemoculi: int = Field(aliases="anemoculus_number")
geoculi: int = Field(aliases="geoculus_number")
dendroculi: int = Field(aliases="dendroculus_number")
electroculi: int = Field(aliases="electroculus_number")
common_chests: int = Field(aliases="common_chest_number")
exquisite_chests: int = Field(aliases="exquisite_chest_number")
precious_chests: int = Field(aliases="precious_chest_number")
luxurious_chests: int = Field(aliases="luxurious_chest_number")
remarkable_chests: int = Field(aliases="magic_chest_number")
unlocked_waypoints: int = Field(aliases="way_point_number")
unlocked_domains: int = Field(aliases="domain_number")
class Offering(APIModel):
@ -183,9 +183,9 @@ class PartialGenshinUserStats(APIModel):
info: UserInfo = Field("role")
stats: Stats
characters: List[PartialCharacter] = Field(alias="avatars")
explorations: List[Exploration] = Field(alias="world_explorations")
teapot: Optional[Teapot] = Field(alias="homes")
characters: List[PartialCharacter] = Field(aliases="avatars")
explorations: List[Exploration] = Field(aliases="world_explorations")
teapot: Optional[Teapot] = Field(aliases="homes")
@validator("teapot", pre=True)
def format_teapot(cls, v: Any) -> Optional[Dict[str, Any]]:

View File

@ -1,4 +1,3 @@
import calendar
from datetime import timezone, timedelta, datetime
from typing import NamedTuple
@ -25,8 +24,7 @@ class DailyRewardInfo(NamedTuple):
"""The number of rewards that the user has missed since the start of the month."""
cn_timezone = timezone(timedelta(hours=8))
now = datetime.now(cn_timezone)
month_days = calendar.monthrange(now.year, now.month)[1]
return month_days - self.claimed_rewards
return now.day - self.claimed_rewards
class DailyReward(APIModel):

View File

@ -15,14 +15,12 @@ class DSType(Enum):
Enumeration of dynamic secret types.
Attributes:
WEB (str): Android dynamic secret type.
ANDROID (str): Android dynamic secret type.
ANDROID_NEW (str): New Android dynamic secret type.
SIGN (str): Sign dynamic secret type.
"""
WEB = "web"
ANDROID = "android"
ANDROID_NEW = "android_new"
SIGN = "sign"
def hex_digest(text):
@ -87,16 +85,22 @@ def generate_dynamic_secret(
salt = "6s25p5ox5y14umn1p61aqyyvbvvl3lrt"
app_version = "1.5.0"
elif region == Region.CHINESE:
if ds_type is None:
salt = "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs"
elif ds_type == DSType.ANDROID:
salt = "KZazpG4cO2QECFDBUCxdhS8cYCsQHfzn"
client_type = "2"
elif ds_type == DSType.ANDROID_NEW:
client_type = "2"
salt = "t0qEgfub6cvueAPgR5m9aQWWVciEer7v"
if new_ds:
if ds_type is None:
salt = "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs"
elif ds_type == DSType.ANDROID:
client_type = "2"
salt = "KZazpG4cO2QECFDBUCxdhS8cYCsQHfzn"
else:
raise ValueError(f"Unknown ds_type: {ds_type}")
else:
raise ValueError(f"Unknown ds_type: {ds_type}")
if ds_type is None:
salt = "X7UOLLnTuNS3kgTJ1BUHOvKpiqp3kmym"
elif ds_type == DSType.ANDROID:
salt = "t0qEgfub6cvueAPgR5m9aQWWVciEer7v"
client_type = "2"
else:
raise ValueError(f"Unknown ds_type: {ds_type}")
else:
raise ValueError(f"Unknown region: {region}")
if new_ds:

View File

@ -1,3 +1,4 @@
import asyncio
from typing import List, Dict, Callable, Any, Awaitable
@ -45,6 +46,7 @@ class WishPaginator:
break
all_items.extend(filtered_items)
await asyncio.sleep(0.5)
# Return up to the specified limit.
return all_items[: min(len(all_items), limit)]
return all_items[: min(len(all_items), limit)] if limit else all_items