Add Lab Client

This commit is contained in:
洛水居室 2023-05-08 10:00:01 +08:00 committed by GitHub
parent 2a86d10640
commit 5f5c222f17
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 343 additions and 4 deletions

View File

@ -0,0 +1,210 @@
import asyncio
from typing import Optional, List, Dict, Any
from simnet.client.base import BaseClient
from simnet.client.headers import Headers
from simnet.client.routes import TAKUMI_URL, HK4E_URL, CODE_URL
from simnet.models.lab.announcement import Announcement
from simnet.models.lab.record import PartialUser, FullUser
from simnet.utils.enum_ import Region
from simnet.utils.lang import create_short_lang_code
from simnet.utils.player import recognize_genshin_server
from simnet.utils.types import HeaderTypes
__all__ = ("LabClient",)
class LabClient(BaseClient):
"""LabClient component."""
async def request_bbs(
self,
endpoint: str,
*,
lang: Optional[str] = None,
region: Optional[Region] = None,
method: Optional[str] = None,
params: Optional[Dict[str, Any]] = None,
data: Any = None,
headers: Optional[HeaderTypes] = None,
) -> Dict[str, Any]:
"""Makes a request to a bbs endpoint.
Args:
endpoint (str): The URL of the endpoint to make the request to.
lang (str, optional): The language code used for the request. Defaults to None.
region (Region, optional): The server region used for the request. Defaults to None.
method (str, optional): The HTTP method used for the request. Defaults to None.
params (dict, optional): The parameters to include in the request. Defaults to None.
data (any, optional): The data to include in the request. Defaults to None.
headers (dict, optional): The headers to include in the request. Defaults to None.
Returns:
dict: The response data from the request.
"""
headers = Headers(headers)
lang = lang or self.lang
region = region or self.region
url = TAKUMI_URL.get_url(region) / endpoint
if self.region == Region.CHINESE:
headers["Referer"] = "https://www.miyoushe.com/"
data = await self.request_lab(
url,
method=method,
params=params,
data=data,
headers=headers,
lang=lang,
new_ds=self.region == Region.CHINESE,
)
return data
async def search_users(
self,
keyword: str,
*,
lang: Optional[str] = None,
) -> List[PartialUser]:
"""Searches for users by keyword.
Args:
keyword (str): The keyword to search for.
lang (str, optional): The language code used for the request. Defaults to None.
Returns:
list of PartialUser: A list of partial user objects that match the search criteria.
"""
data = await self.request_bbs(
"community/search/wapi/search/user",
lang=lang,
params=dict(keyword=keyword, page_size=20),
)
return [PartialUser(**i["user"]) for i in data["list"]]
async def get_user_info(
self,
accident: Optional[int] = None,
*,
lang: Optional[str] = None,
) -> FullUser:
"""Gets user information for the current or specified user.
Args:
accident (int, optional): The user ID to get information for. Defaults to None.
lang (str, optional): The language code used for the request. Defaults to None.
Returns:
FullUser: A full user object for the specified user.
"""
if self.region == Region.OVERSEAS:
url = "/community/painter/wapi/user/full"
elif self.region == Region.CHINESE:
url = "/user/wapi/getUserFullInfo"
else:
raise TypeError(f"{self.region!r} is not a valid region.")
data = await self.request_bbs(
endpoint=url,
lang=lang,
params=dict(uid=accident) if accident else None,
)
return FullUser(**data["user_info"])
async def get_recommended_users(self, *, limit: int = 200) -> List[PartialUser]:
"""Gets a list of recommended active users.
Args:
limit (int, optional): The maximum number of users to retrieve. Defaults to 200.
Returns:
list of PartialUser: A list of partial user objects for recommended active users.
"""
data = await self.request_bbs(
"community/user/wapi/recommendActive",
params=dict(page_size=limit),
)
return [PartialUser(**i["user"]) for i in data["list"]]
async def get_genshin_announcements(
self,
player_id: Optional[str] = None,
*,
lang: Optional[str] = None,
) -> List[Announcement]:
"""Gets a list of Genshin Impact game announcements.
Args:
player_id (str, optional): The player ID to get announcements for. Defaults to None.
lang (str, optional): The language code used for the request. Defaults to None.
Returns:
list of Announcement: A list of announcement objects for the specified player.
"""
player_id = self.player_id or player_id
if player_id is None:
player_id = 900000005
params = dict(
game="hk4e",
game_biz="hk4e_global",
bundle_id="hk4e_global",
platform="pc",
region=recognize_genshin_server(player_id),
uid=player_id,
level=8,
lang=lang or self.lang,
)
info, details = await asyncio.gather(
self.request_bbs(
HK4E_URL.get_url() / "announcement/api/getAnnList",
lang=lang,
params=params,
),
self.request_bbs(
HK4E_URL.get_url() / "announcement/api/getAnnContent",
lang=lang,
params=params,
),
)
announcements: List[Dict[str, Any]] = []
for sublist in info["list"]:
for info in sublist["list"]:
detail = next(
(i for i in details["list"] if i["ann_id"] == info["ann_id"]), None
)
announcements.append({**info, **(detail or {})})
return [Announcement(**i) for i in announcements]
async def redeem_code(
self,
code: str,
player_id: Optional[int] = None,
*,
lang: Optional[str] = None,
) -> None:
"""Redeems a gift code for the current or specified user.
Args:
code (str): The gift code to redeem.
player_id (int, optional): The player ID to redeem the code for. Defaults to None.
lang (str, optional): The language code used for the request. Defaults to None.
"""
player_id = self.player_id or player_id
await self.request_bbs(
CODE_URL.get_url(),
params=dict(
uid=player_id,
region=recognize_genshin_server(player_id),
cdkey=code,
game_biz="hk4e_global",
lang=create_short_lang_code(lang or self.lang),
),
)

View File

@ -3,6 +3,7 @@ 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.lab import LabClient
from simnet.client.components.wish.genshin import GenshinWishClient
from simnet.utils.enum_ import Game
@ -10,7 +11,11 @@ __all__ = ("GenshinClient",)
class GenshinClient(
GenshinBattleChronicleClient, GenshinWishClient, AuthClient, DailyRewardClient
GenshinBattleChronicleClient,
GenshinWishClient,
AuthClient,
DailyRewardClient,
LabClient,
):
"""A simple http client for StarRail endpoints."""

View File

@ -3,12 +3,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.lab import LabClient
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, DailyRewardClient):
class GenshinClient(GenshinBattleChronicleClient, GenshinWishClient, AuthClient, DailyRewardClient, LabClient):
def __init__(
self,
cookies: Optional[CookieTypes] = None,

View File

@ -21,6 +21,10 @@ __all__ = (
"AUTH_KEY_URL",
"HK4E_LOGIN_URL",
"REWARD_URL",
"TAKUMI_URL",
"CALCULATOR_URL",
"HK4E_URL",
"CODE_URL",
)
@ -227,3 +231,20 @@ REWARD_URL = GameRoute(
hkrpg="https://api-takumi.mihoyo.com/event/luna/?act_id=e202304121516551",
),
)
TAKUMI_URL = InternationalRoute(
overseas="https://api-os-takumi.mihoyo.com/",
chinese="https://api-takumi.mihoyo.com/",
)
CALCULATOR_URL = InternationalRoute(
overseas="https://sg-public-api.hoyoverse.com/event/calculateos/",
chinese="https://api-takumi.mihoyo.com/event/e20200928calculate/v1/",
)
HK4E_URL = Route("https://sg-hk4e-api.hoyoverse.com/common/hk4e_global/")
CODE_URL = Route(
"https://sg-hk4e-api.hoyoverse.com/common/apicdkey/api/webExchangeCdkey"
)

View File

@ -2,13 +2,21 @@ from typing import Optional
from simnet.client.components.auth import AuthClient
from simnet.client.components.chronicle.starrail import StarRailBattleChronicleClient
from simnet.client.components.daily import DailyRewardClient
from simnet.client.components.lab import LabClient
from simnet.client.components.wish.starrail import StarRailWishClient
from simnet.utils.enum_ import Game
__all__ = ("StarRailClient",)
class StarRailClient(StarRailBattleChronicleClient, StarRailWishClient, AuthClient):
class StarRailClient(
StarRailBattleChronicleClient,
StarRailWishClient,
DailyRewardClient,
AuthClient,
LabClient,
):
"""A simple http client for StarRail endpoints."""
game: Optional[Game] = Game.STARRAIL

View File

@ -2,11 +2,14 @@ from typing import Optional
from simnet.client.components.auth import AuthClient
from simnet.client.components.chronicle.starrail import StarRailBattleChronicleClient
from simnet.client.components.daily import DailyRewardClient
from simnet.client.components.lab import LabClient
from simnet.client.components.wish.starrail import StarRailWishClient
from simnet.utils.enum_ import Region
from simnet.utils.types import CookieTypes, HeaderTypes, TimeoutTypes
class StarRailClient(StarRailBattleChronicleClient, StarRailWishClient, AuthClient):
class StarRailClient(StarRailBattleChronicleClient, StarRailWishClient,DailyRewardClient, AuthClient, LabClient):
def __init__(
self,
cookies: Optional[CookieTypes] = None,

View File

@ -0,0 +1,62 @@
from datetime import datetime
from pydantic import Field
from simnet.models.base import APIModel
__all__ = ("Announcement",)
class Announcement(APIModel):
"""
Represents an announcement.
Attributes:
id (int): The ID of the announcement.
title (str): The title of the announcement.
subtitle (str): The subtitle of the announcement.
banner (str): The URL of the banner image for the announcement.
content (str): The content of the announcement.
type_label (str): The label of the announcement type.
type (int): The type of the announcement.
tag_icon (str): The URL of the tag icon for the announcement.
login_alert (bool): Indicates whether the announcement is shown to logged-in users only.
remind (bool): Indicates whether to send reminder notifications for the announcement.
alert (bool): Indicates whether to send alert notifications for the announcement.
remind_ver (int): The version of the reminder notification.
extra_remind (bool): Indicates whether to send additional reminder notifications for the announcement.
start_time (datetime): The start time of the announcement.
end_time (datetime): The end time of the announcement.
tag_start_time (datetime): The start time of the tag for the announcement.
tag_end_time (datetime): The end time of the tag for the announcement.
lang (str): The language of the announcement.
has_content (bool): Indicates whether the announcement has content.
"""
id: int = Field(alias="ann_id")
title: str
subtitle: str
banner: str
content: str
type_label: str
type: int
tag_icon: str
login_alert: bool
remind: bool
alert: bool
remind_ver: int
extra_remind: bool
start_time: datetime
end_time: datetime
tag_start_time: datetime
tag_end_time: datetime
lang: str
has_content: bool

29
tests/test_lab_client.py Normal file
View File

@ -0,0 +1,29 @@
from typing import TYPE_CHECKING
import pytest
import pytest_asyncio
from simnet.client.components.lab import LabClient
from simnet.utils.enum_ import Region
if TYPE_CHECKING:
from simnet.client.cookies import Cookies
@pytest_asyncio.fixture
async def client_instance(account_id: int, cookies: "Cookies"):
async with LabClient(
cookies=cookies,
account_id=account_id,
region=Region.CHINESE,
) as client_instance:
yield client_instance
@pytest.mark.asyncio
class TestStarRailClient:
@staticmethod
async def test_get_user_info(account_id: int, client_instance: "LabClient"):
user_info = await client_instance.get_user_info()
assert user_info.nickname
assert user_info.accident_id == account_id