mirror of
https://github.com/PaiGramTeam/SIMNet.git
synced 2024-11-16 12:02:17 +00:00
07977a7738
The project was originally initialized as 'ApiHelper', but has since been formally renamed to 'SIMNet'. This commit updates the base exception class name and the associated documentation comment to reflect this change, for consistency.
316 lines
8.4 KiB
Python
316 lines
8.4 KiB
Python
from typing import Any, Optional, Dict, Union, Tuple, Type, NoReturn
|
|
|
|
|
|
class SIMNetException(Exception):
|
|
"""Base class for SIMNet errors."""
|
|
|
|
|
|
class NetworkError(SIMNetException):
|
|
"""Base class for exceptions due to networking errors."""
|
|
|
|
|
|
class TimedOut(NetworkError):
|
|
"""Raised when a request took too long to finish."""
|
|
|
|
|
|
class BadRequest(SIMNetException):
|
|
"""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.
|
|
"""
|
|
|
|
status_code: int = 200
|
|
ret_code: int = 0
|
|
original: str = ""
|
|
message: str = ""
|
|
|
|
def __init__(
|
|
self,
|
|
response: Optional[Dict[str, Any]] = None,
|
|
message: Optional[str] = None,
|
|
status_code: Optional[int] = None,
|
|
) -> None:
|
|
if status_code is not None:
|
|
self.status_code = status_code
|
|
if response is not None:
|
|
ret_code = response.get("retcode")
|
|
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
|
|
if message is not None or self.original is not None:
|
|
self.message = message or self.original
|
|
|
|
display_code = self.ret_code or self.status_code
|
|
display_message = f"[{display_code}] {self.message}" if display_code else self.message
|
|
|
|
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
|
|
def response(self) -> dict[str, Union[str, Any, None]]:
|
|
return {"retcode": self.ret_code, "message": self.original}
|
|
|
|
@property
|
|
def retcode(self) -> int:
|
|
return self.ret_code
|
|
|
|
|
|
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."
|
|
|
|
|
|
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."
|
|
|
|
|
|
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."
|
|
|
|
|
|
class NotSupported(SIMNetException):
|
|
"""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)
|
|
|
|
|
|
class RedemptionClaimed(RedemptionException):
|
|
"""Redemption code has been claimed already."""
|
|
|
|
message = "Redemption code has been claimed already."
|
|
|
|
|
|
_TBR = Type[BadRequest]
|
|
_errors: Dict[int, Union[_TBR, str, Tuple[_TBR, Optional[str]]]] = {
|
|
# misc hoyolab
|
|
-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,
|
|
10103: (
|
|
InvalidCookies,
|
|
"Cookies are valid but do not have a hoyolab account bound to them.",
|
|
),
|
|
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.",
|
|
1034: NeedChallenge,
|
|
}
|
|
|
|
ERRORS: Dict[int, Tuple[_TBR, Optional[str]]] = {
|
|
ret_code: ((exc, None) if isinstance(exc, type) else (BadRequest, exc) if isinstance(exc, str) else exc)
|
|
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)
|