From be39a5344f1c8305de29618c6af410f21e76fcac Mon Sep 17 00:00:00 2001 From: Younes Aassila <47226184+younesaassila@users.noreply.github.com> Date: Sun, 2 Feb 2025 17:22:48 +0100 Subject: [PATCH] Fix auto whitelist subs feature - The token in the Usher req lies about the sub status (always false) --- src/page/getFetch.ts | 144 ++++++++++++++++++++++++++++++++----------- src/types.ts | 2 + 2 files changed, 111 insertions(+), 35 deletions(-) diff --git a/src/page/getFetch.ts b/src/page/getFetch.ts index 1e76f87..437953c 100644 --- a/src/page/getFetch.ts +++ b/src/page/getFetch.ts @@ -44,14 +44,28 @@ export function getFetch(pageState: PageState): typeof fetch { cachedPlaybackTokenRequestHeaders, cachedPlaybackTokenRequestBody ); - const message = { + pageState.sendMessageToWorkerScripts(pageState.twitchWorkers, { type: MessageType.NewPlaybackAccessTokenResponse, newPlaybackAccessToken, - }; - pageState.sendMessageToWorkerScripts( - pageState.twitchWorkers, - message - ); + }); + break; + case MessageType.ChannelSubStatusQuery: + try { + const req = getSubStatusRequest(message.channelName); + const res = await NATIVE_FETCH(req); + const body = await res.json(); + const isSubscribed = + body.data.user.self.subscriptionBenefit != null; + pageState.sendMessageToWorkerScripts(pageState.twitchWorkers, { + type: MessageType.ChannelSubStatusQueryResponse, + isSubscribed, + }); + } catch (error) { + pageState.sendMessageToWorkerScripts(pageState.twitchWorkers, { + type: MessageType.ChannelSubStatusQueryResponse, + error: `${error}`, + }); + } break; } }); @@ -199,7 +213,7 @@ export function getFetch(pageState: PageState): typeof fetch { pageState.state?.anonymousMode === true || (shouldFlagRequest && willFailIntegrityCheckIfProxied); if (shouldOverrideRequest) { - const newRequest = await getDefaultPlaybackAccessTokenRequest( + const newRequest = getDefaultPlaybackAccessTokenRequest( channelName, pageState.state?.anonymousMode === true ); @@ -273,31 +287,8 @@ export function getFetch(pageState: PageState): typeof fetch { isLivestream && channelName != null ) { - const wasSubscribed = wasChannelSubscriber(channelName, pageState); - const isSubscribed = url.includes( - encodeURIComponent('"subscriber":true') - ); - const hasSubStatusChanged = - (wasSubscribed && !isSubscribed) || (!wasSubscribed && isSubscribed); - if (hasSubStatusChanged) { - try { - const response = - await pageState.sendMessageToContentScriptAndWaitForResponse( - pageState.scope, - { - type: MessageType.ChannelSubStatusChange, - channelName, - wasSubscribed, - isSubscribed, - }, - MessageType.ChannelSubStatusChangeResponse - ); - if (typeof response.whitelistedChannels === "object") { - pageState.state.whitelistedChannels = - response.whitelistedChannels; - } - } catch {} - } + // TODO: Maybe also check before PlaybackAccessToken requests? (But then that's a LOT of overhead.) + await checkChannelSubStatus(channelName, pageState); } const isWhitelisted = isChannelWhitelisted(channelName, pageState); if (!isLivestream || isFrontpage || isWhitelisted) { @@ -726,6 +717,89 @@ async function sleep(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } +function getSubStatusRequest(channelName: string): Request { + const cookieMap = new Map( + document.cookie + .split(";") + .map(cookie => cookie.trim().split("=")) + .map(([name, value]) => [name, decodeURIComponent(value)]) + ); + const headersMap = new Map([ + [ + "Authorization", + cookieMap.has("auth-token") + ? `OAuth ${cookieMap.get("auth-token")}` + : "undefined", + ], + ["Client-ID", "kimne78kx3ncx6brgo4mv6wki5h1ko"], + ["Device-ID", generateRandomString(32)], + ]); + + return new Request("https://gql.twitch.tv/gql", { + method: "POST", + headers: Object.fromEntries(headersMap), + body: JSON.stringify({ + operationName: "ChannelPage_SubscribeButton_User", + variables: { + login: channelName, + }, + extensions: { + persistedQuery: { + version: 1, + sha256Hash: + "a1da17caf3041632c3f9b4069dfc8d93ff10b5b5023307ec0a694a9d8eae991e", + }, + }, + }), + }); +} + +async function checkChannelSubStatus( + channelName: string, + pageState: PageState +) { + try { + const channelSubStatus = + await pageState.sendMessageToPageScriptAndWaitForResponse( + pageState.scope, + { + type: MessageType.ChannelSubStatusQuery, + channelName, + }, + MessageType.ChannelSubStatusQueryResponse + ); + if (!channelSubStatus || channelSubStatus.error) { + throw new Error( + `Error querying channel sub status: ${channelSubStatus.error}` + ); + } + const wasSubscribed = wasChannelSubscriber(channelName, pageState); + const isSubscribed = channelSubStatus.isSubscribed; + const hasSubStatusChanged = + (wasSubscribed && !isSubscribed) || (!wasSubscribed && isSubscribed); + if (hasSubStatusChanged) { + try { + const response = + await pageState.sendMessageToContentScriptAndWaitForResponse( + pageState.scope, + { + type: MessageType.ChannelSubStatusChange, + channelName, + wasSubscribed, + isSubscribed, + }, + MessageType.ChannelSubStatusChangeResponse + ); + if (typeof response.whitelistedChannels === "object") { + pageState.state!.whitelistedChannels = response.whitelistedChannels; + } + } catch {} + } + } catch (error) { + console.error("[TTV LOL PRO] Failed to check channel sub status:", error); + } +} + //#region Video Weaver URL replacement /** @@ -734,10 +808,10 @@ async function sleep(ms: number): Promise { * @param anonymousMode * @returns */ -async function getDefaultPlaybackAccessTokenRequest( +function getDefaultPlaybackAccessTokenRequest( channel: string | null = null, anonymousMode: boolean = false -): Promise { +): Request | null { // We can use `location.href` because we're in the page script. const channelName = channel ?? findChannelFromTwitchTvUrl(location.href); if (!channelName) return null; @@ -793,7 +867,7 @@ async function fetchReplacementPlaybackAccessToken( ): Promise { // Not using the cached request because we'd need to check if integrity requests are proxied. try { - let request = await getDefaultPlaybackAccessTokenRequest( + let request = getDefaultPlaybackAccessTokenRequest( null, pageState.state?.anonymousMode === true ); diff --git a/src/types.ts b/src/types.ts index 2d924e8..43b0aa8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -79,6 +79,8 @@ export const enum MessageType { EnableFullMode = "TLP_EnableFullMode", EnableFullModeResponse = "TLP_EnableFullModeResponse", DisableFullMode = "TLP_DisableFullMode", + ChannelSubStatusQuery = "TLP_ChannelSubStatusQuery", + ChannelSubStatusQueryResponse = "TLP_ChannelSubStatusQueryResponse", ChannelSubStatusChange = "TLP_ChannelSubStatusChange", ChannelSubStatusChangeResponse = "TLP_ChannelSubStatusChangeResponse", UsherResponse = "TLP_UsherResponse",