2023-05-01 09:30:57 +00:00
|
|
|
from typing import Any, Optional, Dict, Union, Tuple, Type, NoReturn
|
|
|
|
|
|
|
|
|
2023-07-19 12:07:44 +00:00
|
|
|
class SIMNetException(Exception):
|
|
|
|
"""Base class for SIMNet errors."""
|
2023-05-01 09:30:57 +00:00
|
|
|
|
|
|
|
|
2023-07-19 12:07:44 +00:00
|
|
|
class NetworkError(SIMNetException):
|
2023-05-01 09:30:57 +00:00
|
|
|
"""Base class for exceptions due to networking errors."""
|
|
|
|
|
|
|
|
|
|
|
|
class TimedOut(NetworkError):
|
|
|
|
"""Raised when a request took too long to finish."""
|
|
|
|
|
|
|
|
|
2023-07-19 12:07:44 +00:00
|
|
|
class BadRequest(SIMNetException):
|
2023-05-01 09:30:57 +00:00
|
|
|
"""Raised when an API request cannot be processed correctly.
|
|
|
|
|
|
|
|
Attributes:
|
|
|
|
status_code (int): The status code of the response.
|
|
|
|
ret_code (int): The error code of the response.
|
|
|
|
original (str): The original error message of the response.
|
|
|
|
message (str): The formatted error message of the response.
|
|
|
|
"""
|
|
|
|
|
2023-06-11 07:15:00 +00:00
|
|
|
status_code: int = 200
|
|
|
|
ret_code: int = 0
|
|
|
|
original: str = ""
|
|
|
|
message: str = ""
|
|
|
|
|
2023-05-01 09:30:57 +00:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
response: Optional[Dict[str, Any]] = None,
|
|
|
|
message: Optional[str] = None,
|
|
|
|
status_code: Optional[int] = None,
|
|
|
|
) -> None:
|
2023-06-11 07:15:00 +00:00
|
|
|
if status_code is not None:
|
|
|
|
self.status_code = status_code
|
2023-06-11 14:19:20 +00:00
|
|
|
if response is not None:
|
2023-07-19 04:41:39 +00:00
|
|
|
ret_code = response.get("retcode")
|
2023-06-11 14:19:20 +00:00
|
|
|
if ret_code is not None:
|
|
|
|
self.ret_code = ret_code
|
|
|
|
response_message = response.get("message")
|
|
|
|
if response_message is not None:
|
|
|
|
self.original = response_message
|
2023-06-11 07:15:00 +00:00
|
|
|
if message is not None or self.original is not None:
|
|
|
|
self.message = message or self.original
|
2023-05-01 09:30:57 +00:00
|
|
|
|
|
|
|
display_code = self.ret_code or self.status_code
|
2023-05-12 03:24:06 +00:00
|
|
|
display_message = f"[{display_code}] {self.message}" if display_code else self.message
|
2023-05-01 09:30:57 +00:00
|
|
|
|
|
|
|
super().__init__(display_message)
|
|
|
|
|
|
|
|
def __repr__(self) -> str:
|
|
|
|
response = {
|
|
|
|
"status_code": self.status_code,
|
|
|
|
"retcode": self.ret_code,
|
|
|
|
"message": self.original,
|
|
|
|
}
|
|
|
|
return f"{type(self).__name__}({repr(response)})"
|
|
|
|
|
|
|
|
@property
|
2023-09-30 11:30:01 +00:00
|
|
|
def response(self) -> Dict[str, Union[str, Any, None]]:
|
2023-05-01 09:30:57 +00:00
|
|
|
return {"retcode": self.ret_code, "message": self.original}
|
|
|
|
|
2023-06-12 02:09:03 +00:00
|
|
|
@property
|
|
|
|
def retcode(self) -> int:
|
|
|
|
return self.ret_code
|
|
|
|
|
2023-05-01 09:30:57 +00:00
|
|
|
|
|
|
|
class InternalDatabaseError(BadRequest):
|
|
|
|
"""Internal database error."""
|
|
|
|
|
|
|
|
ret_code = -1
|
|
|
|
|
|
|
|
|
|
|
|
class AccountNotFound(BadRequest):
|
|
|
|
"""Tried to get data with an invalid uid."""
|
|
|
|
|
|
|
|
message = "Could not find user; uid may be invalid."
|
|
|
|
|
|
|
|
|
|
|
|
class DataNotPublic(BadRequest):
|
|
|
|
"""User hasn't set their data to public."""
|
|
|
|
|
|
|
|
message = "User's data is not public."
|
|
|
|
|
|
|
|
|
|
|
|
class CookieException(BadRequest):
|
|
|
|
"""Base error for cookies."""
|
|
|
|
|
|
|
|
|
|
|
|
class InvalidCookies(CookieException):
|
|
|
|
"""Cookies weren't valid."""
|
|
|
|
|
|
|
|
ret_code = -100
|
|
|
|
message = "Cookies are not valid."
|
|
|
|
|
|
|
|
|
2023-12-17 15:28:58 +00:00
|
|
|
class LabAccountNotFound(InvalidCookies):
|
|
|
|
"""Lab account not found."""
|
|
|
|
|
|
|
|
ret_code = 10103
|
|
|
|
message = "Cookies are valid but do not have a hoyolab account bound to them."
|
|
|
|
|
|
|
|
|
2023-05-01 09:30:57 +00:00
|
|
|
class TooManyRequests(CookieException):
|
|
|
|
"""Made too many requests and got ratelimited."""
|
|
|
|
|
|
|
|
ret_code = 10101
|
|
|
|
message = "Cannot get data for more than 30 accounts per cookie per day."
|
|
|
|
|
|
|
|
|
|
|
|
class VisitsTooFrequently(BadRequest):
|
|
|
|
"""Visited a page too frequently.
|
|
|
|
|
|
|
|
Must be handled with exponential backoff.
|
|
|
|
"""
|
|
|
|
|
|
|
|
ret_code = -110
|
|
|
|
message = "Visits too frequently."
|
|
|
|
|
|
|
|
|
|
|
|
class AlreadyClaimed(BadRequest):
|
|
|
|
"""Already claimed the daily reward today."""
|
|
|
|
|
|
|
|
ret_code = -5003
|
|
|
|
message = "Already claimed the daily reward today."
|
|
|
|
|
|
|
|
|
|
|
|
class AuthkeyException(BadRequest):
|
|
|
|
"""Base error for authkeys."""
|
|
|
|
|
|
|
|
|
|
|
|
class InvalidAuthkey(AuthkeyException):
|
|
|
|
"""Authkey is not valid."""
|
|
|
|
|
|
|
|
ret_code = -100
|
|
|
|
message = "Authkey is not valid."
|
|
|
|
|
|
|
|
|
|
|
|
class AuthkeyTimeout(AuthkeyException):
|
|
|
|
"""Authkey has timed out."""
|
|
|
|
|
|
|
|
ret_code = -101
|
|
|
|
message = "Authkey has timed out."
|
|
|
|
|
|
|
|
|
|
|
|
class RedemptionException(BadRequest):
|
|
|
|
"""Exception caused by redeeming a code."""
|
|
|
|
|
|
|
|
|
|
|
|
class RedemptionInvalid(RedemptionException):
|
|
|
|
"""Invalid redemption code."""
|
|
|
|
|
|
|
|
message = "Invalid redemption code."
|
|
|
|
|
|
|
|
|
|
|
|
class RedemptionCooldown(RedemptionException):
|
|
|
|
"""Redemption is on cooldown."""
|
|
|
|
|
|
|
|
message = "Redemption is on cooldown."
|
|
|
|
|
|
|
|
|
2023-06-11 07:15:00 +00:00
|
|
|
class NeedChallenge(BadRequest):
|
|
|
|
"""Need to complete a captcha challenge."""
|
|
|
|
|
|
|
|
ret_code = 1034
|
|
|
|
message = "Need to complete a captcha challenge."
|
|
|
|
|
|
|
|
|
|
|
|
class GeetestTriggered(NeedChallenge):
|
|
|
|
"""Geetest triggered during daily reward claim."""
|
|
|
|
|
|
|
|
ret_code = 0
|
|
|
|
message = "Geetest triggered during daily reward claim."
|
|
|
|
|
|
|
|
def __init__(self, gt: str, challenge: str, *args, **kwargs):
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
self.gt = gt
|
|
|
|
self.challenge = challenge
|
|
|
|
|
|
|
|
|
|
|
|
class GeetestChallengeFailed(NeedChallenge):
|
|
|
|
"""Geetest challenge failed."""
|
|
|
|
|
|
|
|
message = "Geetest challenge failed."
|
|
|
|
|
|
|
|
|
2023-07-19 12:07:44 +00:00
|
|
|
class NotSupported(SIMNetException):
|
2023-06-11 07:15:00 +00:00
|
|
|
"""API not supported."""
|
|
|
|
|
|
|
|
def __init__(self, message: str = "API not supported."):
|
|
|
|
super().__init__(message)
|
|
|
|
|
|
|
|
|
|
|
|
class RegionNotSupported(NotSupported):
|
|
|
|
"""API not supported for this region."""
|
|
|
|
|
|
|
|
def __init__(self, message: str = "API not supported for this region."):
|
|
|
|
super().__init__(message)
|
|
|
|
|
|
|
|
|
|
|
|
class GameNotSupported(NotSupported):
|
|
|
|
"""API not supported for this game."""
|
|
|
|
|
|
|
|
def __init__(self, message: str = "API not supported for this game."):
|
|
|
|
super().__init__(message)
|
|
|
|
|
|
|
|
|
|
|
|
class RequestNotSupported(BadRequest):
|
|
|
|
"""Service not supported for this request."""
|
|
|
|
|
|
|
|
ret_code = -520
|
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
super().__init__(message="service not supported for this request.", *args, **kwargs)
|
|
|
|
|
|
|
|
|
2023-05-01 09:30:57 +00:00
|
|
|
class RedemptionClaimed(RedemptionException):
|
|
|
|
"""Redemption code has been claimed already."""
|
|
|
|
|
|
|
|
message = "Redemption code has been claimed already."
|
|
|
|
|
|
|
|
|
2023-11-18 15:40:07 +00:00
|
|
|
class InvalidDevice(BadRequest):
|
|
|
|
"""Device is invalid."""
|
|
|
|
|
|
|
|
ret_code = 5003
|
|
|
|
message = "Device id and fp are invalid."
|
|
|
|
|
|
|
|
|
2023-05-01 09:30:57 +00:00
|
|
|
_TBR = Type[BadRequest]
|
|
|
|
_errors: Dict[int, Union[_TBR, str, Tuple[_TBR, Optional[str]]]] = {
|
|
|
|
# misc hoyolab
|
2023-09-11 11:55:26 +00:00
|
|
|
-3: InvalidCookies,
|
2023-05-01 09:30:57 +00:00
|
|
|
-100: InvalidCookies,
|
|
|
|
-108: "Invalid language.",
|
|
|
|
-110: VisitsTooFrequently,
|
|
|
|
# game record
|
|
|
|
10001: InvalidCookies,
|
|
|
|
-10001: "Malformed request.",
|
|
|
|
-10002: "No genshin account associated with cookies.",
|
|
|
|
# database game record
|
|
|
|
10101: TooManyRequests,
|
|
|
|
10102: DataNotPublic,
|
2023-12-17 15:28:58 +00:00
|
|
|
10103: LabAccountNotFound,
|
2023-05-01 09:30:57 +00:00
|
|
|
10104: "Cannot view real-time notes of other users.",
|
|
|
|
# calculator
|
|
|
|
-500001: "Invalid fields in calculation.",
|
|
|
|
-500004: VisitsTooFrequently,
|
|
|
|
-502001: "User does not have this character.",
|
|
|
|
-502002: "Calculator sync is not enabled.",
|
|
|
|
# mixin
|
|
|
|
-1: InternalDatabaseError,
|
|
|
|
1009: AccountNotFound,
|
|
|
|
# redemption
|
|
|
|
-1065: RedemptionInvalid,
|
|
|
|
-1071: InvalidCookies,
|
|
|
|
-1073: (AccountNotFound, "Account has no game account bound to it."),
|
|
|
|
-2001: (RedemptionInvalid, "Redemption code has expired."),
|
|
|
|
-2003: (RedemptionInvalid, "Redemption code is incorrectly formatted."),
|
|
|
|
-2004: RedemptionInvalid,
|
|
|
|
-2014: (RedemptionInvalid, "Redemption code not activated"),
|
|
|
|
-2016: RedemptionCooldown,
|
|
|
|
-2017: RedemptionClaimed,
|
|
|
|
-2018: RedemptionClaimed,
|
|
|
|
-2021: (
|
|
|
|
RedemptionException,
|
|
|
|
"Cannot claim codes for accounts with adventure rank lower than 10.",
|
|
|
|
),
|
|
|
|
# rewards
|
|
|
|
-5003: AlreadyClaimed,
|
|
|
|
# chinese
|
|
|
|
1008: AccountNotFound,
|
|
|
|
-1104: "This action must be done in the app.",
|
2023-06-11 07:15:00 +00:00
|
|
|
1034: NeedChallenge,
|
2023-11-18 15:40:07 +00:00
|
|
|
5003: InvalidDevice,
|
2023-05-01 09:30:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
ERRORS: Dict[int, Tuple[_TBR, Optional[str]]] = {
|
2023-05-12 03:24:06 +00:00
|
|
|
ret_code: ((exc, None) if isinstance(exc, type) else (BadRequest, exc) if isinstance(exc, str) else exc)
|
2023-05-01 09:30:57 +00:00
|
|
|
for ret_code, exc in _errors.items()
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def raise_for_ret_code(data: Dict[str, Any]) -> NoReturn:
|
|
|
|
"""Raise an equivalent error to a response.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
data (dict): The response data.
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
InvalidAuthkey: If the authkey is invalid.
|
|
|
|
AuthkeyTimeout: If the authkey has timed out.
|
|
|
|
AuthkeyException: If there is an authkey exception.
|
|
|
|
RedemptionException: If there is a redemption exception.
|
|
|
|
BadRequest: If there is a bad request.
|
|
|
|
|
|
|
|
game record:
|
|
|
|
10001 = invalid cookie
|
|
|
|
101xx = generic errors
|
|
|
|
authkey:
|
|
|
|
-100 = invalid authkey
|
|
|
|
-101 = authkey timed out
|
|
|
|
code redemption:
|
|
|
|
20xx = invalid code or state
|
|
|
|
-107x = invalid cookies
|
|
|
|
daily reward:
|
|
|
|
-500x = already claimed the daily reward
|
|
|
|
"""
|
|
|
|
r, m = data.get("retcode", 0), data.get("message", "")
|
|
|
|
|
|
|
|
if m.startswith("authkey"):
|
|
|
|
if r == -100:
|
|
|
|
raise InvalidAuthkey(data)
|
|
|
|
if r == -101:
|
|
|
|
raise AuthkeyTimeout(data)
|
|
|
|
raise AuthkeyException(data)
|
|
|
|
|
|
|
|
if r in ERRORS:
|
|
|
|
exc_type, msg = ERRORS[r]
|
|
|
|
raise exc_type(data, msg)
|
|
|
|
|
|
|
|
if "redemption" in m:
|
|
|
|
raise RedemptionException(data)
|
|
|
|
|
|
|
|
raise BadRequest(data)
|