diff --git a/src/common/ts/regexes.ts b/src/common/ts/regexes.ts index 53cbc15..f9fbef7 100644 --- a/src/common/ts/regexes.ts +++ b/src/common/ts/regexes.ts @@ -1,7 +1,7 @@ export const passportHostRegex = /^passport\.twitch\.tv$/i; export const twitchApiChannelNameRegex = /\/hls\/(.+)\.m3u8/i; export const twitchChannelNameRegex = - /^https?:\/\/(?:www|m)\.twitch\.tv\/(?:videos\/|popout\/|moderator\/)?((?!(?:directory|jobs|p|privacy|store|turbo)\b)\w+)/i; + /^https?:\/\/(?:www|m)\.twitch\.tv\/(?:videos\/|popout\/|moderator\/)?((?!(?:directory|downloads|jobs|p|privacy|search|settings|store|turbo)\b)\w+)/i; export const twitchGqlHostRegex = /^gql\.twitch\.tv$/i; export const twitchTvHostRegex = /^(?:www|m)\.twitch\.tv$/i; export const usherHostRegex = /^usher\.ttvnw\.net$/i; diff --git a/src/common/ts/wasChannelSubscriber.ts b/src/common/ts/wasChannelSubscriber.ts deleted file mode 100644 index 672bf2c..0000000 --- a/src/common/ts/wasChannelSubscriber.ts +++ /dev/null @@ -1,11 +0,0 @@ -import store from "../../store"; - -export default function wasChannelSubscriber( - channelName: string | null -): boolean { - if (!channelName) return false; - const channelNameLower = channelName.toLowerCase(); - return store.state.activeChannelSubscriptions.some( - c => c.toLowerCase() === channelNameLower - ); -} diff --git a/src/content/content.ts b/src/content/content.ts index 9445e20..e660ae1 100644 --- a/src/content/content.ts +++ b/src/content/content.ts @@ -158,6 +158,7 @@ function onPageMessage(event: MessageEvent) { console.log(`[TTV LOL PRO] Adding '${channelName}' to whitelist.`); store.state.whitelistedChannels.push(channelName); } + location.reload(); } else if (wasSubscribed && !isSubscribed) { store.state.activeChannelSubscriptions = store.state.activeChannelSubscriptions.filter( @@ -173,17 +174,9 @@ function onPageMessage(event: MessageEvent) { c => c.toLowerCase() !== channelName.toLowerCase() ); } + location.reload(); } } - window.postMessage({ - type: responseType, - message: { - type: responseMessageType, - whitelistedChannels: JSON.parse( - JSON.stringify(store.state.whitelistedChannels) - ), - }, - }); } // --- else if (message.type === MessageType.UsherResponse) { diff --git a/src/page/getFetch.ts b/src/page/getFetch.ts index 437953c..cfb6bc6 100644 --- a/src/page/getFetch.ts +++ b/src/page/getFetch.ts @@ -49,24 +49,6 @@ export function getFetch(pageState: PageState): typeof fetch { newPlaybackAccessToken, }); 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; } }); } @@ -282,14 +264,6 @@ export function getFetch(pageState: PageState): typeof fetch { encodeURIComponent('"player_type":"frontpage"') ); const channelName = findChannelFromUsherUrl(url); - if ( - pageState.state?.whitelistChannelSubscriptions && - isLivestream && - channelName != null - ) { - // 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) { console.log( @@ -425,6 +399,65 @@ export function getFetch(pageState: PageState): typeof fetch { //#region Responses + graphqlRes: if ( + host != null && + twitchGqlHostRegex.test(host) && + response.status < 400 + ) { + responseBody ??= await readResponseBody(); + // Preliminary check to avoid parsing the response body if possible. + if ( + !responseBody.includes('"UserSelfConnection"') || + !responseBody.includes('"subscriptionBenefit"') || + !responseBody.includes('"login"') + ) { + break graphqlRes; + } + try { + let channelName: string; + let isSubscribed: boolean; + const body = JSON.parse(responseBody); + if (Array.isArray(body)) { + const match = body.find( + (obj: any) => + obj.data && + obj.data.user && + obj.data.user.login != null && + obj.data.user.self && + "subscriptionBenefit" in obj.data.user.self + ); + if (match == null) break graphqlRes; + channelName = match.data.user.login; + isSubscribed = match.data.user.self.subscriptionBenefit != null; + } else { + const isMatch = + body.data && + body.data.user && + body.data.user.login != null && + body.data.user.self && + "subscriptionBenefit" in body.data.user.self; + if (!isMatch) break graphqlRes; + channelName = body.data.user.login; + isSubscribed = body.data.user.self.subscriptionBenefit != null; + } + const isLivestream = !/^\d+$/.test(channelName); // VODs have numeric IDs. + if (!isLivestream) break graphqlRes; + const wasSubscribed = wasChannelSubscriber(channelName, pageState); + const hasSubStatusChanged = + (wasSubscribed && !isSubscribed) || (!wasSubscribed && isSubscribed); + if (hasSubStatusChanged) { + pageState.sendMessageToContentScript({ + type: MessageType.ChannelSubStatusChange, + channelName, + wasSubscribed, + isSubscribed, + }); + } + } catch (error) { + console.error("[TTV LOL PRO] Failed to parse GraphQL response:", error); + } + } + // Twitch Usher responses. usherRes: if ( host != null && @@ -717,89 +750,6 @@ 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 /** diff --git a/src/page/page.ts b/src/page/page.ts index b7619ac..694c963 100644 --- a/src/page/page.ts +++ b/src/page/page.ts @@ -39,6 +39,7 @@ const pageState: PageState = { sendMessageToWorkerScriptsAndWaitForResponse, }; +const NATIVE_FETCH = window.fetch; window.fetch = getFetch(pageState); const NATIVE_WORKER = window.Worker;