SIMNet/simnet/errors.py

317 lines
8.4 KiB
Python
Raw Normal View History

2023-05-01 09:30:57 +00:00
from typing import Any, Optional, Dict, Union, Tuple, Type, NoReturn
class SIMNetException(Exception):
"""Base class for SIMNet errors."""
2023-05-01 09:30:57 +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."""
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.
"""
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:
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
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
def response(self) -> dict[str, Union[str, Any, None]]:
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."
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)
2023-05-01 09:30:57 +00:00
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
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,
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,
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)