diff --git a/simnet/client/components/chronicle/genshin.py b/simnet/client/components/chronicle/genshin.py index 3c15900..8f943df 100644 --- a/simnet/client/components/chronicle/genshin.py +++ b/simnet/client/components/chronicle/genshin.py @@ -2,10 +2,11 @@ import asyncio from typing import Optional, Any, List, Dict from simnet.client.components.chronicle.base import BaseChronicleClient -from simnet.errors import DataNotPublic, BadRequest, RegionNotSupported +from simnet.client.routes import RECORD_URL +from simnet.errors import DataNotPublic, BadRequest from simnet.models.genshin.chronicle.abyss import SpiralAbyss, SpiralAbyssPair from simnet.models.genshin.chronicle.characters import Character -from simnet.models.genshin.chronicle.notes import Notes, NotesWidget +from simnet.models.genshin.chronicle.notes import Notes, NotesWidget, NotesOverseaWidget from simnet.models.genshin.chronicle.stats import ( PartialGenshinUserStats, GenshinUserStats, @@ -278,8 +279,6 @@ class GenshinBattleChronicleClient(BaseChronicleClient): RegionNotSupported: If the region is not supported. BadRequest: If the request is invalid. """ - if self.region == Region.OVERSEAS: - raise RegionNotSupported("Notes widget is not supported in overseas region.") stoken = self.cookies.get("stoken") if stoken is None: raise ValueError("stoken not found in cookies.") @@ -288,5 +287,12 @@ class GenshinBattleChronicleClient(BaseChronicleClient): raise ValueError("account_id or stuid not found") if self.account_id is not None and stuid is None: self.cookies.set("stuid", str(self.account_id)) - data = await self._request_genshin_record("widget/v2", endpoint_type="aapi", lang=lang) - return NotesWidget(**data) + if self.region == Region.OVERSEAS: + route = RECORD_URL.get_url(self.region) / "../community/apihub/api/widget/data" + params = {"game_id": "2"} + data = await self.request_lab(route, params=params, lang=lang) + model = NotesOverseaWidget + else: + data = await self._request_genshin_record("widget/v2", endpoint_type="aapi", lang=lang) + model = NotesWidget + return model(**data) diff --git a/simnet/client/components/chronicle/starrail.py b/simnet/client/components/chronicle/starrail.py index a5133f8..1f262b1 100644 --- a/simnet/client/components/chronicle/starrail.py +++ b/simnet/client/components/chronicle/starrail.py @@ -1,14 +1,15 @@ import asyncio -from typing import Optional, Mapping, Dict, Any +from typing import Optional, Mapping, Dict, Any, Union from simnet.client.components.chronicle.base import BaseChronicleClient -from simnet.errors import BadRequest, DataNotPublic, RegionNotSupported +from simnet.client.routes import RECORD_URL +from simnet.errors import BadRequest, DataNotPublic from simnet.models.lab.record import RecordCard from simnet.models.starrail.chronicle.activity import StarRailActivity from simnet.models.starrail.chronicle.challenge import StarRailChallenge from simnet.models.starrail.chronicle.characters import StarRailDetailCharacters from simnet.models.starrail.chronicle.museum import StarRailMuseumBasic, StarRailMuseumDetail -from simnet.models.starrail.chronicle.notes import StarRailNote, StarRailNoteWidget +from simnet.models.starrail.chronicle.notes import StarRailNote, StarRailNoteWidget, StarRailNoteOverseaWidget from simnet.models.starrail.chronicle.rogue import StarRailRogue from simnet.models.starrail.chronicle.stats import StarRailUserStats, StarRailUserInfo from simnet.utils.enum_ import Game, Region @@ -293,7 +294,7 @@ class StarRailBattleChronicleClient(BaseChronicleClient): async def get_starrail_notes_by_stoken( self, lang: Optional[str] = None, - ) -> StarRailNoteWidget: + ) -> Union[StarRailNoteWidget, StarRailNoteOverseaWidget]: """Get StarRail's real-time notes. Args: @@ -306,8 +307,6 @@ class StarRailBattleChronicleClient(BaseChronicleClient): RegionNotSupported: If the region is not supported. BadRequest: If the request is invalid. """ - if self.region == Region.OVERSEAS: - raise RegionNotSupported("Notes widget is not supported in overseas region.") stoken = self.cookies.get("stoken") if stoken is None: raise ValueError("stoken not found in cookies.") @@ -316,5 +315,11 @@ class StarRailBattleChronicleClient(BaseChronicleClient): raise ValueError("account_id or stuid not found") if self.account_id is not None and stuid is None: self.cookies.set("stuid", str(self.account_id)) - data = await self._request_starrail_record("widget", endpoint_type="aapi", lang=lang) - return StarRailNoteWidget(**data) + if self.region == Region.OVERSEAS: + route = RECORD_URL.get_url(self.region) / "../community/apihub/api/hsr_widget" + data = await self.request_lab(route, lang=lang) + model = StarRailNoteOverseaWidget + else: + data = await self._request_starrail_record("widget", endpoint_type="aapi", lang=lang) + model = StarRailNoteWidget + return model(**data) diff --git a/simnet/models/genshin/chronicle/notes.py b/simnet/models/genshin/chronicle/notes.py index ef0d86a..ec23721 100644 --- a/simnet/models/genshin/chronicle/notes.py +++ b/simnet/models/genshin/chronicle/notes.py @@ -5,7 +5,15 @@ from pydantic import Field, root_validator from simnet.models.base import APIModel -__all__ = ("Expedition", "Notes", "ExpeditionWidget", "NotesWidget") +__all__ = [ + "Expedition", + "Notes", + "ExpeditionWidget", + "NotesWidget", + "NotesOverseaWidget", + "NotesOverseaWidgetResin", + "NotesOverseaWidgetRealm", +] def _process_timedelta(time: Union[int, timedelta, datetime]) -> datetime: @@ -236,3 +244,57 @@ class NotesWidget(APIModel): def resin_recovery_time(self) -> datetime: """A property that returns the time when resin will be fully recovered.""" return datetime.now().astimezone() + self.remaining_resin_recovery_time + + +class NotesOverseaWidgetResin(APIModel): + """The model for real-time notes resin. + + Attributes: + current_val (int): The current amount of resin. + max_val (int): The maximum amount of resin. + remaining_resin_recovery_time (timedelta): The remaining time until resin recovery. + """ + + current_val: int + max_val: int + remaining_resin_recovery_time: timedelta = Field(alias="recovery_time") + + @property + def resin_recovery_time(self) -> datetime: + """A property that returns the time when resin will be fully recovered.""" + return datetime.now().astimezone() + self.remaining_resin_recovery_time + + +class NotesOverseaWidgetRealm(APIModel): + """The model for real-time notes realm currency. + + Attributes: + current_val (int): The current amount of realm currency. + max_val (int): The maximum amount of realm currency. + remaining_realm_currency_recovery_time (timedelta): The remaining time until realm currency recovery. + """ + + current_val: int + max_val: int + remaining_realm_currency_recovery_time: timedelta = Field(alias="recovery_time") + + @property + def realm_currency_recovery_time(self) -> datetime: + """A property that returns the time when realm currency will be fully recovered.""" + return datetime.now().astimezone() + self.remaining_realm_currency_recovery_time + + +class NotesOverseaWidget(APIModel): + """The model for real-time notes. + + Attributes: + resin (NotesOverseaWidgetResin): The resin information. + home_coin (NotesOverseaWidgetRealm): The realm currency information. + + Raises: + ValueError: If the remaining resin recovery time is less than 0 or greater than 8 hours, + or if the remaining realm currency recovery time is less than 0 or greater than 24 hours. + """ + + resin: NotesOverseaWidgetResin + home_coin: NotesOverseaWidgetRealm diff --git a/simnet/models/starrail/chronicle/notes.py b/simnet/models/starrail/chronicle/notes.py index c89d22f..a30b9ac 100644 --- a/simnet/models/starrail/chronicle/notes.py +++ b/simnet/models/starrail/chronicle/notes.py @@ -110,3 +110,80 @@ class StarRailNoteWidget(APIModel): has_signed: bool """Whether the user has signed in today""" + + +class StarRailNoteOverseaWidgetChallenge(APIModel): + """Represents a StarRail Note. + + Attributes: + begin_time (datetime): The time at which the challenge begins. + current_floor (int): The current floor of the challenge. + end_time (datetime): The time at which the challenge ends. + max_floor (int): The max floor of the challenge. + schedule_id (int): The ID of the challenge. + """ + + begin_time: datetime + """The time at which the challenge begins.""" + current_floor: int + """The current floor of the challenge.""" + end_time: datetime + """The time at which the challenge ends.""" + max_floor: int + """The max floor of the challenge.""" + schedule_id: int + """The ID of the challenge.""" + + +class StarRailNoteOverseaWidgetRogue(APIModel): + """Represents a StarRail Note. + + Attributes: + current_rogue_score (int): The current simulated universe weekly points. + max_rogue_score (int): The max simulated universe weekly points. + schedule_end (datetime): The time at which the challenge ends. + schedule_start (datetime): The time at which the challenge begins. + """ + + current_rogue_score: int + """Current simulated universe weekly points""" + max_rogue_score: int + """Max simulated universe weekly points""" + schedule_end: datetime + """The time at which the challenge ends.""" + schedule_start: datetime + """The time at which the challenge begins.""" + + +class StarRailNoteOverseaWidget(APIModel): + """Represents a StarRail Note. + + Attributes: + current_stamina (int): The current stamina of the user. + max_stamina (int): The maximum stamina of the user. + stamina_recover_time (timedelta): The time it takes for one stamina to recover. + accepted_expedition_num (int): The number of expeditions the user has accepted. + total_expedition_num (int): The total number of expeditions the user has participated in. + expeditions (Sequence[StarRailExpedition]): A list of expeditions the user has participated in. + current_train_score (int): The current daily training activity. + max_train_score (int): The max daily training activity. + challenge (StarRailNoteOverseaWidgetChallenge): The challenge widget. + rogue (StarRailNoteOverseaWidgetRogue): The rogue widget. + """ + + current_stamina: int + max_stamina: int + stamina_recover_time: timedelta + accepted_expedition_num: int + total_expedition_num: int + expeditions: Sequence[StarRailExpedition] + + current_train_score: int + """Current daily training activity""" + max_train_score: int + """Max daily training activity""" + + challenge: StarRailNoteOverseaWidgetChallenge + """The challenge widget.""" + rogue: StarRailNoteOverseaWidgetRogue + """The rogue widget."""