Improve implementation of auto whitelist feature

This commit is contained in:
Younes Aassila 2025-02-04 14:52:18 +01:00
parent be39a5344f
commit ac50e0c8b2
No known key found for this signature in database
5 changed files with 63 additions and 130 deletions

View File

@ -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;

View File

@ -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
);
}

View File

@ -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) {

View File

@ -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<void> {
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
/**

View File

@ -39,6 +39,7 @@ const pageState: PageState = {
sendMessageToWorkerScriptsAndWaitForResponse,
};
const NATIVE_FETCH = window.fetch;
window.fetch = getFetch(pageState);
const NATIVE_WORKER = window.Worker;