Fix auto whitelist subs feature

- The token in the Usher req lies about the sub status (always false)
This commit is contained in:
Younes Aassila 2025-02-02 17:22:48 +01:00
parent 2a79647fae
commit be39a5344f
No known key found for this signature in database
2 changed files with 111 additions and 35 deletions

View File

@ -44,14 +44,28 @@ export function getFetch(pageState: PageState): typeof fetch {
cachedPlaybackTokenRequestHeaders, cachedPlaybackTokenRequestHeaders,
cachedPlaybackTokenRequestBody cachedPlaybackTokenRequestBody
); );
const message = { pageState.sendMessageToWorkerScripts(pageState.twitchWorkers, {
type: MessageType.NewPlaybackAccessTokenResponse, type: MessageType.NewPlaybackAccessTokenResponse,
newPlaybackAccessToken, newPlaybackAccessToken,
}; });
pageState.sendMessageToWorkerScripts( break;
pageState.twitchWorkers, case MessageType.ChannelSubStatusQuery:
message 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; break;
} }
}); });
@ -199,7 +213,7 @@ export function getFetch(pageState: PageState): typeof fetch {
pageState.state?.anonymousMode === true || pageState.state?.anonymousMode === true ||
(shouldFlagRequest && willFailIntegrityCheckIfProxied); (shouldFlagRequest && willFailIntegrityCheckIfProxied);
if (shouldOverrideRequest) { if (shouldOverrideRequest) {
const newRequest = await getDefaultPlaybackAccessTokenRequest( const newRequest = getDefaultPlaybackAccessTokenRequest(
channelName, channelName,
pageState.state?.anonymousMode === true pageState.state?.anonymousMode === true
); );
@ -273,31 +287,8 @@ export function getFetch(pageState: PageState): typeof fetch {
isLivestream && isLivestream &&
channelName != null channelName != null
) { ) {
const wasSubscribed = wasChannelSubscriber(channelName, pageState); // TODO: Maybe also check before PlaybackAccessToken requests? (But then that's a LOT of overhead.)
const isSubscribed = url.includes( await checkChannelSubStatus(channelName, pageState);
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 {}
}
} }
const isWhitelisted = isChannelWhitelisted(channelName, pageState); const isWhitelisted = isChannelWhitelisted(channelName, pageState);
if (!isLivestream || isFrontpage || isWhitelisted) { if (!isLivestream || isFrontpage || isWhitelisted) {
@ -726,6 +717,89 @@ async function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms)); return new Promise(resolve => setTimeout(resolve, ms));
} }
function getSubStatusRequest(channelName: string): Request {
const cookieMap = new Map<string, string>(
document.cookie
.split(";")
.map(cookie => cookie.trim().split("="))
.map(([name, value]) => [name, decodeURIComponent(value)])
);
const headersMap = new Map<string, string>([
[
"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 //#region Video Weaver URL replacement
/** /**
@ -734,10 +808,10 @@ async function sleep(ms: number): Promise<void> {
* @param anonymousMode * @param anonymousMode
* @returns * @returns
*/ */
async function getDefaultPlaybackAccessTokenRequest( function getDefaultPlaybackAccessTokenRequest(
channel: string | null = null, channel: string | null = null,
anonymousMode: boolean = false anonymousMode: boolean = false
): Promise<Request | null> { ): Request | null {
// We can use `location.href` because we're in the page script. // We can use `location.href` because we're in the page script.
const channelName = channel ?? findChannelFromTwitchTvUrl(location.href); const channelName = channel ?? findChannelFromTwitchTvUrl(location.href);
if (!channelName) return null; if (!channelName) return null;
@ -793,7 +867,7 @@ async function fetchReplacementPlaybackAccessToken(
): Promise<PlaybackAccessToken | null> { ): Promise<PlaybackAccessToken | null> {
// Not using the cached request because we'd need to check if integrity requests are proxied. // Not using the cached request because we'd need to check if integrity requests are proxied.
try { try {
let request = await getDefaultPlaybackAccessTokenRequest( let request = getDefaultPlaybackAccessTokenRequest(
null, null,
pageState.state?.anonymousMode === true pageState.state?.anonymousMode === true
); );

View File

@ -79,6 +79,8 @@ export const enum MessageType {
EnableFullMode = "TLP_EnableFullMode", EnableFullMode = "TLP_EnableFullMode",
EnableFullModeResponse = "TLP_EnableFullModeResponse", EnableFullModeResponse = "TLP_EnableFullModeResponse",
DisableFullMode = "TLP_DisableFullMode", DisableFullMode = "TLP_DisableFullMode",
ChannelSubStatusQuery = "TLP_ChannelSubStatusQuery",
ChannelSubStatusQueryResponse = "TLP_ChannelSubStatusQueryResponse",
ChannelSubStatusChange = "TLP_ChannelSubStatusChange", ChannelSubStatusChange = "TLP_ChannelSubStatusChange",
ChannelSubStatusChangeResponse = "TLP_ChannelSubStatusChangeResponse", ChannelSubStatusChangeResponse = "TLP_ChannelSubStatusChangeResponse",
UsherResponse = "TLP_UsherResponse", UsherResponse = "TLP_UsherResponse",