mirror of
https://github.com/PaiGramTeam/SIMNet.git
synced 2024-11-21 21:58:05 +00:00
🐛 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:
parent
5d2d3609bb
commit
4250481727
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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]
|
||||
|
@ -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(
|
||||
|
@ -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,9 +34,12 @@ 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
|
||||
)
|
||||
wishes = []
|
||||
for banner_type in banner_types:
|
||||
paginator = WishPaginator(
|
||||
end_id,
|
||||
partial(
|
||||
@ -50,5 +51,5 @@ class GenshinWishClient(BaseWishClient):
|
||||
)
|
||||
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.extend([Wish(**i, banner_name=banner_name) for i in items])
|
||||
return sorted(wishes, key=lambda wish: wish.time.timestamp())
|
||||
|
@ -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,6 +35,9 @@ class StarRailWishClient(BaseWishClient):
|
||||
Returns:
|
||||
List[StarRailWish]: A list of StarRailWish objects representing the retrieved wishes.
|
||||
"""
|
||||
banner_types = banner_types or [1, 2, 11, 12]
|
||||
wishes = []
|
||||
for banner_type in banner_types:
|
||||
paginator = WishPaginator(
|
||||
end_id,
|
||||
partial(
|
||||
@ -44,5 +48,5 @@ class StarRailWishClient(BaseWishClient):
|
||||
),
|
||||
)
|
||||
items = await paginator.get(limit)
|
||||
wish = [StarRailWish(**i) for i in items]
|
||||
return wish
|
||||
wishes.extend([StarRailWish(**i) for i in items])
|
||||
return sorted(wishes, key=lambda wish: wish.time.timestamp())
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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."""
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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]]:
|
||||
|
@ -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):
|
||||
|
@ -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,14 +85,20 @@ def generate_dynamic_secret(
|
||||
salt = "6s25p5ox5y14umn1p61aqyyvbvvl3lrt"
|
||||
app_version = "1.5.0"
|
||||
elif region == Region.CHINESE:
|
||||
if new_ds:
|
||||
if ds_type is None:
|
||||
salt = "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs"
|
||||
elif ds_type == DSType.ANDROID:
|
||||
client_type = "2"
|
||||
salt = "KZazpG4cO2QECFDBUCxdhS8cYCsQHfzn"
|
||||
client_type = "2"
|
||||
elif ds_type == DSType.ANDROID_NEW:
|
||||
client_type = "2"
|
||||
else:
|
||||
raise ValueError(f"Unknown ds_type: {ds_type}")
|
||||
else:
|
||||
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:
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user