Improve GQL error reporting
This commit is contained in:
1 parent
1810d802b4
commit
0fef027879
2 files changed
+75
-35
No files matched your search
+16
-12
@@ -15,24 +15,28 @@ class ExitRequest(MinerException):
|
|||||||
|
|
||||||
Intended for internal use only.
|
Intended for internal use only.
|
||||||
"""
|
"""
|
||||||
def __init__(self, *args: object):
|
def __init__(self):
|
||||||
if args:
|
super().__init__("Application was requested to exit")
|
||||||
super().__init__(*args)
|
|
||||||
else:
|
|
||||||
super().__init__("Application was requested to exit")
|
|
||||||
|
|
||||||
|
|
||||||
class ReloadRequest(MinerException):
|
class ReloadRequest(MinerException):
|
||||||
"""
|
"""
|
||||||
Raised when the application is requested to reload by an expired request.
|
Raised when the application is requested to reload entirely, without closing the GUI.
|
||||||
|
|
||||||
Intended for internal use only.
|
Intended for internal use only.
|
||||||
"""
|
"""
|
||||||
def __init__(self, *args: object):
|
def __init__(self):
|
||||||
if args:
|
super().__init__("Application was requested to reload entirely")
|
||||||
super().__init__(*args)
|
|
||||||
else:
|
|
||||||
super().__init__("Application was requested to reload its state")
|
class RequestInvalid(MinerException):
|
||||||
|
"""
|
||||||
|
Raised when a request becomes no longer valid inside its retry loop.
|
||||||
|
|
||||||
|
Intended for internal use only.
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__("Request became invalid during its retry loop")
|
||||||
|
|
||||||
|
|
||||||
class RequestException(MinerException):
|
class RequestException(MinerException):
|
||||||
@@ -76,7 +80,7 @@ class LoginException(RequestException):
|
|||||||
|
|
||||||
class CaptchaRequired(LoginException):
|
class CaptchaRequired(LoginException):
|
||||||
"""
|
"""
|
||||||
The most dreaded thing about automated scripts.
|
The most dreaded thing about automated scripts...
|
||||||
"""
|
"""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__("Captcha is required")
|
super().__init__("Captcha is required")
|
||||||
@@ -21,7 +21,9 @@ from gui import GUIManager
|
|||||||
from channel import Channel
|
from channel import Channel
|
||||||
from websocket import WebsocketPool
|
from websocket import WebsocketPool
|
||||||
from inventory import DropsCampaign
|
from inventory import DropsCampaign
|
||||||
from exceptions import MinerException, LoginException, CaptchaRequired, ExitRequest, ReloadRequest
|
from exceptions import (
|
||||||
|
MinerException, LoginException, CaptchaRequired, ExitRequest, ReloadRequest, RequestInvalid
|
||||||
|
)
|
||||||
from utils import (
|
from utils import (
|
||||||
CHARS_HEX_LOWER,
|
CHARS_HEX_LOWER,
|
||||||
timestamp,
|
timestamp,
|
||||||
@@ -69,10 +71,15 @@ class _AuthState:
|
|||||||
self.client_version: str
|
self.client_version: str
|
||||||
self.integrity_token: str
|
self.integrity_token: str
|
||||||
self.integrity_expires: datetime
|
self.integrity_expires: datetime
|
||||||
|
self.integrity_request: datetime
|
||||||
|
self.integrity_response: datetime
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def integrity_expired(self) -> bool:
|
def integrity_expired(self) -> bool:
|
||||||
return datetime.now(timezone.utc) >= self.integrity_expires
|
return (
|
||||||
|
hasattr(self, "integrity_expires")
|
||||||
|
and datetime.now(timezone.utc) >= self.integrity_expires
|
||||||
|
)
|
||||||
|
|
||||||
def _delattr(self, attr: str) -> None:
|
def _delattr(self, attr: str) -> None:
|
||||||
if hasattr(self, attr):
|
if hasattr(self, attr):
|
||||||
@@ -187,11 +194,11 @@ class _AuthState:
|
|||||||
headers["Client-Integrity"] = self.integrity_token
|
headers["Client-Integrity"] = self.integrity_token
|
||||||
return headers
|
return headers
|
||||||
|
|
||||||
async def verify(self):
|
async def validate(self):
|
||||||
async with self._lock:
|
async with self._lock:
|
||||||
await self._verify()
|
await self._validate()
|
||||||
|
|
||||||
async def _verify(self):
|
async def _validate(self):
|
||||||
if not hasattr(self, "session_id"):
|
if not hasattr(self, "session_id"):
|
||||||
self.session_id = create_nonce(CHARS_HEX_LOWER, 16)
|
self.session_id = create_nonce(CHARS_HEX_LOWER, 16)
|
||||||
if not (hasattr(self, "client_version") and hasattr(self, "device_id")):
|
if not (hasattr(self, "client_version") and hasattr(self, "device_id")):
|
||||||
@@ -247,16 +254,20 @@ class _AuthState:
|
|||||||
jar.update_cookies(cookie, url)
|
jar.update_cookies(cookie, url)
|
||||||
jar.save(COOKIES_PATH)
|
jar.save(COOKIES_PATH)
|
||||||
if not hasattr(self, "integrity_token") or self.integrity_expired:
|
if not hasattr(self, "integrity_token") or self.integrity_expired:
|
||||||
|
self.integrity_request = datetime.now(timezone.utc)
|
||||||
async with self._twitch.request(
|
async with self._twitch.request(
|
||||||
"POST",
|
"POST",
|
||||||
"https://gql.twitch.tv/integrity",
|
"https://gql.twitch.tv/integrity",
|
||||||
headers=self.gql_headers(integrity=False)
|
headers=self.gql_headers(integrity=False)
|
||||||
) as response:
|
) as response:
|
||||||
|
self.integrity_response = datetime.now(timezone.utc)
|
||||||
response_json: JsonType = await response.json()
|
response_json: JsonType = await response.json()
|
||||||
self.integrity_token = cast(str, response_json["token"])
|
self.integrity_token = cast(str, response_json["token"])
|
||||||
self.integrity_expires = datetime.fromtimestamp(
|
# (i = Math.round(.9 * (n.expiration - Date.now()))) < 0
|
||||||
response_json["expiration"] / 1000, timezone.utc
|
# || (this.logger.debug("Refreshing in ".concat(Math.round(i / 1e3 / 60), " minutes"))
|
||||||
)
|
now = datetime.now(timezone.utc)
|
||||||
|
expiration = datetime.fromtimestamp(response_json["expiration"] / 1000, timezone.utc)
|
||||||
|
self.integrity_expires = ((expiration - now) * 0.9) + now
|
||||||
# verify the integrity token's contents for the "is_bad_bot" flag
|
# verify the integrity token's contents for the "is_bad_bot" flag
|
||||||
stripped_token: str = self.integrity_token.split('.')[2] + "=="
|
stripped_token: str = self.integrity_token.split('.')[2] + "=="
|
||||||
messy_json: str = urlsafe_b64decode(
|
messy_json: str = urlsafe_b64decode(
|
||||||
@@ -273,6 +284,12 @@ class _AuthState:
|
|||||||
)
|
)
|
||||||
self._logged_in.set()
|
self._logged_in.set()
|
||||||
|
|
||||||
|
def invalidate(self, *, auth: bool = False, integrity: bool = False):
|
||||||
|
if auth:
|
||||||
|
self._delattr("access_token")
|
||||||
|
if integrity:
|
||||||
|
self.integrity_expires = datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
class Twitch:
|
class Twitch:
|
||||||
def __init__(self, settings: Settings):
|
def __init__(self, settings: Settings):
|
||||||
@@ -962,7 +979,7 @@ class Twitch:
|
|||||||
self.gui.print(_("status", "claimed_points").format(points=points))
|
self.gui.print(_("status", "claimed_points").format(points=points))
|
||||||
|
|
||||||
async def get_auth(self) -> _AuthState:
|
async def get_auth(self) -> _AuthState:
|
||||||
await self._auth_state.verify()
|
await self._auth_state.validate()
|
||||||
return self._auth_state
|
return self._auth_state
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
@@ -985,7 +1002,7 @@ class Twitch:
|
|||||||
# account for the expiration landing during the request
|
# account for the expiration landing during the request
|
||||||
and datetime.now(timezone.utc) >= (invalidate_after - session_timeout)
|
and datetime.now(timezone.utc) >= (invalidate_after - session_timeout)
|
||||||
):
|
):
|
||||||
raise ReloadRequest()
|
raise RequestInvalid()
|
||||||
try:
|
try:
|
||||||
response: aiohttp.ClientResponse | None = None
|
response: aiohttp.ClientResponse | None = None
|
||||||
done, pending = await asyncio.wait(
|
done, pending = await asyncio.wait(
|
||||||
@@ -1018,19 +1035,38 @@ class Twitch:
|
|||||||
|
|
||||||
async def gql_request(self, op: GQLOperation) -> JsonType:
|
async def gql_request(self, op: GQLOperation) -> JsonType:
|
||||||
gql_logger.debug(f"GQL Request: {op}")
|
gql_logger.debug(f"GQL Request: {op}")
|
||||||
auth_state = await self.get_auth()
|
while True:
|
||||||
async with self.request(
|
try:
|
||||||
"POST",
|
auth_state = await self.get_auth()
|
||||||
"https://gql.twitch.tv/gql",
|
async with self.request(
|
||||||
json=op,
|
"POST",
|
||||||
headers=auth_state.gql_headers(integrity=True),
|
"https://gql.twitch.tv/gql",
|
||||||
invalidate_after=auth_state.integrity_expires,
|
json=op,
|
||||||
) as response:
|
headers=auth_state.gql_headers(integrity=True),
|
||||||
response_json: JsonType = await response.json()
|
invalidate_after=auth_state.integrity_expires,
|
||||||
gql_logger.debug(f"GQL Response: {response_json}")
|
) as response:
|
||||||
if "errors" in response_json and response_json["errors"]:
|
response_json: JsonType = await response.json()
|
||||||
raise MinerException(f"GQL error: {response_json['errors']}")
|
except RequestInvalid:
|
||||||
return response_json
|
continue
|
||||||
|
gql_logger.debug(f"GQL Response: {response_json}")
|
||||||
|
if "errors" in response_json and response_json["errors"]:
|
||||||
|
errors_list = response_json["errors"]
|
||||||
|
if (
|
||||||
|
len(errors_list) == 1
|
||||||
|
and all("message" in error_dict for error_dict in errors_list)
|
||||||
|
and errors_list[0]["message"] == "failed integrity check"
|
||||||
|
):
|
||||||
|
# auth_state.invalidate(integrity=True)
|
||||||
|
# continue
|
||||||
|
raise MinerException(
|
||||||
|
"GQL integrity error: "
|
||||||
|
f"now={datetime.now(timezone.utc).replace(tzinfo=None)!s}, "
|
||||||
|
f"request={auth_state.integrity_request.replace(tzinfo=None)!s}, "
|
||||||
|
f"response={auth_state.integrity_response.replace(tzinfo=None)!s}, "
|
||||||
|
f"expires={auth_state.integrity_expires.replace(tzinfo=None)!s}"
|
||||||
|
)
|
||||||
|
raise MinerException(f"GQL error: {errors_list}")
|
||||||
|
return response_json
|
||||||
|
|
||||||
async def fetch_campaign(
|
async def fetch_campaign(
|
||||||
self,
|
self,
|
||||||
|
|||||||
Reference in new issue
Block a user