Implement a way to send watch events through GQL

This commit is contained in:
DevilXD committed 2026-04-28 20:25:21 +02:00
1 parent cac9b29587
commit 40000cf295
5 files changed
+98 -38

No files matched your search

+50 -9
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
import re
import gzip
import json
import asyncio
import logging
@@ -11,14 +12,14 @@ from typing import Any, SupportsInt, cast, TYPE_CHECKING
import aiohttp
from yarl import URL
from utils import Game, json_minify
from utils import Game, json_minify, isonow
from exceptions import MinerException, RequestException
from constants import CALL, GQL_OPERATIONS, ONLINE_DELAY, URLType
from constants import CALL, GQL_QUERIES, ONLINE_DELAY, URLType, GQLQuery
if TYPE_CHECKING:
from twitch import Twitch
from gui import ChannelList
from constants import JsonType, GQLOperation
from constants import JsonType, GQLPersistedQuery
logger = logging.getLogger("TwitchDrops")
@@ -63,6 +64,36 @@ class Stream:
]
return {"data": (b64encode(json_minify(payload).encode("utf8"))).decode("utf8")}
@cached_property
def _gql_payload(self) -> GQLQuery:
payload = [
{
"event": "minute-watched",
"properties": {
"broadcast_id": str(self.broadcast_id),
"channel_id": str(self.channel.id),
"channel": self.channel._login,
"client_time": isonow(),
"game": self.game.name if self.game is not None else "",
"game_id": str(self.game.id) if self.game is not None else "",
"hidden": False,
"is_live": True,
"live": True,
"logged_in": True,
"minutes_logged": 1,
"muted": False,
"user_id": self.channel._twitch._auth_state.user_id,
}
}
]
return GQLQuery(
(
"\n mutation SendEvents($input: SendSpadeEventsInput!) "
"{\n sendSpadeEvents(input: $input) {\n statusCode\n}\n}\n"
),
b64encode(gzip.compress(json_minify(payload).encode("utf8"))).decode("utf8")
)
@classmethod
def from_get_stream(cls, channel: Channel, channel_data: JsonType) -> Stream:
stream = channel_data["stream"]
@@ -99,7 +130,7 @@ class Stream:
return self._stream_url
# get the stream playback access token from GQL
playback_token_response: JsonType = await self.channel._twitch.gql_request(
GQL_OPERATIONS["PlaybackAccessToken"].with_variables({"login": self.channel._login})
GQL_QUERIES["PlaybackAccessToken"].with_variables({"login": self.channel._login})
)
token_data: JsonType = playback_token_response["data"]["streamPlaybackAccessToken"]
token_value = token_data["value"]
@@ -203,8 +234,8 @@ class Channel:
return self.id
@property
def stream_gql(self) -> GQLOperation:
return GQL_OPERATIONS["GetStreamInfo"].with_variables({"channel": self._login})
def stream_gql(self) -> GQLPersistedQuery:
return GQL_QUERIES["GetStreamInfo"].with_variables({"channel": self._login})
@property
def name(self) -> str:
@@ -342,7 +373,7 @@ class Channel:
if not stream.drops_enabled:
try:
available_drops_campaigns: JsonType = await self._twitch.gql_request(
GQL_OPERATIONS["AvailableDrops"].with_variables({"channelID": str(self.id)})
GQL_QUERIES["AvailableDrops"].with_variables({"channelID": str(self.id)})
)
except MinerException:
logger.log(CALL, f"AvailableDrops GQL call failed for channel: {self._login}")
@@ -408,7 +439,7 @@ class Channel:
self.display()
# NOTE: This is currently unused.
async def _send_watch(self) -> bool:
async def _send_watch_playlist(self) -> bool:
"""
This performs a HEAD request on the stream's current playlist,
to simulate watching the stream.
@@ -460,7 +491,8 @@ class Channel:
async with self._twitch.request("HEAD", stream_chunk_url) as head_response:
return head_response.status == 200
async def send_watch(self) -> bool:
# NOTE: This is currently unused.
async def _send_watch_spade(self) -> bool:
if self._stream is None:
return False
if self._spade_url is None:
@@ -472,3 +504,12 @@ class Channel:
return response.status == 204
except RequestException:
return False
async def send_watch(self) -> bool: # send_watch_gql
if self._stream is None:
return False
try:
watch_response = await self._twitch.gql_request(self._stream._gql_payload)
return watch_response["data"]["sendSpadeEvents"]["statusCode"] == 204
except RequestException:
return False