🔖 Release version 2.3.0
28
README.md
@ -1,5 +1,5 @@
|
|||||||
<h1 align="center">
|
<h1 align="center">
|
||||||
<img alt="Icon" src="src/images/brand/icon.png" height="100" width="100" />
|
<img alt="Icon" src="src/common/images/brand/icon.png" height="100" width="100" />
|
||||||
<br />
|
<br />
|
||||||
TTV LOL PRO
|
TTV LOL PRO
|
||||||
<br />
|
<br />
|
||||||
@ -48,14 +48,14 @@
|
|||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="Chrome Web Store"
|
alt="Chrome Web Store"
|
||||||
src="src/images/badges/chrome_web_store.png"
|
src="src/common/images/badges/chrome_web_store.png"
|
||||||
height="50"
|
height="50"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://addons.mozilla.org/addon/ttv-lol-pro/">
|
<a href="https://addons.mozilla.org/addon/ttv-lol-pro/">
|
||||||
<img
|
<img
|
||||||
alt="Firefox Add-ons"
|
alt="Firefox Add-ons"
|
||||||
src="src/images/badges/firefox_addons.png"
|
src="src/common/images/badges/firefox_addons.png"
|
||||||
height="50"
|
height="50"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
@ -65,25 +65,19 @@
|
|||||||
|
|
||||||
> ℹ️ Looking for TTV LOL PRO v1? [Click here](https://github.com/younesaassila/ttv-lol-pro/tree/v1).
|
> ℹ️ Looking for TTV LOL PRO v1? [Click here](https://github.com/younesaassila/ttv-lol-pro/tree/v1).
|
||||||
|
|
||||||
TTV LOL PRO removes _most_ livestream ads from Twitch. This is free, don't expect it to be perfect. Issues? Complain to Twitch
|
TTV LOL PRO removes most livestream ads from Twitch. This is free, don't expect it to be perfect.
|
||||||
|
|
||||||
**TTV LOL PRO:**
|
TTV LOL PRO is a fork of TTV LOL that:
|
||||||
|
|
||||||
- removes _most_ livestream ads from Twitch,
|
- uses an improved ad blocking method,
|
||||||
|
- uses standard HTTP proxies (thus improving proxy compatibility and your privacy),
|
||||||
|
- adds a stream status widget to the popup,
|
||||||
- lets you whitelist channels,
|
- lets you whitelist channels,
|
||||||
- improves TTV LOL's popup by showing stream status,
|
- lets you use your own proxies.
|
||||||
- lets you add custom primary/fallback proxies.
|
|
||||||
|
|
||||||
**Recommended:**
|
TTV LOL PRO does not remove banner ads, nor does it remove ads from VODs. For the best experience, we recommend using [uBlock Origin](https://ublockorigin.com/) alongside TTV LOL PRO.
|
||||||
|
|
||||||
- [uBlock Origin](https://ublockorigin.com/)
|
**Any questions? Please read the [FAQ](FAQ.md).**
|
||||||
|
|
||||||
- removes banner ads,
|
|
||||||
- removes ads on VODs.
|
|
||||||
|
|
||||||
**Frequently Asked Questions (FAQ):**
|
|
||||||
|
|
||||||
- [Click here](FAQ.md)
|
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
|
1323
package-lock.json
generated
17
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ttv-lol-pro",
|
"name": "ttv-lol-pro",
|
||||||
"version": "2.2.3",
|
"version": "2.3.0",
|
||||||
"description": "TTV LOL PRO removes most livestream ads from Twitch.",
|
"description": "TTV LOL PRO removes most livestream ads from Twitch.",
|
||||||
"@parcel/bundler-default": {
|
"@parcel/bundler-default": {
|
||||||
"minBundles": 10000000,
|
"minBundles": 10000000,
|
||||||
@ -36,24 +36,25 @@
|
|||||||
"web-extension",
|
"web-extension",
|
||||||
"adblocker"
|
"adblocker"
|
||||||
],
|
],
|
||||||
"author": "TTV-LOL (https://github.com/TTV-LOL)",
|
"author": "Younes Aassila (https://github.com/younesaassila)",
|
||||||
"contributors": [
|
"contributors": [
|
||||||
"Younes Aassila (https://github.com/younesaassila)"
|
"Marc Gómez (https://github.com/zGato)"
|
||||||
],
|
],
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bowser": "^2.11.0",
|
"bowser": "^2.11.0",
|
||||||
"ip": "^1.1.8"
|
"ip": "^1.1.8",
|
||||||
|
"m3u8-parser": "^7.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@parcel/config-webextension": "^2.10.3",
|
"@parcel/config-webextension": "^2.11.0",
|
||||||
"@types/chrome": "^0.0.254",
|
"@types/chrome": "^0.0.259",
|
||||||
"@types/ip": "^1.1.3",
|
"@types/ip": "^1.1.3",
|
||||||
"@types/webextension-polyfill": "^0.10.7",
|
"@types/webextension-polyfill": "^0.10.7",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"os-browserify": "^0.3.0",
|
"os-browserify": "^0.3.0",
|
||||||
"parcel": "^2.10.3",
|
"parcel": "^2.11.0",
|
||||||
"postcss": "^8.4.32",
|
"postcss": "^8.4.33",
|
||||||
"prettier": "2.8.8",
|
"prettier": "2.8.8",
|
||||||
"prettier-plugin-css-order": "^1.3.1",
|
"prettier-plugin-css-order": "^1.3.1",
|
||||||
"prettier-plugin-organize-imports": "^3.2.4",
|
"prettier-plugin-organize-imports": "^3.2.4",
|
||||||
|
@ -3,9 +3,11 @@ import isChromium from "../common/ts/isChromium";
|
|||||||
import checkForOpenedTwitchTabs from "./handlers/checkForOpenedTwitchTabs";
|
import checkForOpenedTwitchTabs from "./handlers/checkForOpenedTwitchTabs";
|
||||||
import onAuthRequired from "./handlers/onAuthRequired";
|
import onAuthRequired from "./handlers/onAuthRequired";
|
||||||
import onBeforeSendHeaders from "./handlers/onBeforeSendHeaders";
|
import onBeforeSendHeaders from "./handlers/onBeforeSendHeaders";
|
||||||
|
import onBeforeTwitchTvSendHeaders from "./handlers/onBeforeTwitchTvSendHeaders";
|
||||||
import onBeforeVideoWeaverRequest from "./handlers/onBeforeVideoWeaverRequest";
|
import onBeforeVideoWeaverRequest from "./handlers/onBeforeVideoWeaverRequest";
|
||||||
|
import onContentScriptMessage from "./handlers/onContentScriptMessage";
|
||||||
|
import onInstalledStoreCleanup from "./handlers/onInstalledStoreCleanup";
|
||||||
import onProxyRequest from "./handlers/onProxyRequest";
|
import onProxyRequest from "./handlers/onProxyRequest";
|
||||||
import onProxySettingsChange from "./handlers/onProxySettingsChanged";
|
|
||||||
import onResponseStarted from "./handlers/onResponseStarted";
|
import onResponseStarted from "./handlers/onResponseStarted";
|
||||||
import onStartupStoreCleanup from "./handlers/onStartupStoreCleanup";
|
import onStartupStoreCleanup from "./handlers/onStartupStoreCleanup";
|
||||||
import onTabCreated from "./handlers/onTabCreated";
|
import onTabCreated from "./handlers/onTabCreated";
|
||||||
@ -15,7 +17,10 @@ import onTabUpdated from "./handlers/onTabUpdated";
|
|||||||
|
|
||||||
console.info("🚀 Background script loaded.");
|
console.info("🚀 Background script loaded.");
|
||||||
|
|
||||||
// Cleanup the session-related data in the store on startup.
|
// Cleanup old data in the store on update.
|
||||||
|
browser.runtime.onInstalled.addListener(onInstalledStoreCleanup);
|
||||||
|
|
||||||
|
// Cleanup session data in the store on startup.
|
||||||
browser.runtime.onStartup.addListener(onStartupStoreCleanup);
|
browser.runtime.onStartup.addListener(onStartupStoreCleanup);
|
||||||
|
|
||||||
// Handle proxy authentication.
|
// Handle proxy authentication.
|
||||||
@ -31,8 +36,8 @@ browser.webRequest.onResponseStarted.addListener(onResponseStarted, {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (isChromium) {
|
if (isChromium) {
|
||||||
// Listen to whether proxy is set or not.
|
// Listen to messages from the content script.
|
||||||
browser.proxy.settings.onChange.addListener(onProxySettingsChange);
|
browser.runtime.onMessage.addListener(onContentScriptMessage);
|
||||||
|
|
||||||
// Check if there are any opened Twitch tabs on startup.
|
// Check if there are any opened Twitch tabs on startup.
|
||||||
checkForOpenedTwitchTabs();
|
checkForOpenedTwitchTabs();
|
||||||
@ -43,15 +48,21 @@ if (isChromium) {
|
|||||||
browser.tabs.onRemoved.addListener(onTabRemoved);
|
browser.tabs.onRemoved.addListener(onTabRemoved);
|
||||||
browser.tabs.onReplaced.addListener(onTabReplaced);
|
browser.tabs.onReplaced.addListener(onTabReplaced);
|
||||||
} else {
|
} else {
|
||||||
|
// Inject page script.
|
||||||
|
browser.webRequest.onBeforeSendHeaders.addListener(
|
||||||
|
onBeforeTwitchTvSendHeaders,
|
||||||
|
{
|
||||||
|
urls: ["https://www.twitch.tv/*", "https://m.twitch.tv/*"],
|
||||||
|
types: ["main_frame"],
|
||||||
|
},
|
||||||
|
["blocking", "requestHeaders"]
|
||||||
|
);
|
||||||
|
|
||||||
// Block tracking pixels.
|
// Block tracking pixels.
|
||||||
browser.webRequest.onBeforeRequest.addListener(
|
browser.webRequest.onBeforeRequest.addListener(
|
||||||
() => ({ cancel: true }),
|
() => ({ cancel: true }),
|
||||||
{
|
{
|
||||||
urls: [
|
urls: ["https://*.twitch.tv/r/s/*", "https://*.twitch.tv/r/c/*"],
|
||||||
"https://*.twitch.tv/r/s/*",
|
|
||||||
"https://*.twitch.tv/r/c/*",
|
|
||||||
"https://*.ads.twitch.tv/*",
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
["blocking"]
|
["blocking"]
|
||||||
);
|
);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { WebRequest } from "webextension-polyfill";
|
import { WebRequest } from "webextension-polyfill";
|
||||||
import getProxyInfoFromUrl from "../../common/ts/getProxyInfoFromUrl";
|
import { getProxyInfoFromUrl } from "../../common/ts/proxyInfo";
|
||||||
import store from "../../store";
|
import store from "../../store";
|
||||||
|
|
||||||
const pendingRequests: string[] = [];
|
const pendingRequests: string[] = [];
|
||||||
|
44
src/background/handlers/onBeforeTwitchTvSendHeaders.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import pageScriptURL from "url:../../page/page.ts";
|
||||||
|
import workerScriptURL from "url:../../page/worker.ts";
|
||||||
|
import { WebRequest } from "webextension-polyfill";
|
||||||
|
import filterResponseDataWrapper from "../../common/ts/filterResponseDataWrapper";
|
||||||
|
import getHostFromUrl from "../../common/ts/getHostFromUrl";
|
||||||
|
import isChromium from "../../common/ts/isChromium";
|
||||||
|
import { twitchTvHostRegex } from "../../common/ts/regexes";
|
||||||
|
|
||||||
|
export default function onBeforeTwitchTvSendHeaders(
|
||||||
|
details: WebRequest.OnBeforeSendHeadersDetailsType
|
||||||
|
): void | WebRequest.BlockingResponseOrPromise {
|
||||||
|
const host = getHostFromUrl(details.url);
|
||||||
|
if (!host || !twitchTvHostRegex.test(host)) return;
|
||||||
|
|
||||||
|
// Ignore requests for non-HTML resources.
|
||||||
|
const acceptHeader = details.requestHeaders?.find(
|
||||||
|
header => header.name.toLowerCase() === "accept"
|
||||||
|
);
|
||||||
|
if (!acceptHeader || !acceptHeader.value) return;
|
||||||
|
if (!acceptHeader.value.toLowerCase().includes("text/html")) return;
|
||||||
|
|
||||||
|
filterResponseDataWrapper(details, text => {
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const document = parser.parseFromString(text, "text/html");
|
||||||
|
const script = document.createElement("script");
|
||||||
|
script.src = pageScriptURL; // src/page/page.ts
|
||||||
|
script.dataset.params = JSON.stringify({
|
||||||
|
isChromium,
|
||||||
|
workerScriptURL, // src/page/worker.ts
|
||||||
|
});
|
||||||
|
script.onload = () => script.remove();
|
||||||
|
// ---------------------------------------
|
||||||
|
// 🦊 Attention Firefox Addon Reviewer 🦊
|
||||||
|
// ---------------------------------------
|
||||||
|
// Please note that this does NOT involve remote code execution. The injected scripts are bundled
|
||||||
|
// with the extension. The `url:` imports above are used to get the runtime URLs of the respective scripts.
|
||||||
|
// Additionally, there is no custom Content Security Policy (CSP) in use.
|
||||||
|
(document.head || document.documentElement).prepend(script);
|
||||||
|
return (
|
||||||
|
(document.compatMode === "BackCompat" ? "" : "<!DOCTYPE html>") +
|
||||||
|
document.documentElement.outerHTML
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
@ -3,6 +3,7 @@ import filterResponseDataWrapper from "../../common/ts/filterResponseDataWrapper
|
|||||||
import findChannelFromTwitchTvUrl from "../../common/ts/findChannelFromTwitchTvUrl";
|
import findChannelFromTwitchTvUrl from "../../common/ts/findChannelFromTwitchTvUrl";
|
||||||
import findChannelFromVideoWeaverUrl from "../../common/ts/findChannelFromVideoWeaverUrl";
|
import findChannelFromVideoWeaverUrl from "../../common/ts/findChannelFromVideoWeaverUrl";
|
||||||
import getHostFromUrl from "../../common/ts/getHostFromUrl";
|
import getHostFromUrl from "../../common/ts/getHostFromUrl";
|
||||||
|
import { getUrlFromProxyInfo } from "../../common/ts/proxyInfo";
|
||||||
import { videoWeaverHostRegex } from "../../common/ts/regexes";
|
import { videoWeaverHostRegex } from "../../common/ts/regexes";
|
||||||
import store from "../../store";
|
import store from "../../store";
|
||||||
import { AdType, ProxyInfo } from "../../types";
|
import { AdType, ProxyInfo } from "../../types";
|
||||||
@ -41,27 +42,20 @@ export default function onBeforeVideoWeaverRequest(
|
|||||||
);
|
);
|
||||||
const proxy =
|
const proxy =
|
||||||
details.proxyInfo && details.proxyInfo.type !== "direct"
|
details.proxyInfo && details.proxyInfo.type !== "direct"
|
||||||
? `${details.proxyInfo.host}:${details.proxyInfo.port}`
|
? getUrlFromProxyInfo(details.proxyInfo)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const adLog = store.state.adLog.filter(
|
store.state.adLog.push({
|
||||||
entry => details.timeStamp - entry.timestamp < 1000 * 60 * 60 * 24 * 7 // 7 days
|
adType: isMidroll ? AdType.MIDROLL : AdType.PREROLL,
|
||||||
);
|
isPurpleScreen,
|
||||||
store.state.adLog = [
|
proxy,
|
||||||
...adLog,
|
channel: channelName,
|
||||||
{
|
passportLevel: store.state.passportLevel,
|
||||||
adType: isMidroll ? AdType.MIDROLL : AdType.PREROLL,
|
anonymousMode: store.state.anonymousMode,
|
||||||
channel: channelName,
|
timestamp: details.timeStamp,
|
||||||
isPurpleScreen,
|
videoWeaverHost: host,
|
||||||
proxy,
|
videoWeaverUrl: details.url,
|
||||||
proxyTwitchWebpage: store.state.proxyTwitchWebpage,
|
});
|
||||||
proxyUsherRequests: store.state.proxyUsherRequests,
|
|
||||||
anonymousMode: store.state.anonymousMode,
|
|
||||||
timestamp: details.timeStamp,
|
|
||||||
videoWeaverHost: host,
|
|
||||||
videoWeaverUrl: details.url,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
console.log(`📝 Ad log updated (${store.state.adLog.length} entries).`);
|
console.log(`📝 Ad log updated (${store.state.adLog.length} entries).`);
|
||||||
console.log(text);
|
console.log(text);
|
||||||
|
|
||||||
|
75
src/background/handlers/onContentScriptMessage.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import browser, { Runtime } from "webextension-polyfill";
|
||||||
|
import { updateProxySettings } from "../../common/ts/proxySettings";
|
||||||
|
import store from "../../store";
|
||||||
|
import { MessageType, ProxyRequestType } from "../../types";
|
||||||
|
|
||||||
|
type Timeout = string | number | NodeJS.Timeout | undefined;
|
||||||
|
|
||||||
|
const timeoutMap: Map<ProxyRequestType, Timeout> = new Map();
|
||||||
|
|
||||||
|
export default function onContentScriptMessage(
|
||||||
|
message: any,
|
||||||
|
sender: Runtime.MessageSender,
|
||||||
|
sendResponse: () => void
|
||||||
|
): true | void | Promise<any> {
|
||||||
|
if (message.type === MessageType.EnableFullMode) {
|
||||||
|
if (!sender.tab?.id) return;
|
||||||
|
|
||||||
|
const requestType = message.requestType as ProxyRequestType;
|
||||||
|
|
||||||
|
// Clear existing timeout for request type.
|
||||||
|
if (timeoutMap.has(requestType)) {
|
||||||
|
clearTimeout(timeoutMap.get(requestType));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set new timeout for request type.
|
||||||
|
const fetchTimeoutMs = 3000; // Time for fetch to be called.
|
||||||
|
const replyTimeoutMs = Date.now() - message.timestamp; // Time for reply to be received.
|
||||||
|
timeoutMap.set(
|
||||||
|
requestType,
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log(
|
||||||
|
`[TTV LOL PRO] Disabling full mode (request type: ${requestType}, timeout)`
|
||||||
|
);
|
||||||
|
timeoutMap.delete(requestType);
|
||||||
|
if (store.state.chromiumProxyActive) {
|
||||||
|
updateProxySettings([...timeoutMap.keys()]);
|
||||||
|
}
|
||||||
|
}, fetchTimeoutMs + replyTimeoutMs)
|
||||||
|
);
|
||||||
|
if (store.state.chromiumProxyActive) {
|
||||||
|
updateProxySettings([...timeoutMap.keys()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[TTV LOL PRO] Enabled full mode for ${
|
||||||
|
fetchTimeoutMs + replyTimeoutMs
|
||||||
|
}ms (request type: ${requestType})`
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
browser.tabs.sendMessage(sender.tab.id, {
|
||||||
|
type: MessageType.EnableFullModeResponse,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"[TTV LOL PRO] Failed to send EnableFullModeResponse message",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.type === MessageType.DisableFullMode) {
|
||||||
|
const requestType = message.requestType as ProxyRequestType;
|
||||||
|
// Clear existing timeout for request type.
|
||||||
|
if (timeoutMap.has(requestType)) {
|
||||||
|
clearTimeout(timeoutMap.get(requestType));
|
||||||
|
timeoutMap.delete(requestType);
|
||||||
|
}
|
||||||
|
if (store.state.chromiumProxyActive) {
|
||||||
|
updateProxySettings([...timeoutMap.keys()]);
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
`[TTV LOL PRO] Disabled full mode (request type: ${requestType})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
41
src/background/handlers/onInstalledStoreCleanup.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { Runtime } from "webextension-polyfill";
|
||||||
|
import isChromium from "../../common/ts/isChromium";
|
||||||
|
import store from "../../store";
|
||||||
|
|
||||||
|
export default function onInstalledStoreCleanup(
|
||||||
|
details: Runtime.OnInstalledDetailsType
|
||||||
|
): void {
|
||||||
|
if (store.readyState !== "complete")
|
||||||
|
return store.addEventListener("load", () =>
|
||||||
|
onInstalledStoreCleanup(details)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (details.reason === "update") {
|
||||||
|
// Remove old Chromium normal proxy.
|
||||||
|
const oldChromiumProxy = "chrome.api.cdn-perfprod.com:4023";
|
||||||
|
if (store.state.normalProxies.includes(oldChromiumProxy)) {
|
||||||
|
store.state.normalProxies = store.state.normalProxies.filter(
|
||||||
|
proxy => proxy !== oldChromiumProxy
|
||||||
|
);
|
||||||
|
if (store.state.normalProxies.length === 0) {
|
||||||
|
store.state.optimizedProxiesEnabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Add new Chromium optimized proxy.
|
||||||
|
const newChromiumProxy = "chromium.api.cdn-perfprod.com:2023";
|
||||||
|
if (
|
||||||
|
isChromium &&
|
||||||
|
!store.state.optimizedProxies.includes(newChromiumProxy)
|
||||||
|
) {
|
||||||
|
// Remove Firefox optimized proxy (used during beta).
|
||||||
|
const firefoxProxy = "firefox.api.cdn-perfprod.com:2023";
|
||||||
|
if (store.state.optimizedProxies.includes(firefoxProxy)) {
|
||||||
|
store.state.optimizedProxies = store.state.optimizedProxies.filter(
|
||||||
|
proxy => proxy !== firefoxProxy
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
store.state.optimizedProxies.push(newChromiumProxy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,9 +3,10 @@ import findChannelFromTwitchTvUrl from "../../common/ts/findChannelFromTwitchTvU
|
|||||||
import findChannelFromUsherUrl from "../../common/ts/findChannelFromUsherUrl";
|
import findChannelFromUsherUrl from "../../common/ts/findChannelFromUsherUrl";
|
||||||
import findChannelFromVideoWeaverUrl from "../../common/ts/findChannelFromVideoWeaverUrl";
|
import findChannelFromVideoWeaverUrl from "../../common/ts/findChannelFromVideoWeaverUrl";
|
||||||
import getHostFromUrl from "../../common/ts/getHostFromUrl";
|
import getHostFromUrl from "../../common/ts/getHostFromUrl";
|
||||||
import getProxyInfoFromUrl from "../../common/ts/getProxyInfoFromUrl";
|
|
||||||
import isChannelWhitelisted from "../../common/ts/isChannelWhitelisted";
|
import isChannelWhitelisted from "../../common/ts/isChannelWhitelisted";
|
||||||
import isFlaggedRequest from "../../common/ts/isFlaggedRequest";
|
import isFlaggedRequest from "../../common/ts/isFlaggedRequest";
|
||||||
|
import isRequestTypeProxied from "../../common/ts/isRequestTypeProxied";
|
||||||
|
import { getProxyInfoFromUrl } from "../../common/ts/proxyInfo";
|
||||||
import {
|
import {
|
||||||
passportHostRegex,
|
passportHostRegex,
|
||||||
twitchGqlHostRegex,
|
twitchGqlHostRegex,
|
||||||
@ -14,20 +15,11 @@ import {
|
|||||||
videoWeaverHostRegex,
|
videoWeaverHostRegex,
|
||||||
} from "../../common/ts/regexes";
|
} from "../../common/ts/regexes";
|
||||||
import store from "../../store";
|
import store from "../../store";
|
||||||
import type { ProxyInfo } from "../../types";
|
import { ProxyInfo, ProxyRequestType } from "../../types";
|
||||||
|
|
||||||
export default async function onProxyRequest(
|
export default async function onProxyRequest(
|
||||||
details: Proxy.OnRequestDetailsType
|
details: Proxy.OnRequestDetailsType
|
||||||
): Promise<ProxyInfo | ProxyInfo[]> {
|
): Promise<ProxyInfo | ProxyInfo[]> {
|
||||||
const host = getHostFromUrl(details.url);
|
|
||||||
if (!host) return { type: "direct" };
|
|
||||||
|
|
||||||
const documentHost = details.documentUrl
|
|
||||||
? getHostFromUrl(details.documentUrl)
|
|
||||||
: null;
|
|
||||||
const isFromTwitchTvHost =
|
|
||||||
documentHost && twitchTvHostRegex.test(documentHost);
|
|
||||||
|
|
||||||
// Wait for the store to be ready.
|
// Wait for the store to be ready.
|
||||||
if (store.readyState !== "complete") {
|
if (store.readyState !== "complete") {
|
||||||
await new Promise(resolve => {
|
await new Promise(resolve => {
|
||||||
@ -39,37 +31,55 @@ export default async function onProxyRequest(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const isFlagged =
|
const host = getHostFromUrl(details.url);
|
||||||
(store.state.optimizedProxiesEnabled &&
|
if (!host) return { type: "direct" };
|
||||||
isFlaggedRequest(details.requestHeaders)) ||
|
|
||||||
!store.state.optimizedProxiesEnabled;
|
const documentHost = details.documentUrl
|
||||||
|
? getHostFromUrl(details.documentUrl)
|
||||||
|
: null;
|
||||||
|
// Twitch requests from non-Twitch hosts are not supported.
|
||||||
|
if (
|
||||||
|
documentHost != null && // Twitch webpage requests have no document URL.
|
||||||
|
!passportHostRegex.test(documentHost) && // Passport requests have a `passport.twitch.tv` document URL.
|
||||||
|
!twitchTvHostRegex.test(documentHost)
|
||||||
|
) {
|
||||||
|
return { type: "direct" };
|
||||||
|
}
|
||||||
|
|
||||||
const proxies = store.state.optimizedProxiesEnabled
|
const proxies = store.state.optimizedProxiesEnabled
|
||||||
? store.state.optimizedProxies
|
? store.state.optimizedProxies
|
||||||
: store.state.normalProxies;
|
: store.state.normalProxies;
|
||||||
const proxyInfoArray = getProxyInfoArrayFromUrls(proxies);
|
const proxyInfoArray = getProxyInfoArrayFromUrls(proxies);
|
||||||
|
|
||||||
// Twitch webpage requests.
|
const requestParams = {
|
||||||
if (store.state.proxyTwitchWebpage && twitchTvHostRegex.test(host)) {
|
isChromium: false,
|
||||||
console.log(`⌛ Proxying ${details.url} through one of: <empty>`);
|
optimizedProxiesEnabled: store.state.optimizedProxiesEnabled,
|
||||||
return proxyInfoArray;
|
passportLevel: store.state.passportLevel,
|
||||||
}
|
isFlagged: isFlaggedRequest(details.requestHeaders),
|
||||||
|
};
|
||||||
// Twitch GraphQL requests.
|
const proxyPassportRequest = isRequestTypeProxied(
|
||||||
if (
|
ProxyRequestType.Passport,
|
||||||
store.state.proxyTwitchWebpage &&
|
requestParams
|
||||||
twitchGqlHostRegex.test(host) &&
|
);
|
||||||
isFlagged
|
const proxyUsherRequest = isRequestTypeProxied(
|
||||||
) {
|
ProxyRequestType.Usher,
|
||||||
console.log(
|
requestParams
|
||||||
`⌛ Proxying ${details.url} through one of: ${
|
);
|
||||||
proxies.toString() || "<empty>"
|
const proxyVideoWeaverRequest = isRequestTypeProxied(
|
||||||
}`
|
ProxyRequestType.VideoWeaver,
|
||||||
);
|
requestParams
|
||||||
return proxyInfoArray;
|
);
|
||||||
}
|
const proxyGraphQLRequest = isRequestTypeProxied(
|
||||||
|
ProxyRequestType.GraphQL,
|
||||||
|
requestParams
|
||||||
|
);
|
||||||
|
const proxyTwitchWebpageRequest = isRequestTypeProxied(
|
||||||
|
ProxyRequestType.TwitchWebpage,
|
||||||
|
requestParams
|
||||||
|
);
|
||||||
|
|
||||||
// Passport requests.
|
// Passport requests.
|
||||||
if (store.state.proxyUsherRequests && passportHostRegex.test(host)) {
|
if (proxyPassportRequest && passportHostRegex.test(host)) {
|
||||||
console.log(
|
console.log(
|
||||||
`⌛ Proxying ${details.url} through one of: ${
|
`⌛ Proxying ${details.url} through one of: ${
|
||||||
proxies.toString() || "<empty>"
|
proxies.toString() || "<empty>"
|
||||||
@ -79,15 +89,11 @@ export default async function onProxyRequest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Usher requests.
|
// Usher requests.
|
||||||
if (store.state.proxyUsherRequests && usherHostRegex.test(host)) {
|
if (proxyUsherRequest && usherHostRegex.test(host)) {
|
||||||
// Don't proxy Usher requests from non-supported hosts.
|
if (details.url.includes("/vod/")) {
|
||||||
if (!isFromTwitchTvHost) {
|
console.log(`✋ '${details.url}' is a VOD manifest.`);
|
||||||
console.log(
|
|
||||||
`✋ '${details.url}' from host '${documentHost}' is not supported.`
|
|
||||||
);
|
|
||||||
return { type: "direct" };
|
return { type: "direct" };
|
||||||
}
|
}
|
||||||
// Don't proxy whitelisted channels.
|
|
||||||
const channelName = findChannelFromUsherUrl(details.url);
|
const channelName = findChannelFromUsherUrl(details.url);
|
||||||
if (isChannelWhitelisted(channelName)) {
|
if (isChannelWhitelisted(channelName)) {
|
||||||
console.log(`✋ Channel '${channelName}' is whitelisted.`);
|
console.log(`✋ Channel '${channelName}' is whitelisted.`);
|
||||||
@ -102,15 +108,7 @@ export default async function onProxyRequest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Video Weaver requests.
|
// Video Weaver requests.
|
||||||
if (videoWeaverHostRegex.test(host) && isFlagged) {
|
if (proxyVideoWeaverRequest && videoWeaverHostRegex.test(host)) {
|
||||||
// Don't proxy Video Weaver requests from non-supported hosts.
|
|
||||||
if (!isFromTwitchTvHost) {
|
|
||||||
console.log(
|
|
||||||
`✋ '${details.url}' from host '${documentHost}' is not supported.`
|
|
||||||
);
|
|
||||||
return { type: "direct" };
|
|
||||||
}
|
|
||||||
// Don't proxy whitelisted channels.
|
|
||||||
const channelName =
|
const channelName =
|
||||||
findChannelFromVideoWeaverUrl(details.url) ??
|
findChannelFromVideoWeaverUrl(details.url) ??
|
||||||
findChannelFromTwitchTvUrl(details.documentUrl);
|
findChannelFromTwitchTvUrl(details.documentUrl);
|
||||||
@ -126,6 +124,26 @@ export default async function onProxyRequest(
|
|||||||
return proxyInfoArray;
|
return proxyInfoArray;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Twitch GraphQL requests.
|
||||||
|
if (proxyGraphQLRequest && twitchGqlHostRegex.test(host)) {
|
||||||
|
console.log(
|
||||||
|
`⌛ Proxying ${details.url} through one of: ${
|
||||||
|
proxies.toString() || "<empty>"
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
return proxyInfoArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Twitch webpage requests.
|
||||||
|
if (proxyTwitchWebpageRequest && twitchTvHostRegex.test(host)) {
|
||||||
|
console.log(
|
||||||
|
`⌛ Proxying ${details.url} through one of: ${
|
||||||
|
proxies.toString() || "<empty>"
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
return proxyInfoArray;
|
||||||
|
}
|
||||||
|
|
||||||
return { type: "direct" };
|
return { type: "direct" };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
import { Types } from "webextension-polyfill";
|
|
||||||
import store from "../../store";
|
|
||||||
|
|
||||||
export default function onProxySettingsChange(
|
|
||||||
details: Types.SettingOnChangeDetailsType
|
|
||||||
) {
|
|
||||||
console.log(`⚙️ Proxy settings changed: ${details.levelOfControl}`);
|
|
||||||
store.state.chromiumProxyActive =
|
|
||||||
details.levelOfControl == "controlled_by_this_extension";
|
|
||||||
}
|
|
@ -2,8 +2,12 @@ import { WebRequest } from "webextension-polyfill";
|
|||||||
import findChannelFromTwitchTvUrl from "../../common/ts/findChannelFromTwitchTvUrl";
|
import findChannelFromTwitchTvUrl from "../../common/ts/findChannelFromTwitchTvUrl";
|
||||||
import findChannelFromVideoWeaverUrl from "../../common/ts/findChannelFromVideoWeaverUrl";
|
import findChannelFromVideoWeaverUrl from "../../common/ts/findChannelFromVideoWeaverUrl";
|
||||||
import getHostFromUrl from "../../common/ts/getHostFromUrl";
|
import getHostFromUrl from "../../common/ts/getHostFromUrl";
|
||||||
import getProxyInfoFromUrl from "../../common/ts/getProxyInfoFromUrl";
|
|
||||||
import isChromium from "../../common/ts/isChromium";
|
import isChromium from "../../common/ts/isChromium";
|
||||||
|
import isRequestTypeProxied from "../../common/ts/isRequestTypeProxied";
|
||||||
|
import {
|
||||||
|
getProxyInfoFromUrl,
|
||||||
|
getUrlFromProxyInfo,
|
||||||
|
} from "../../common/ts/proxyInfo";
|
||||||
import {
|
import {
|
||||||
passportHostRegex,
|
passportHostRegex,
|
||||||
twitchGqlHostRegex,
|
twitchGqlHostRegex,
|
||||||
@ -13,7 +17,7 @@ import {
|
|||||||
} from "../../common/ts/regexes";
|
} from "../../common/ts/regexes";
|
||||||
import { getStreamStatus, setStreamStatus } from "../../common/ts/streamStatus";
|
import { getStreamStatus, setStreamStatus } from "../../common/ts/streamStatus";
|
||||||
import store from "../../store";
|
import store from "../../store";
|
||||||
import type { ProxyInfo } from "../../types";
|
import { ProxyInfo, ProxyRequestType } from "../../types";
|
||||||
|
|
||||||
export default function onResponseStarted(
|
export default function onResponseStarted(
|
||||||
details: WebRequest.OnResponseStartedDetailsType & {
|
details: WebRequest.OnResponseStartedDetailsType & {
|
||||||
@ -25,30 +29,46 @@ export default function onResponseStarted(
|
|||||||
|
|
||||||
const proxy = getProxyFromDetails(details);
|
const proxy = getProxyFromDetails(details);
|
||||||
|
|
||||||
// Twitch webpage requests.
|
const requestParams = {
|
||||||
if (store.state.proxyTwitchWebpage && twitchTvHostRegex.test(host)) {
|
isChromium: isChromium,
|
||||||
|
optimizedProxiesEnabled: store.state.optimizedProxiesEnabled,
|
||||||
|
passportLevel: store.state.passportLevel,
|
||||||
|
};
|
||||||
|
const proxiedPassportRequest = isRequestTypeProxied(
|
||||||
|
ProxyRequestType.Passport,
|
||||||
|
requestParams
|
||||||
|
);
|
||||||
|
const proxiedUsherRequest = isRequestTypeProxied(
|
||||||
|
ProxyRequestType.Usher,
|
||||||
|
requestParams
|
||||||
|
);
|
||||||
|
const proxiedVideoWeaverRequest = isRequestTypeProxied(
|
||||||
|
ProxyRequestType.VideoWeaver,
|
||||||
|
requestParams
|
||||||
|
);
|
||||||
|
const proxiedGraphQLRequest = isRequestTypeProxied(
|
||||||
|
ProxyRequestType.GraphQL,
|
||||||
|
requestParams
|
||||||
|
);
|
||||||
|
const proxiedTwitchWebpageRequest = isRequestTypeProxied(
|
||||||
|
ProxyRequestType.TwitchWebpage,
|
||||||
|
requestParams
|
||||||
|
);
|
||||||
|
|
||||||
|
// Passport requests.
|
||||||
|
if (proxiedPassportRequest && passportHostRegex.test(host)) {
|
||||||
if (!proxy) return console.log(`❌ Did not proxy ${details.url}`);
|
if (!proxy) return console.log(`❌ Did not proxy ${details.url}`);
|
||||||
console.log(`✅ Proxied ${details.url} through ${proxy}`);
|
console.log(`✅ Proxied ${details.url} through ${proxy}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Twitch GraphQL requests.
|
// Usher requests.
|
||||||
if (store.state.proxyTwitchWebpage && twitchGqlHostRegex.test(host)) {
|
if (proxiedUsherRequest && usherHostRegex.test(host)) {
|
||||||
if (!proxy && store.state.optimizedProxiesEnabled) return; // Expected for most requests.
|
|
||||||
if (!proxy) return console.log(`❌ Did not proxy ${details.url}`);
|
|
||||||
console.log(`✅ Proxied ${details.url} through ${proxy}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Passport & Usher requests.
|
|
||||||
if (
|
|
||||||
store.state.proxyUsherRequests &&
|
|
||||||
(passportHostRegex.test(host) || usherHostRegex.test(host))
|
|
||||||
) {
|
|
||||||
if (!proxy) return console.log(`❌ Did not proxy ${details.url}`);
|
if (!proxy) return console.log(`❌ Did not proxy ${details.url}`);
|
||||||
console.log(`✅ Proxied ${details.url} through ${proxy}`);
|
console.log(`✅ Proxied ${details.url} through ${proxy}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Video-weaver requests.
|
// Video-weaver requests.
|
||||||
if (videoWeaverHostRegex.test(host)) {
|
if (proxiedVideoWeaverRequest && videoWeaverHostRegex.test(host)) {
|
||||||
const channelName =
|
const channelName =
|
||||||
findChannelFromVideoWeaverUrl(details.url) ??
|
findChannelFromVideoWeaverUrl(details.url) ??
|
||||||
findChannelFromTwitchTvUrl(details.documentUrl);
|
findChannelFromTwitchTvUrl(details.documentUrl);
|
||||||
@ -60,7 +80,7 @@ export default function onResponseStarted(
|
|||||||
proxied: false,
|
proxied: false,
|
||||||
proxyHost: streamStatus?.proxyHost ? streamStatus.proxyHost : undefined,
|
proxyHost: streamStatus?.proxyHost ? streamStatus.proxyHost : undefined,
|
||||||
proxyCountry: streamStatus?.proxyCountry,
|
proxyCountry: streamStatus?.proxyCountry,
|
||||||
reason: `Proxied: ${stats.proxied} | [Not proxied]: ${stats.notProxied}`,
|
reason: `Proxied: ${stats.proxied} | Not proxied: ${stats.notProxied}`,
|
||||||
stats,
|
stats,
|
||||||
});
|
});
|
||||||
console.log(
|
console.log(
|
||||||
@ -73,13 +93,26 @@ export default function onResponseStarted(
|
|||||||
proxied: true,
|
proxied: true,
|
||||||
proxyHost: proxy,
|
proxyHost: proxy,
|
||||||
proxyCountry: streamStatus?.proxyCountry,
|
proxyCountry: streamStatus?.proxyCountry,
|
||||||
reason: `[Proxied]: ${stats.proxied} | Not proxied: ${stats.notProxied}`,
|
reason: `Proxied: ${stats.proxied} | Not proxied: ${stats.notProxied}`,
|
||||||
stats,
|
stats,
|
||||||
});
|
});
|
||||||
console.log(
|
console.log(
|
||||||
`✅ Proxied ${details.url} (${channelName ?? "unknown"}) through ${proxy}`
|
`✅ Proxied ${details.url} (${channelName ?? "unknown"}) through ${proxy}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Twitch GraphQL requests.
|
||||||
|
if (proxiedGraphQLRequest && twitchGqlHostRegex.test(host)) {
|
||||||
|
if (!proxy && store.state.optimizedProxiesEnabled) return; // Expected for most requests.
|
||||||
|
if (!proxy) return console.log(`❌ Did not proxy ${details.url}`);
|
||||||
|
console.log(`✅ Proxied ${details.url} through ${proxy}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Twitch webpage requests.
|
||||||
|
if (proxiedTwitchWebpageRequest && twitchTvHostRegex.test(host)) {
|
||||||
|
if (!proxy) return console.log(`❌ Did not proxy ${details.url}`);
|
||||||
|
console.log(`✅ Proxied ${details.url} through ${proxy}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProxyFromDetails(
|
function getProxyFromDetails(
|
||||||
@ -103,11 +136,11 @@ function getProxyFromDetails(
|
|||||||
proxy => proxy.host === dnsResponse.host
|
proxy => proxy.host === dnsResponse.host
|
||||||
);
|
);
|
||||||
if (possibleProxies.length === 1)
|
if (possibleProxies.length === 1)
|
||||||
return `${possibleProxies[0].host}:${possibleProxies[0].port}`;
|
return getUrlFromProxyInfo(possibleProxies[0]);
|
||||||
return dnsResponse.host;
|
return dnsResponse.host;
|
||||||
} else {
|
} else {
|
||||||
const proxyInfo = details.proxyInfo; // Firefox only.
|
const proxyInfo = details.proxyInfo; // Firefox only.
|
||||||
if (!proxyInfo || proxyInfo.type === "direct") return null;
|
if (!proxyInfo || proxyInfo.type === "direct") return null;
|
||||||
return `${proxyInfo.host}:${proxyInfo.port}`;
|
return getUrlFromProxyInfo(proxyInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,10 @@ export default function onStartupStoreCleanup(): void {
|
|||||||
if (store.readyState !== "complete")
|
if (store.readyState !== "complete")
|
||||||
return store.addEventListener("load", onStartupStoreCleanup);
|
return store.addEventListener("load", onStartupStoreCleanup);
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
store.state.adLog = store.state.adLog.filter(
|
||||||
|
entry => now - entry.timestamp < 1000 * 60 * 60 * 24 * 7 // 7 days
|
||||||
|
);
|
||||||
store.state.chromiumProxyActive = false;
|
store.state.chromiumProxyActive = false;
|
||||||
store.state.dnsResponses = [];
|
store.state.dnsResponses = [];
|
||||||
store.state.openedTwitchTabs = [];
|
store.state.openedTwitchTabs = [];
|
||||||
|
@ -16,9 +16,6 @@ export default function onTabCreated(tab: Tabs.Tab): void {
|
|||||||
const host = getHostFromUrl(url);
|
const host = getHostFromUrl(url);
|
||||||
if (!host) return;
|
if (!host) return;
|
||||||
|
|
||||||
// TODO: `twitchTvHostRegex` doesn't match `appeals.twitch.tv` and
|
|
||||||
// `dashboard.twitch.tv` which means that passport requests from those
|
|
||||||
// subdomains will not be proxied. This could mess up the cookie country.
|
|
||||||
if (twitchTvHostRegex.test(host)) {
|
if (twitchTvHostRegex.test(host)) {
|
||||||
console.log(`➕ Opened Twitch tab: ${tab.id}`);
|
console.log(`➕ Opened Twitch tab: ${tab.id}`);
|
||||||
store.state.openedTwitchTabs.push(tab);
|
store.state.openedTwitchTabs.push(tab);
|
||||||
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 97 KiB |
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
BIN
src/common/images/options_bg.png
Normal file
After Width: | Height: | Size: 91 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
@ -1,5 +1,5 @@
|
|||||||
import ip from "ip";
|
import ip from "ip";
|
||||||
import getProxyInfoFromUrl from "./getProxyInfoFromUrl";
|
import { getProxyInfoFromUrl } from "./proxyInfo";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Anonymize an IP address by masking the last 2 octets of an IPv4 address
|
* Anonymize an IP address by masking the last 2 octets of an IPv4 address
|
||||||
@ -11,8 +11,6 @@ export function anonymizeIpAddress(url: string): string {
|
|||||||
const proxyInfo = getProxyInfoFromUrl(url);
|
const proxyInfo = getProxyInfoFromUrl(url);
|
||||||
|
|
||||||
let proxyHost = proxyInfo.host;
|
let proxyHost = proxyInfo.host;
|
||||||
const withinBrackets = /^\[.*\]$/.test(proxyHost);
|
|
||||||
if (withinBrackets) proxyHost = proxyHost.slice(1, -1);
|
|
||||||
|
|
||||||
const isIPv4 = ip.isV4Format(proxyHost);
|
const isIPv4 = ip.isV4Format(proxyHost);
|
||||||
const isIPv6 = ip.isV6Format(proxyHost);
|
const isIPv6 = ip.isV6Format(proxyHost);
|
||||||
@ -27,9 +25,7 @@ export function anonymizeIpAddress(url: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (withinBrackets) proxyHost = `[${proxyHost}]`;
|
return proxyHost; // Also anonymizes port.
|
||||||
|
|
||||||
return `${proxyHost}:${proxyInfo.port}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
254
src/common/ts/countryCodes.ts
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
const alpha2 = {
|
||||||
|
AD: "Andorra",
|
||||||
|
AE: "United Arab Emirates",
|
||||||
|
AF: "Afghanistan",
|
||||||
|
AG: "Antigua and Barbuda",
|
||||||
|
AI: "Anguilla",
|
||||||
|
AL: "Albania",
|
||||||
|
AM: "Armenia",
|
||||||
|
AO: "Angola",
|
||||||
|
AQ: "Antarctica",
|
||||||
|
AR: "Argentina",
|
||||||
|
AS: "American Samoa",
|
||||||
|
AT: "Austria",
|
||||||
|
AU: "Australia",
|
||||||
|
AW: "Aruba",
|
||||||
|
AX: "Åland Islands",
|
||||||
|
AZ: "Azerbaijan",
|
||||||
|
BA: "Bosnia and Herzegovina",
|
||||||
|
BB: "Barbados",
|
||||||
|
BD: "Bangladesh",
|
||||||
|
BE: "Belgium",
|
||||||
|
BF: "Burkina Faso",
|
||||||
|
BG: "Bulgaria",
|
||||||
|
BH: "Bahrain",
|
||||||
|
BI: "Burundi",
|
||||||
|
BJ: "Benin",
|
||||||
|
BL: "Saint Barthélemy",
|
||||||
|
BM: "Bermuda",
|
||||||
|
BN: "Brunei Darussalam",
|
||||||
|
BO: "Bolivia",
|
||||||
|
BQ: "Bonaire, Sint Eustatius and Saba",
|
||||||
|
BR: "Brazil",
|
||||||
|
BS: "Bahamas",
|
||||||
|
BT: "Bhutan",
|
||||||
|
BV: "Bouvet Island",
|
||||||
|
BW: "Botswana",
|
||||||
|
BY: "Belarus",
|
||||||
|
BZ: "Belize",
|
||||||
|
CA: "Canada",
|
||||||
|
CC: "Cocos (Keeling) Islands",
|
||||||
|
CD: "Democratic Republic of the Congo",
|
||||||
|
CF: "Central African Republic",
|
||||||
|
CG: "Republic of the Congo",
|
||||||
|
CH: "Switzerland",
|
||||||
|
CI: "Côte d'Ivoire",
|
||||||
|
CK: "Cook Islands",
|
||||||
|
CL: "Chile",
|
||||||
|
CM: "Cameroon",
|
||||||
|
CN: "China",
|
||||||
|
CO: "Colombia",
|
||||||
|
CR: "Costa Rica",
|
||||||
|
CU: "Cuba",
|
||||||
|
CV: "Cabo Verde",
|
||||||
|
CW: "Curaçao",
|
||||||
|
CX: "Christmas Island",
|
||||||
|
CY: "Cyprus",
|
||||||
|
CZ: "Czechia",
|
||||||
|
DE: "Germany",
|
||||||
|
DJ: "Djibouti",
|
||||||
|
DK: "Denmark",
|
||||||
|
DM: "Dominica",
|
||||||
|
DO: "Dominican Republic",
|
||||||
|
DZ: "Algeria",
|
||||||
|
EC: "Ecuador",
|
||||||
|
EE: "Estonia",
|
||||||
|
EG: "Egypt",
|
||||||
|
EH: "Western Sahara",
|
||||||
|
ER: "Eritrea",
|
||||||
|
ES: "Spain",
|
||||||
|
ET: "Ethiopia",
|
||||||
|
FI: "Finland",
|
||||||
|
FJ: "Fiji",
|
||||||
|
FK: "Falkland Islands",
|
||||||
|
FM: "Micronesia",
|
||||||
|
FO: "Faroe Islands",
|
||||||
|
FR: "France",
|
||||||
|
GA: "Gabon",
|
||||||
|
GB: "United Kingdom",
|
||||||
|
GD: "Grenada",
|
||||||
|
GE: "Georgia",
|
||||||
|
GF: "French Guiana",
|
||||||
|
GG: "Guernsey",
|
||||||
|
GH: "Ghana",
|
||||||
|
GI: "Gibraltar",
|
||||||
|
GL: "Greenland",
|
||||||
|
GM: "Gambia",
|
||||||
|
GN: "Guinea",
|
||||||
|
GP: "Guadeloupe",
|
||||||
|
GQ: "Equatorial Guinea",
|
||||||
|
GR: "Greece",
|
||||||
|
GS: "South Georgia and the South Sandwich Islands",
|
||||||
|
GT: "Guatemala",
|
||||||
|
GU: "Guam",
|
||||||
|
GW: "Guinea-Bissau",
|
||||||
|
GY: "Guyana",
|
||||||
|
HK: "Hong Kong",
|
||||||
|
HM: "Heard Island and McDonald Islands",
|
||||||
|
HN: "Honduras",
|
||||||
|
HR: "Croatia",
|
||||||
|
HT: "Haiti",
|
||||||
|
HU: "Hungary",
|
||||||
|
ID: "Indonesia",
|
||||||
|
IE: "Ireland",
|
||||||
|
IL: "Israel",
|
||||||
|
IM: "Isle of Man",
|
||||||
|
IN: "India",
|
||||||
|
IO: "British Indian Ocean Territory",
|
||||||
|
IQ: "Iraq",
|
||||||
|
IR: "Iran",
|
||||||
|
IS: "Iceland",
|
||||||
|
IT: "Italy",
|
||||||
|
JE: "Jersey",
|
||||||
|
JM: "Jamaica",
|
||||||
|
JO: "Jordan",
|
||||||
|
JP: "Japan",
|
||||||
|
KE: "Kenya",
|
||||||
|
KG: "Kyrgyzstan",
|
||||||
|
KH: "Cambodia",
|
||||||
|
KI: "Kiribati",
|
||||||
|
KM: "Comoros",
|
||||||
|
KN: "Saint Kitts and Nevis",
|
||||||
|
KP: "North Korea",
|
||||||
|
KR: "South Korea",
|
||||||
|
KW: "Kuwait",
|
||||||
|
KY: "Cayman Islands",
|
||||||
|
KZ: "Kazakhstan",
|
||||||
|
LA: "Laos",
|
||||||
|
LB: "Lebanon",
|
||||||
|
LC: "Saint Lucia",
|
||||||
|
LI: "Liechtenstein",
|
||||||
|
LK: "Sri Lanka",
|
||||||
|
LR: "Liberia",
|
||||||
|
LS: "Lesotho",
|
||||||
|
LT: "Lithuania",
|
||||||
|
LU: "Luxembourg",
|
||||||
|
LV: "Latvia",
|
||||||
|
LY: "Libya",
|
||||||
|
MA: "Morocco",
|
||||||
|
MC: "Monaco",
|
||||||
|
MD: "Moldova",
|
||||||
|
ME: "Montenegro",
|
||||||
|
MF: "Saint Martin",
|
||||||
|
MG: "Madagascar",
|
||||||
|
MH: "Marshall Islands",
|
||||||
|
MK: "North Macedonia",
|
||||||
|
ML: "Mali",
|
||||||
|
MM: "Myanmar",
|
||||||
|
MN: "Mongolia",
|
||||||
|
MO: "Macao",
|
||||||
|
MP: "Northern Mariana Islands",
|
||||||
|
MQ: "Martinique",
|
||||||
|
MR: "Mauritania",
|
||||||
|
MS: "Montserrat",
|
||||||
|
MT: "Malta",
|
||||||
|
MU: "Mauritius",
|
||||||
|
MV: "Maldives",
|
||||||
|
MW: "Malawi",
|
||||||
|
MX: "Mexico",
|
||||||
|
MY: "Malaysia",
|
||||||
|
MZ: "Mozambique",
|
||||||
|
NA: "Namibia",
|
||||||
|
NC: "New Caledonia",
|
||||||
|
NE: "Niger",
|
||||||
|
NF: "Norfolk Island",
|
||||||
|
NG: "Nigeria",
|
||||||
|
NI: "Nicaragua",
|
||||||
|
NL: "Netherlands",
|
||||||
|
NO: "Norway",
|
||||||
|
NP: "Nepal",
|
||||||
|
NR: "Nauru",
|
||||||
|
NU: "Niue",
|
||||||
|
NZ: "New Zealand",
|
||||||
|
OM: "Oman",
|
||||||
|
PA: "Panama",
|
||||||
|
PE: "Peru",
|
||||||
|
PF: "French Polynesia",
|
||||||
|
PG: "Papua New Guinea",
|
||||||
|
PH: "Philippines",
|
||||||
|
PK: "Pakistan",
|
||||||
|
PL: "Poland",
|
||||||
|
PM: "Saint Pierre and Miquelon",
|
||||||
|
PN: "Pitcairn",
|
||||||
|
PR: "Puerto Rico",
|
||||||
|
PS: "Palestine",
|
||||||
|
PT: "Portugal",
|
||||||
|
PW: "Palau",
|
||||||
|
PY: "Paraguay",
|
||||||
|
QA: "Qatar",
|
||||||
|
RE: "Réunion",
|
||||||
|
RO: "Romania",
|
||||||
|
RS: "Serbia",
|
||||||
|
RU: "Russia",
|
||||||
|
RW: "Rwanda",
|
||||||
|
SA: "Saudi Arabia",
|
||||||
|
SB: "Solomon Islands",
|
||||||
|
SC: "Seychelles",
|
||||||
|
SD: "Sudan",
|
||||||
|
SE: "Sweden",
|
||||||
|
SG: "Singapore",
|
||||||
|
SH: "Saint Helena, Ascension and Tristan da Cunha",
|
||||||
|
SI: "Slovenia",
|
||||||
|
SJ: "Svalbard and Jan Mayen",
|
||||||
|
SK: "Slovakia",
|
||||||
|
SL: "Sierra Leone",
|
||||||
|
SM: "San Marino",
|
||||||
|
SN: "Senegal",
|
||||||
|
SO: "Somalia",
|
||||||
|
SR: "Suriname",
|
||||||
|
SS: "South Sudan",
|
||||||
|
ST: "Sao Tome and Principe",
|
||||||
|
SV: "El Salvador",
|
||||||
|
SX: "Sint Maarten",
|
||||||
|
SY: "Syria",
|
||||||
|
SZ: "Eswatini",
|
||||||
|
TC: "Turks and Caicos Islands",
|
||||||
|
TD: "Chad",
|
||||||
|
TF: "French Southern Territories",
|
||||||
|
TG: "Togo",
|
||||||
|
TH: "Thailand",
|
||||||
|
TJ: "Tajikistan",
|
||||||
|
TK: "Tokelau",
|
||||||
|
TL: "Timor-Leste",
|
||||||
|
TM: "Turkmenistan",
|
||||||
|
TN: "Tunisia",
|
||||||
|
TO: "Tonga",
|
||||||
|
TR: "Türkiye",
|
||||||
|
TT: "Trinidad and Tobago",
|
||||||
|
TV: "Tuvalu",
|
||||||
|
TW: "Taiwan",
|
||||||
|
TZ: "Tanzania",
|
||||||
|
UA: "Ukraine",
|
||||||
|
UG: "Uganda",
|
||||||
|
UK: "United Kingdom",
|
||||||
|
UM: "United States Minor Outlying Islands",
|
||||||
|
US: "United States",
|
||||||
|
UY: "Uruguay",
|
||||||
|
UZ: "Uzbekistan",
|
||||||
|
VA: "Vatican City",
|
||||||
|
VC: "Saint Vincent and the Grenadines",
|
||||||
|
VE: "Venezuela",
|
||||||
|
VG: "Virgin Islands (British)",
|
||||||
|
VI: "Virgin Islands (U.S.)",
|
||||||
|
VN: "Vietnam",
|
||||||
|
VU: "Vanuatu",
|
||||||
|
WF: "Wallis and Futuna",
|
||||||
|
WS: "Samoa",
|
||||||
|
YE: "Yemen",
|
||||||
|
YT: "Mayotte",
|
||||||
|
ZA: "South Africa",
|
||||||
|
ZM: "Zambia",
|
||||||
|
ZW: "Zimbabwe",
|
||||||
|
};
|
||||||
|
|
||||||
|
export { alpha2 };
|
92
src/common/ts/isRequestTypeProxied.ts
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import { ProxyRequestParams, ProxyRequestType } from "../../types";
|
||||||
|
|
||||||
|
export default function isRequestTypeProxied(
|
||||||
|
type: ProxyRequestType,
|
||||||
|
params: ProxyRequestParams
|
||||||
|
): boolean {
|
||||||
|
if (type === ProxyRequestType.Passport) {
|
||||||
|
if (params.isChromium && !params.optimizedProxiesEnabled) {
|
||||||
|
return params.passportLevel >= 0;
|
||||||
|
} else {
|
||||||
|
return params.passportLevel >= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === ProxyRequestType.Usher) {
|
||||||
|
if (params.optimizedProxiesEnabled) {
|
||||||
|
if (params.isChromium && params.fullModeEnabled === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!params.isChromium && params.isFlagged === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return params.passportLevel >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === ProxyRequestType.VideoWeaver) {
|
||||||
|
if (params.optimizedProxiesEnabled) {
|
||||||
|
if (params.isChromium && params.fullModeEnabled === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!params.isChromium && params.isFlagged === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === ProxyRequestType.GraphQLToken) {
|
||||||
|
if (params.isChromium) {
|
||||||
|
return params.passportLevel >= 1;
|
||||||
|
} else {
|
||||||
|
return params.passportLevel >= 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === ProxyRequestType.GraphQLIntegrity) {
|
||||||
|
if (params.optimizedProxiesEnabled) {
|
||||||
|
return params.passportLevel >= 2;
|
||||||
|
} else {
|
||||||
|
return params.passportLevel >= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === ProxyRequestType.GraphQL) {
|
||||||
|
// Proxy all GQL requests when passport is unoptimized official+ (Chromium)
|
||||||
|
// or unoptimized diplomatic+ (Firefox).
|
||||||
|
if (
|
||||||
|
params.isChromium &&
|
||||||
|
!params.optimizedProxiesEnabled &&
|
||||||
|
params.passportLevel >= 1
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!params.isChromium &&
|
||||||
|
!params.optimizedProxiesEnabled &&
|
||||||
|
params.passportLevel >= 2
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Proxy flagged GQL requests when passport is official+ (Chromium) or
|
||||||
|
// ordinary+ (Firefox).
|
||||||
|
if (params.isChromium && params.fullModeEnabled === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!params.isChromium && params.isFlagged === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (params.isChromium) {
|
||||||
|
return params.passportLevel >= 1;
|
||||||
|
} else {
|
||||||
|
return params.passportLevel >= 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === ProxyRequestType.TwitchWebpage) {
|
||||||
|
return params.passportLevel >= 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
|
import ip from "ip";
|
||||||
import type { ProxyInfo } from "../../types";
|
import type { ProxyInfo } from "../../types";
|
||||||
|
|
||||||
export default function getProxyInfoFromUrl(
|
export function getProxyInfoFromUrl(
|
||||||
url: string
|
url: string
|
||||||
): ProxyInfo & { type: "http"; host: string; port: number } {
|
): ProxyInfo & { type: "http"; host: string; port: number } {
|
||||||
const lastIndexOfAt = url.lastIndexOf("@");
|
const lastIndexOfAt = url.lastIndexOf("@");
|
||||||
@ -16,6 +17,9 @@ export default function getProxyInfoFromUrl(
|
|||||||
host = hostname.substring(0, lastIndexOfColon);
|
host = hostname.substring(0, lastIndexOfColon);
|
||||||
port = Number(hostname.substring(lastIndexOfColon + 1, hostname.length));
|
port = Number(hostname.substring(lastIndexOfColon + 1, hostname.length));
|
||||||
}
|
}
|
||||||
|
if (host.startsWith("[") && host.endsWith("]")) {
|
||||||
|
host = host.substring(1, host.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
let username: string | undefined = undefined;
|
let username: string | undefined = undefined;
|
||||||
let password: string | undefined = undefined;
|
let password: string | undefined = undefined;
|
||||||
@ -57,3 +61,21 @@ function getLastIndexOfColon(hostname: string): number {
|
|||||||
}
|
}
|
||||||
return lastIndexOfColon;
|
return lastIndexOfColon;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getUrlFromProxyInfo(proxyInfo: ProxyInfo): string {
|
||||||
|
const { host, port, username, password } = proxyInfo;
|
||||||
|
if (!host) return "";
|
||||||
|
let url = "";
|
||||||
|
if (username && password) {
|
||||||
|
url = `${username}:${password}@`;
|
||||||
|
} else if (username) {
|
||||||
|
url = `${username}@`;
|
||||||
|
}
|
||||||
|
if (ip.isV6Format(host)) {
|
||||||
|
url += `[${host}]`;
|
||||||
|
} else {
|
||||||
|
url += host;
|
||||||
|
}
|
||||||
|
if (port) url += `:${port}`;
|
||||||
|
return url;
|
||||||
|
}
|
@ -1,5 +1,7 @@
|
|||||||
import store from "../../store";
|
import store from "../../store";
|
||||||
import getProxyInfoFromUrl from "./getProxyInfoFromUrl";
|
import { ProxyRequestType } from "../../types";
|
||||||
|
import isRequestTypeProxied from "./isRequestTypeProxied";
|
||||||
|
import { getProxyInfoFromUrl, getUrlFromProxyInfo } from "./proxyInfo";
|
||||||
import {
|
import {
|
||||||
passportHostRegex,
|
passportHostRegex,
|
||||||
twitchGqlHostRegex,
|
twitchGqlHostRegex,
|
||||||
@ -9,29 +11,66 @@ import {
|
|||||||
} from "./regexes";
|
} from "./regexes";
|
||||||
import updateDnsResponses from "./updateDnsResponses";
|
import updateDnsResponses from "./updateDnsResponses";
|
||||||
|
|
||||||
export function updateProxySettings() {
|
export function updateProxySettings(requestFilter?: ProxyRequestType[]) {
|
||||||
const { proxyTwitchWebpage, proxyUsherRequests } = store.state;
|
const { optimizedProxiesEnabled, passportLevel } = store.state;
|
||||||
|
|
||||||
const proxies = store.state.optimizedProxiesEnabled
|
const proxies = optimizedProxiesEnabled
|
||||||
? store.state.optimizedProxies
|
? store.state.optimizedProxies
|
||||||
: store.state.normalProxies;
|
: store.state.normalProxies;
|
||||||
const proxyInfoString = getProxyInfoStringFromUrls(proxies);
|
const proxyInfoString = getProxyInfoStringFromUrls(proxies);
|
||||||
|
|
||||||
|
const getRequestParams = (requestType: ProxyRequestType) => ({
|
||||||
|
isChromium: true,
|
||||||
|
optimizedProxiesEnabled: optimizedProxiesEnabled,
|
||||||
|
passportLevel: passportLevel,
|
||||||
|
fullModeEnabled:
|
||||||
|
!optimizedProxiesEnabled ||
|
||||||
|
(requestFilter != null && requestFilter.includes(requestType)),
|
||||||
|
});
|
||||||
|
const proxyPassportRequests = isRequestTypeProxied(
|
||||||
|
ProxyRequestType.Passport,
|
||||||
|
getRequestParams(ProxyRequestType.Passport)
|
||||||
|
);
|
||||||
|
const proxyUsherRequests = isRequestTypeProxied(
|
||||||
|
ProxyRequestType.Usher,
|
||||||
|
getRequestParams(ProxyRequestType.Usher)
|
||||||
|
);
|
||||||
|
const proxyVideoWeaverRequests = isRequestTypeProxied(
|
||||||
|
ProxyRequestType.VideoWeaver,
|
||||||
|
getRequestParams(ProxyRequestType.VideoWeaver)
|
||||||
|
);
|
||||||
|
const proxyGraphQLRequests = isRequestTypeProxied(
|
||||||
|
ProxyRequestType.GraphQL,
|
||||||
|
getRequestParams(ProxyRequestType.GraphQL)
|
||||||
|
);
|
||||||
|
const proxyTwitchWebpageRequests = isRequestTypeProxied(
|
||||||
|
ProxyRequestType.TwitchWebpage,
|
||||||
|
getRequestParams(ProxyRequestType.TwitchWebpage)
|
||||||
|
);
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
mode: "pac_script",
|
mode: "pac_script",
|
||||||
pacScript: {
|
pacScript: {
|
||||||
data: `
|
data: `
|
||||||
function FindProxyForURL(url, host) {
|
function FindProxyForURL(url, host) {
|
||||||
// Twitch webpage & GraphQL requests.
|
// Passport requests.
|
||||||
if (${proxyTwitchWebpage} && (${twitchTvHostRegex}.test(host) || ${twitchGqlHostRegex}.test(host))) {
|
if (${proxyPassportRequests} && ${passportHostRegex}.test(host)) {
|
||||||
return "${proxyInfoString}";
|
return "${proxyInfoString}";
|
||||||
}
|
}
|
||||||
// Passport & Usher requests.
|
// Usher requests.
|
||||||
if (${proxyUsherRequests} && (${passportHostRegex}.test(host) || ${usherHostRegex}.test(host))) {
|
if (${proxyUsherRequests} && ${usherHostRegex}.test(host)) {
|
||||||
return "${proxyInfoString}";
|
return "${proxyInfoString}";
|
||||||
}
|
}
|
||||||
// Video Weaver requests.
|
// Video Weaver requests.
|
||||||
if (${videoWeaverHostRegex}.test(host)) {
|
if (${proxyVideoWeaverRequests} && ${videoWeaverHostRegex}.test(host)) {
|
||||||
|
return "${proxyInfoString}";
|
||||||
|
}
|
||||||
|
// GraphQL requests.
|
||||||
|
if (${proxyGraphQLRequests} && ${twitchGqlHostRegex}.test(host)) {
|
||||||
|
return "${proxyInfoString}";
|
||||||
|
}
|
||||||
|
// Twitch webpage requests.
|
||||||
|
if (${proxyTwitchWebpageRequests} && ${twitchTvHostRegex}.test(host)) {
|
||||||
return "${proxyInfoString}";
|
return "${proxyInfoString}";
|
||||||
}
|
}
|
||||||
return "DIRECT";
|
return "DIRECT";
|
||||||
@ -44,6 +83,7 @@ export function updateProxySettings() {
|
|||||||
console.log(
|
console.log(
|
||||||
`⚙️ Proxying requests through one of: ${proxies.toString() || "<empty>"}`
|
`⚙️ Proxying requests through one of: ${proxies.toString() || "<empty>"}`
|
||||||
);
|
);
|
||||||
|
store.state.chromiumProxyActive = true;
|
||||||
updateDnsResponses();
|
updateDnsResponses();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -52,7 +92,12 @@ function getProxyInfoStringFromUrls(urls: string[]): string {
|
|||||||
return [
|
return [
|
||||||
...urls.map(url => {
|
...urls.map(url => {
|
||||||
const proxyInfo = getProxyInfoFromUrl(url);
|
const proxyInfo = getProxyInfoFromUrl(url);
|
||||||
return `PROXY ${proxyInfo.host}:${proxyInfo.port}`;
|
return `PROXY ${getUrlFromProxyInfo({
|
||||||
|
...proxyInfo,
|
||||||
|
// Don't include username/password in PAC script.
|
||||||
|
username: undefined,
|
||||||
|
password: undefined,
|
||||||
|
})}`;
|
||||||
}),
|
}),
|
||||||
"DIRECT",
|
"DIRECT",
|
||||||
].join("; ");
|
].join("; ");
|
||||||
@ -61,5 +106,6 @@ function getProxyInfoStringFromUrls(urls: string[]): string {
|
|||||||
export function clearProxySettings() {
|
export function clearProxySettings() {
|
||||||
chrome.proxy.settings.clear({ scope: "regular" }, function () {
|
chrome.proxy.settings.clear({ scope: "regular" }, function () {
|
||||||
console.log("⚙️ Proxy settings cleared");
|
console.log("⚙️ Proxy settings cleared");
|
||||||
|
store.state.chromiumProxyActive = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
export const passportHostRegex = /^passport\.twitch\.tv$/i;
|
export const passportHostRegex = /^passport\.twitch\.tv$/i;
|
||||||
export const twitchApiChannelNameRegex = /\/hls\/(.+)\.m3u8/i;
|
export const twitchApiChannelNameRegex = /\/hls\/(.+)\.m3u8/i;
|
||||||
export const twitchChannelNameRegex =
|
export const twitchChannelNameRegex =
|
||||||
/^https?:\/\/(?:www|m)\.twitch\.tv\/(?:videos\/|popout\/)?(\w+)/i;
|
/^https?:\/\/(?:www|m)\.twitch\.tv\/(?:videos\/|popout\/)?((?!(?:directory|jobs|p|privacy|store|turbo)\b)\w+)/i;
|
||||||
export const twitchGqlHostRegex = /^gql\.twitch\.tv$/i;
|
export const twitchGqlHostRegex = /^gql\.twitch\.tv$/i;
|
||||||
export const twitchTvHostRegex = /^(?:www|m)\.twitch\.tv$/i;
|
export const twitchTvHostRegex = /^(?:www|m)\.twitch\.tv$/i;
|
||||||
export const usherHostRegex = /^usher\.ttvnw\.net$/i;
|
export const usherHostRegex = /^usher\.ttvnw\.net$/i;
|
||||||
|
8
src/common/ts/toAbsoluteUrl.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export default function toAbsoluteUrl(url: string): string {
|
||||||
|
try {
|
||||||
|
const Url = new URL(url, location.href);
|
||||||
|
return Url.href;
|
||||||
|
} catch {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
import ip from "ip";
|
import ip from "ip";
|
||||||
import store from "../../store";
|
import store from "../../store";
|
||||||
import type { DnsResponse } from "../../types";
|
import type { DnsResponse } from "../../types";
|
||||||
import getProxyInfoFromUrl from "./getProxyInfoFromUrl";
|
import { getProxyInfoFromUrl } from "./proxyInfo";
|
||||||
|
|
||||||
export default async function updateDnsResponses() {
|
export default async function updateDnsResponses() {
|
||||||
const proxies = [
|
const proxies = [
|
||||||
|
@ -1,18 +1,24 @@
|
|||||||
import pageScriptURL from "url:../page/page.ts";
|
import pageScriptURL from "url:../page/page.ts";
|
||||||
import workerScriptURL from "url:../page/worker.ts";
|
import workerScriptURL from "url:../page/worker.ts";
|
||||||
|
import browser, { Storage } from "webextension-polyfill";
|
||||||
import findChannelFromTwitchTvUrl from "../common/ts/findChannelFromTwitchTvUrl";
|
import findChannelFromTwitchTvUrl from "../common/ts/findChannelFromTwitchTvUrl";
|
||||||
import isChromium from "../common/ts/isChromium";
|
import isChromium from "../common/ts/isChromium";
|
||||||
import { getStreamStatus, setStreamStatus } from "../common/ts/streamStatus";
|
import { getStreamStatus, setStreamStatus } from "../common/ts/streamStatus";
|
||||||
import store from "../store";
|
import store from "../store";
|
||||||
|
import { State } from "../store/types";
|
||||||
|
import { MessageType } from "../types";
|
||||||
|
|
||||||
console.info("[TTV LOL PRO] 🚀 Content script running.");
|
console.info("[TTV LOL PRO] Content script running.");
|
||||||
|
|
||||||
injectPageScript();
|
if (isChromium) injectPageScript();
|
||||||
|
// Firefox uses FilterResponseData to inject the page script.
|
||||||
|
|
||||||
if (store.readyState === "complete") onStoreReady();
|
if (store.readyState === "complete") onStoreLoad();
|
||||||
else store.addEventListener("load", onStoreReady);
|
else store.addEventListener("load", onStoreLoad);
|
||||||
|
store.addEventListener("change", onStoreChange);
|
||||||
|
|
||||||
window.addEventListener("message", onMessage);
|
browser.runtime.onMessage.addListener(onBackgroundMessage);
|
||||||
|
window.addEventListener("message", onPageMessage);
|
||||||
|
|
||||||
function injectPageScript() {
|
function injectPageScript() {
|
||||||
// From https://stackoverflow.com/a/9517879
|
// From https://stackoverflow.com/a/9517879
|
||||||
@ -29,19 +35,10 @@ function injectPageScript() {
|
|||||||
// Please note that this does NOT involve remote code execution. The injected scripts are bundled
|
// Please note that this does NOT involve remote code execution. The injected scripts are bundled
|
||||||
// with the extension. The `url:` imports above are used to get the runtime URLs of the respective scripts.
|
// with the extension. The `url:` imports above are used to get the runtime URLs of the respective scripts.
|
||||||
// Additionally, there is no custom Content Security Policy (CSP) in use.
|
// Additionally, there is no custom Content Security Policy (CSP) in use.
|
||||||
(document.head || document.documentElement).append(script); // Note: Despite what the TS types say, `document.head` can be `null`.
|
(document.head || document.documentElement).prepend(script); // Note: Despite what the TS types say, `document.head` can be `null`.
|
||||||
}
|
}
|
||||||
|
|
||||||
function onStoreReady() {
|
function onStoreLoad() {
|
||||||
// Send store state to page script.
|
|
||||||
const message = {
|
|
||||||
type: "StoreReady",
|
|
||||||
state: JSON.parse(JSON.stringify(store.state)),
|
|
||||||
};
|
|
||||||
window.postMessage({
|
|
||||||
type: "PageScriptMessage",
|
|
||||||
message,
|
|
||||||
});
|
|
||||||
// Clear stats for stream on page load/reload.
|
// Clear stats for stream on page load/reload.
|
||||||
clearStats();
|
clearStats();
|
||||||
}
|
}
|
||||||
@ -51,32 +48,113 @@ function onStoreReady() {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
function clearStats() {
|
function clearStats() {
|
||||||
// TODO: Clear stats on navigation.
|
|
||||||
const channelName = findChannelFromTwitchTvUrl(location.href);
|
const channelName = findChannelFromTwitchTvUrl(location.href);
|
||||||
if (!channelName) return;
|
if (!channelName) return;
|
||||||
|
|
||||||
if (store.state.streamStatuses.hasOwnProperty(channelName)) {
|
if (store.state.streamStatuses.hasOwnProperty(channelName)) {
|
||||||
store.state.streamStatuses[channelName].stats = {
|
setStreamStatus(channelName, {
|
||||||
proxied: 0,
|
proxied: false,
|
||||||
notProxied: 0,
|
reason: "",
|
||||||
};
|
});
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
`[TTV LOL PRO] Cleared stats for channel '${channelName}' (content script).`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onStoreChange(changes: Record<string, Storage.StorageChange>) {
|
||||||
|
const changedKeys = Object.keys(changes) as (keyof State)[];
|
||||||
|
// This is mainly to reduce the amount of messages sent to the page script.
|
||||||
|
// (Also to reduce the number of console logs.)
|
||||||
|
const ignoredKeys: (keyof State)[] = [
|
||||||
|
"adLog",
|
||||||
|
"dnsResponses",
|
||||||
|
"openedTwitchTabs",
|
||||||
|
"streamStatuses",
|
||||||
|
"videoWeaverUrlsByChannel",
|
||||||
|
];
|
||||||
|
if (changedKeys.every(key => ignoredKeys.includes(key))) return;
|
||||||
|
console.log("[TTV LOL PRO] Store changed:", changes);
|
||||||
|
window.postMessage({
|
||||||
|
type: MessageType.PageScriptMessage,
|
||||||
|
message: {
|
||||||
|
type: MessageType.GetStoreStateResponse,
|
||||||
|
state: JSON.parse(JSON.stringify(store.state)),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBackgroundMessage(message: any) {
|
||||||
|
switch (message.type) {
|
||||||
|
case MessageType.EnableFullModeResponse:
|
||||||
|
window.postMessage({
|
||||||
|
type: MessageType.PageScriptMessage,
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
window.postMessage({
|
||||||
|
type: MessageType.WorkerScriptMessage,
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMessage(event: MessageEvent) {
|
function onPageMessage(event: MessageEvent) {
|
||||||
if (event.source !== window) return;
|
if (event.data?.type !== MessageType.ContentScriptMessage) return;
|
||||||
if (event.data?.type === "UsherResponse") {
|
|
||||||
const { channel, videoWeaverUrls, proxyCountry } = event.data;
|
const message = event.data?.message;
|
||||||
// Update Video Weaver URLs.
|
if (!message) return;
|
||||||
store.state.videoWeaverUrlsByChannel[channel] = [
|
|
||||||
...(store.state.videoWeaverUrlsByChannel[channel] ?? []),
|
switch (message.type) {
|
||||||
...videoWeaverUrls,
|
case MessageType.GetStoreState:
|
||||||
];
|
const sendStoreState = () => {
|
||||||
// Update proxy country.
|
window.postMessage({
|
||||||
const streamStatus = getStreamStatus(channel);
|
type: MessageType.PageScriptMessage,
|
||||||
setStreamStatus(channel, {
|
message: {
|
||||||
...(streamStatus ?? { proxied: false, reason: "" }),
|
type: MessageType.GetStoreStateResponse,
|
||||||
proxyCountry,
|
state: JSON.parse(JSON.stringify(store.state)),
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
if (store.readyState === "complete") sendStoreState();
|
||||||
|
else store.addEventListener("load", sendStoreState);
|
||||||
|
break;
|
||||||
|
case MessageType.EnableFullMode:
|
||||||
|
try {
|
||||||
|
browser.runtime.sendMessage(message);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"[TTV LOL PRO] Failed to send EnableFullMode message",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case MessageType.DisableFullMode:
|
||||||
|
try {
|
||||||
|
browser.runtime.sendMessage(message);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"[TTV LOL PRO] Failed to send DisableFullMode message",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case MessageType.UsherResponse:
|
||||||
|
const { channel, videoWeaverUrls, proxyCountry } = message;
|
||||||
|
// Update Video Weaver URLs.
|
||||||
|
store.state.videoWeaverUrlsByChannel[channel] = [
|
||||||
|
...(store.state.videoWeaverUrlsByChannel[channel] ?? []),
|
||||||
|
...videoWeaverUrls,
|
||||||
|
];
|
||||||
|
// Update proxy country.
|
||||||
|
const streamStatus = getStreamStatus(channel);
|
||||||
|
setStreamStatus(channel, {
|
||||||
|
...(streamStatus ?? { proxied: false, reason: "" }),
|
||||||
|
proxyCountry,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case MessageType.ClearStats:
|
||||||
|
clearStats();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 45 KiB |
73
src/m3u8-parser.d.ts
vendored
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
declare module "m3u8-parser" {
|
||||||
|
// https://github.com/videojs/m3u8-parser#parsed-output
|
||||||
|
interface Manifest {
|
||||||
|
allowCache: boolean;
|
||||||
|
endList?: boolean;
|
||||||
|
mediaSequence?: number;
|
||||||
|
dateRanges: [];
|
||||||
|
discontinuitySequence?: number;
|
||||||
|
playlistType?: string;
|
||||||
|
custom?: {};
|
||||||
|
playlists?: {
|
||||||
|
attributes: {
|
||||||
|
"FRAME-RATE": number;
|
||||||
|
VIDEO: string;
|
||||||
|
CODECS: string;
|
||||||
|
RESOLUTION: {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
BANDWIDTH: number;
|
||||||
|
};
|
||||||
|
uri: string;
|
||||||
|
timeline: number;
|
||||||
|
}[];
|
||||||
|
mediaGroups?: {
|
||||||
|
AUDIO: {};
|
||||||
|
VIDEO: {};
|
||||||
|
"CLOSED-CAPTIONS": {};
|
||||||
|
SUBTITLES: {};
|
||||||
|
};
|
||||||
|
dateTimeString?: string;
|
||||||
|
dateTimeObject?: Date;
|
||||||
|
targetDuration?: number;
|
||||||
|
totalDuration?: number;
|
||||||
|
discontinuityStarts: number[];
|
||||||
|
segments: {
|
||||||
|
title: string;
|
||||||
|
byterange: {
|
||||||
|
length: number;
|
||||||
|
offset: number;
|
||||||
|
};
|
||||||
|
duration: number;
|
||||||
|
programDateTime: number;
|
||||||
|
attributes: {};
|
||||||
|
discontinuity: number;
|
||||||
|
uri: string;
|
||||||
|
timeline: number;
|
||||||
|
key: {
|
||||||
|
method: string;
|
||||||
|
uri: string;
|
||||||
|
iv: string;
|
||||||
|
};
|
||||||
|
map: {
|
||||||
|
uri: string;
|
||||||
|
byterange: {
|
||||||
|
length: number;
|
||||||
|
offset: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
"cue-out": string;
|
||||||
|
"cue-out-cont": string;
|
||||||
|
"cue-in": string;
|
||||||
|
custom: {};
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Parser {
|
||||||
|
constructor();
|
||||||
|
push(chunk: string): void;
|
||||||
|
end(): void;
|
||||||
|
manifest: Manifest;
|
||||||
|
}
|
||||||
|
}
|
@ -2,7 +2,8 @@
|
|||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "TTV LOL PRO",
|
"name": "TTV LOL PRO",
|
||||||
"description": "TTV LOL PRO removes most livestream ads from Twitch.",
|
"description": "TTV LOL PRO removes most livestream ads from Twitch.",
|
||||||
"version": "2.2.3",
|
"homepage_url": "https://github.com/younesaassila/ttv-lol-pro",
|
||||||
|
"version": "2.3.0",
|
||||||
"background": {
|
"background": {
|
||||||
"service_worker": "background/background.ts",
|
"service_worker": "background/background.ts",
|
||||||
"type": "module"
|
"type": "module"
|
||||||
@ -18,7 +19,7 @@
|
|||||||
},
|
},
|
||||||
"action": {
|
"action": {
|
||||||
"default_icon": {
|
"default_icon": {
|
||||||
"128": "images/brand/icon.png"
|
"128": "common/images/brand/icon.png"
|
||||||
},
|
},
|
||||||
"default_title": "TTV LOL PRO",
|
"default_title": "TTV LOL PRO",
|
||||||
"default_popup": "popup/menu.html"
|
"default_popup": "popup/menu.html"
|
||||||
@ -31,7 +32,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icons": {
|
"icons": {
|
||||||
"128": "images/brand/icon.png"
|
"128": "common/images/brand/icon.png"
|
||||||
},
|
},
|
||||||
"options_ui": {
|
"options_ui": {
|
||||||
"browser_style": false,
|
"browser_style": false,
|
||||||
|
@ -2,14 +2,15 @@
|
|||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"name": "TTV LOL PRO",
|
"name": "TTV LOL PRO",
|
||||||
"description": "TTV LOL PRO removes most livestream ads from Twitch.",
|
"description": "TTV LOL PRO removes most livestream ads from Twitch.",
|
||||||
"version": "2.2.3",
|
"homepage_url": "https://github.com/younesaassila/ttv-lol-pro",
|
||||||
|
"version": "2.3.0",
|
||||||
"background": {
|
"background": {
|
||||||
"scripts": ["background/background.ts"],
|
"scripts": ["background/background.ts"],
|
||||||
"persistent": false
|
"persistent": false
|
||||||
},
|
},
|
||||||
"browser_action": {
|
"browser_action": {
|
||||||
"default_icon": {
|
"default_icon": {
|
||||||
"128": "images/brand/icon.png"
|
"128": "common/images/brand/icon.png"
|
||||||
},
|
},
|
||||||
"default_title": "TTV LOL PRO",
|
"default_title": "TTV LOL PRO",
|
||||||
"default_popup": "popup/menu.html"
|
"default_popup": "popup/menu.html"
|
||||||
@ -28,7 +29,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icons": {
|
"icons": {
|
||||||
"128": "images/brand/icon.png"
|
"128": "common/images/brand/icon.png"
|
||||||
},
|
},
|
||||||
"options_ui": {
|
"options_ui": {
|
||||||
"browser_style": false,
|
"browser_style": false,
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
|
import Bowser from "bowser";
|
||||||
|
import browser from "webextension-polyfill";
|
||||||
import $ from "../common/ts/$";
|
import $ from "../common/ts/$";
|
||||||
import { readFile, saveFile } from "../common/ts/file";
|
import { readFile, saveFile } from "../common/ts/file";
|
||||||
import getProxyInfoFromUrl from "../common/ts/getProxyInfoFromUrl";
|
import findChannelFromTwitchTvUrl from "../common/ts/findChannelFromTwitchTvUrl";
|
||||||
|
import isChannelWhitelisted from "../common/ts/isChannelWhitelisted";
|
||||||
import isChromium from "../common/ts/isChromium";
|
import isChromium from "../common/ts/isChromium";
|
||||||
|
import isRequestTypeProxied from "../common/ts/isRequestTypeProxied";
|
||||||
|
import { getProxyInfoFromUrl } from "../common/ts/proxyInfo";
|
||||||
import {
|
import {
|
||||||
clearProxySettings,
|
clearProxySettings,
|
||||||
updateProxySettings,
|
updateProxySettings,
|
||||||
@ -10,7 +15,7 @@ import sendAdLog from "../common/ts/sendAdLog";
|
|||||||
import store from "../store";
|
import store from "../store";
|
||||||
import getDefaultState from "../store/getDefaultState";
|
import getDefaultState from "../store/getDefaultState";
|
||||||
import type { State } from "../store/types";
|
import type { State } from "../store/types";
|
||||||
import type { KeyOfType } from "../types";
|
import { KeyOfType, ProxyRequestType } from "../types";
|
||||||
|
|
||||||
//#region Types
|
//#region Types
|
||||||
type AllowedResult = [boolean, string?];
|
type AllowedResult = [boolean, string?];
|
||||||
@ -31,29 +36,45 @@ type ListOptions = {
|
|||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region HTML Elements
|
//#region HTML Elements
|
||||||
// Proxy settings
|
// Import/Export
|
||||||
const proxyUsherRequestsCheckboxElement = $(
|
const exportButtonElement = $("#export-button") as HTMLButtonElement;
|
||||||
"#proxy-usher-requests-checkbox"
|
const importButtonElement = $("#import-button") as HTMLButtonElement;
|
||||||
|
const resetButtonElement = $("#reset-button") as HTMLButtonElement;
|
||||||
|
// Passport
|
||||||
|
const passportLevelSliderElement = $(
|
||||||
|
"#passport-level-slider"
|
||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
const proxyTwitchWebpageCheckboxElement = $(
|
const passportLevelWarningElement = $("#passport-level-warning") as HTMLElement;
|
||||||
"#proxy-twitch-webpage-checkbox"
|
|
||||||
) as HTMLInputElement;
|
|
||||||
const anonymousModeLiElement = $("#anonymous-mode-li") as HTMLLIElement;
|
|
||||||
const anonymousModeCheckboxElement = $(
|
const anonymousModeCheckboxElement = $(
|
||||||
"#anonymous-mode-checkbox"
|
"#anonymous-mode-checkbox"
|
||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
// Whitelisted channels
|
// Proxy usage
|
||||||
const whitelistedChannelsSectionElement = $(
|
const passportLevelProxyUsageElement = $(
|
||||||
"#whitelisted-channels-section"
|
"#passport-level-proxy-usage"
|
||||||
|
) as HTMLDetailsElement;
|
||||||
|
const passportLevelProxyUsageSummaryElement = $(
|
||||||
|
"#passport-level-proxy-usage-summary"
|
||||||
) as HTMLElement;
|
) as HTMLElement;
|
||||||
|
const passportLevelProxyUsagePassportElement = $(
|
||||||
|
"#passport-level-proxy-usage-passport"
|
||||||
|
) as HTMLTableCellElement;
|
||||||
|
const passportLevelProxyUsageUsherElement = $(
|
||||||
|
"#passport-level-proxy-usage-usher"
|
||||||
|
) as HTMLTableCellElement;
|
||||||
|
const passportLevelProxyUsageVideoWeaverElement = $(
|
||||||
|
"#passport-level-proxy-usage-video-weaver"
|
||||||
|
) as HTMLTableCellElement;
|
||||||
|
const passportLevelProxyUsageGqlElement = $(
|
||||||
|
"#passport-level-proxy-usage-gql"
|
||||||
|
) as HTMLTableCellElement;
|
||||||
|
const passportLevelProxyUsageWwwElement = $(
|
||||||
|
"#passport-level-proxy-usage-www"
|
||||||
|
) as HTMLTableCellElement;
|
||||||
|
// Whitelisted channels
|
||||||
const whitelistedChannelsListElement = $(
|
const whitelistedChannelsListElement = $(
|
||||||
"#whitelisted-channels-list"
|
"#whitelisted-channels-list"
|
||||||
) as HTMLUListElement;
|
) as HTMLUListElement;
|
||||||
$;
|
|
||||||
// Proxies
|
// Proxies
|
||||||
const optimizedProxiesDivElement = $(
|
|
||||||
"#optimized-proxies-div"
|
|
||||||
) as HTMLDivElement;
|
|
||||||
const optimizedProxiesInputElement = $("#optimized") as HTMLInputElement;
|
const optimizedProxiesInputElement = $("#optimized") as HTMLInputElement;
|
||||||
const optimizedProxiesListElement = $(
|
const optimizedProxiesListElement = $(
|
||||||
"#optimized-proxies-list"
|
"#optimized-proxies-list"
|
||||||
@ -61,7 +82,6 @@ const optimizedProxiesListElement = $(
|
|||||||
const normalProxiesInputElement = $("#normal") as HTMLInputElement;
|
const normalProxiesInputElement = $("#normal") as HTMLInputElement;
|
||||||
const normalProxiesListElement = $("#normal-proxies-list") as HTMLOListElement;
|
const normalProxiesListElement = $("#normal-proxies-list") as HTMLOListElement;
|
||||||
// Ad log
|
// Ad log
|
||||||
const adLogSectionElement = $("#ad-log-section") as HTMLElement;
|
|
||||||
const adLogEnabledCheckboxElement = $(
|
const adLogEnabledCheckboxElement = $(
|
||||||
"#ad-log-enabled-checkbox"
|
"#ad-log-enabled-checkbox"
|
||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
@ -70,13 +90,15 @@ const adLogExportButtonElement = $(
|
|||||||
"#ad-log-export-button"
|
"#ad-log-export-button"
|
||||||
) as HTMLButtonElement;
|
) as HTMLButtonElement;
|
||||||
const adLogClearButtonElement = $("#ad-log-clear-button") as HTMLButtonElement;
|
const adLogClearButtonElement = $("#ad-log-clear-button") as HTMLButtonElement;
|
||||||
// Import/Export
|
// Troubleshooting
|
||||||
const exportButtonElement = $("#export-button") as HTMLButtonElement;
|
const twitchTabsReportButtonElement = $(
|
||||||
const importButtonElement = $("#import-button") as HTMLButtonElement;
|
"#twitch-tabs-report-button"
|
||||||
const resetButtonElement = $("#reset-button") as HTMLButtonElement;
|
) as HTMLButtonElement;
|
||||||
const unsetPacScriptButtonElement = $(
|
const unsetPacScriptButtonElement = $(
|
||||||
"#unset-pac-script-button"
|
"#unset-pac-script-button"
|
||||||
) as HTMLButtonElement;
|
) as HTMLButtonElement;
|
||||||
|
// Footer
|
||||||
|
const versionElement = $("#version") as HTMLParagraphElement;
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
const DEFAULT_STATE = Object.freeze(getDefaultState());
|
const DEFAULT_STATE = Object.freeze(getDefaultState());
|
||||||
@ -96,52 +118,55 @@ if (store.readyState === "complete") main();
|
|||||||
else store.addEventListener("load", main);
|
else store.addEventListener("load", main);
|
||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
// Proxy settings
|
// Remove elements that are only for Chromium or Firefox.
|
||||||
proxyUsherRequestsCheckboxElement.checked = store.state.proxyUsherRequests;
|
document
|
||||||
proxyUsherRequestsCheckboxElement.addEventListener("change", () => {
|
.querySelectorAll(isChromium ? ".firefox-only" : ".chromium-only")
|
||||||
const checked = proxyUsherRequestsCheckboxElement.checked;
|
.forEach(element => element.remove());
|
||||||
store.state.proxyUsherRequests = checked;
|
// Passport
|
||||||
|
passportLevelSliderElement.value = store.state.passportLevel.toString();
|
||||||
|
passportLevelSliderElement.addEventListener("input", () => {
|
||||||
|
store.state.passportLevel = parseInt(passportLevelSliderElement.value);
|
||||||
if (isChromium && store.state.chromiumProxyActive) {
|
if (isChromium && store.state.chromiumProxyActive) {
|
||||||
updateProxySettings();
|
updateProxySettings();
|
||||||
}
|
}
|
||||||
|
updateProxyUsage();
|
||||||
});
|
});
|
||||||
proxyTwitchWebpageCheckboxElement.checked = store.state.proxyTwitchWebpage;
|
updateProxyUsage();
|
||||||
proxyTwitchWebpageCheckboxElement.addEventListener("change", () => {
|
anonymousModeCheckboxElement.checked = store.state.anonymousMode;
|
||||||
store.state.proxyTwitchWebpage = proxyTwitchWebpageCheckboxElement.checked;
|
anonymousModeCheckboxElement.addEventListener("change", () => {
|
||||||
if (isChromium && store.state.chromiumProxyActive) {
|
store.state.anonymousMode = anonymousModeCheckboxElement.checked;
|
||||||
updateProxySettings();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
// TODO: Figure out why this feature doesn't work in Chromium.
|
|
||||||
if (isChromium) {
|
|
||||||
anonymousModeLiElement.style.display = "none";
|
|
||||||
} else {
|
|
||||||
anonymousModeCheckboxElement.checked = store.state.anonymousMode;
|
|
||||||
anonymousModeCheckboxElement.addEventListener("change", () => {
|
|
||||||
store.state.anonymousMode = anonymousModeCheckboxElement.checked;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Whitelisted channels
|
// Whitelisted channels
|
||||||
listInit(whitelistedChannelsListElement, "whitelistedChannels", {
|
listInit(whitelistedChannelsListElement, "whitelistedChannels", {
|
||||||
getAlreadyExistsAlertMessage: channelName =>
|
getAlreadyExistsAlertMessage: channelName =>
|
||||||
`'${channelName}' is already whitelisted`,
|
`'${channelName}' is already whitelisted`,
|
||||||
getPromptPlaceholder: () => "Enter a channel name…",
|
getPromptPlaceholder: () => "Enter a channel name…",
|
||||||
|
isAddAllowed(text) {
|
||||||
|
if (!/^[a-z0-9_]+$/i.test(text)) {
|
||||||
|
return [false, `'${text}' is not a valid channel name`];
|
||||||
|
}
|
||||||
|
return [true];
|
||||||
|
},
|
||||||
|
isEditAllowed(text) {
|
||||||
|
if (!/^[a-z0-9_]+$/i.test(text)) {
|
||||||
|
return [false, `'${text}' is not a valid channel name`];
|
||||||
|
}
|
||||||
|
return [true];
|
||||||
|
},
|
||||||
});
|
});
|
||||||
// Proxies
|
// Proxies
|
||||||
if (isChromium) {
|
if (store.state.optimizedProxiesEnabled)
|
||||||
optimizedProxiesDivElement.style.display = "none";
|
optimizedProxiesInputElement.checked = true;
|
||||||
normalProxiesInputElement.checked = true;
|
else normalProxiesInputElement.checked = true;
|
||||||
} else {
|
const onProxyTypeChange = () => {
|
||||||
if (store.state.optimizedProxiesEnabled)
|
store.state.optimizedProxiesEnabled = optimizedProxiesInputElement.checked;
|
||||||
optimizedProxiesInputElement.checked = true;
|
if (isChromium && store.state.chromiumProxyActive) {
|
||||||
else normalProxiesInputElement.checked = true;
|
updateProxySettings();
|
||||||
const onProxyTypeChange = () => {
|
}
|
||||||
store.state.optimizedProxiesEnabled =
|
updateProxyUsage();
|
||||||
optimizedProxiesInputElement.checked;
|
};
|
||||||
};
|
optimizedProxiesInputElement.addEventListener("change", onProxyTypeChange);
|
||||||
optimizedProxiesInputElement.addEventListener("change", onProxyTypeChange);
|
normalProxiesInputElement.addEventListener("change", onProxyTypeChange);
|
||||||
normalProxiesInputElement.addEventListener("change", onProxyTypeChange);
|
|
||||||
}
|
|
||||||
listInit(optimizedProxiesListElement, "optimizedProxies", {
|
listInit(optimizedProxiesListElement, "optimizedProxies", {
|
||||||
getPromptPlaceholder: insertMode => {
|
getPromptPlaceholder: insertMode => {
|
||||||
if (insertMode == "prepend") return "Enter a proxy URL… (Primary)";
|
if (insertMode == "prepend") return "Enter a proxy URL… (Primary)";
|
||||||
@ -149,6 +174,11 @@ function main() {
|
|||||||
},
|
},
|
||||||
isAddAllowed: isOptimizedProxyUrlAllowed,
|
isAddAllowed: isOptimizedProxyUrlAllowed,
|
||||||
isEditAllowed: isOptimizedProxyUrlAllowed,
|
isEditAllowed: isOptimizedProxyUrlAllowed,
|
||||||
|
onEdit() {
|
||||||
|
if (isChromium && store.state.chromiumProxyActive) {
|
||||||
|
updateProxySettings();
|
||||||
|
}
|
||||||
|
},
|
||||||
hidePromptMarker: true,
|
hidePromptMarker: true,
|
||||||
insertMode: "both",
|
insertMode: "both",
|
||||||
});
|
});
|
||||||
@ -168,16 +198,85 @@ function main() {
|
|||||||
insertMode: "both",
|
insertMode: "both",
|
||||||
});
|
});
|
||||||
// Ad log
|
// Ad log
|
||||||
if (isChromium) {
|
adLogEnabledCheckboxElement.checked = store.state.adLogEnabled;
|
||||||
adLogSectionElement.style.display = "none";
|
adLogEnabledCheckboxElement.addEventListener("change", () => {
|
||||||
|
store.state.adLogEnabled = adLogEnabledCheckboxElement.checked;
|
||||||
|
});
|
||||||
|
// Footer
|
||||||
|
versionElement.textContent = `Version ${
|
||||||
|
browser.runtime.getManifest().version
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateProxyUsage() {
|
||||||
|
const requestParams = {
|
||||||
|
isChromium: isChromium,
|
||||||
|
optimizedProxiesEnabled: store.state.optimizedProxiesEnabled,
|
||||||
|
passportLevel: store.state.passportLevel,
|
||||||
|
fullModeEnabled: false,
|
||||||
|
isFlagged: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Proxy usage label.
|
||||||
|
let usageScore = 0;
|
||||||
|
// Unoptimized mode penalty.
|
||||||
|
if (!store.state.optimizedProxiesEnabled) usageScore += 1;
|
||||||
|
// GraphQL integrity penalty and warning.
|
||||||
|
if (isRequestTypeProxied(ProxyRequestType.GraphQLIntegrity, requestParams)) {
|
||||||
|
usageScore += 1;
|
||||||
|
passportLevelWarningElement.style.display = "block";
|
||||||
} else {
|
} else {
|
||||||
adLogEnabledCheckboxElement.checked = store.state.adLogEnabled;
|
passportLevelWarningElement.style.display = "none";
|
||||||
adLogEnabledCheckboxElement.addEventListener("change", () => {
|
|
||||||
store.state.adLogEnabled = adLogEnabledCheckboxElement.checked;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if (!isChromium) {
|
switch (usageScore) {
|
||||||
unsetPacScriptButtonElement.style.display = "none";
|
case 0:
|
||||||
|
passportLevelProxyUsageSummaryElement.textContent = "🙂 Low proxy usage";
|
||||||
|
passportLevelProxyUsageElement.dataset.usage = "low";
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
passportLevelProxyUsageSummaryElement.textContent =
|
||||||
|
"😐 Medium proxy usage";
|
||||||
|
passportLevelProxyUsageElement.dataset.usage = "medium";
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
passportLevelProxyUsageSummaryElement.textContent = "🙁 High proxy usage";
|
||||||
|
passportLevelProxyUsageElement.dataset.usage = "high";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Passport
|
||||||
|
if (isRequestTypeProxied(ProxyRequestType.Passport, requestParams)) {
|
||||||
|
passportLevelProxyUsagePassportElement.textContent = "All";
|
||||||
|
} else {
|
||||||
|
passportLevelProxyUsagePassportElement.textContent = "None";
|
||||||
|
}
|
||||||
|
// Usher
|
||||||
|
passportLevelProxyUsageUsherElement.textContent = "All";
|
||||||
|
// Video Weaver
|
||||||
|
if (isRequestTypeProxied(ProxyRequestType.VideoWeaver, requestParams)) {
|
||||||
|
passportLevelProxyUsageVideoWeaverElement.textContent = "All";
|
||||||
|
} else {
|
||||||
|
passportLevelProxyUsageVideoWeaverElement.textContent = "Few";
|
||||||
|
}
|
||||||
|
// GraphQL
|
||||||
|
if (isRequestTypeProxied(ProxyRequestType.GraphQL, requestParams)) {
|
||||||
|
passportLevelProxyUsageGqlElement.textContent = "All";
|
||||||
|
} else if (
|
||||||
|
isRequestTypeProxied(ProxyRequestType.GraphQLIntegrity, requestParams)
|
||||||
|
) {
|
||||||
|
passportLevelProxyUsageGqlElement.textContent = "Some";
|
||||||
|
} else if (
|
||||||
|
isRequestTypeProxied(ProxyRequestType.GraphQLToken, requestParams)
|
||||||
|
) {
|
||||||
|
passportLevelProxyUsageGqlElement.textContent = "Few";
|
||||||
|
} else {
|
||||||
|
passportLevelProxyUsageGqlElement.textContent = "None";
|
||||||
|
}
|
||||||
|
// WWW
|
||||||
|
if (isRequestTypeProxied(ProxyRequestType.TwitchWebpage, requestParams)) {
|
||||||
|
passportLevelProxyUsageWwwElement.textContent = "All";
|
||||||
|
} else {
|
||||||
|
passportLevelProxyUsageWwwElement.textContent = "None";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -425,33 +524,6 @@ function _listPrompt(
|
|||||||
if (options.focusPrompt) promptInput.focus();
|
if (options.focusPrompt) promptInput.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
adLogSendButtonElement.addEventListener("click", async () => {
|
|
||||||
const success = await sendAdLog();
|
|
||||||
if (success === null) {
|
|
||||||
return alert("No log entries to send.");
|
|
||||||
}
|
|
||||||
if (!success) {
|
|
||||||
return alert("Failed to send log.");
|
|
||||||
}
|
|
||||||
alert("Log sent successfully.");
|
|
||||||
});
|
|
||||||
|
|
||||||
adLogExportButtonElement.addEventListener("click", () => {
|
|
||||||
saveFile(
|
|
||||||
"ttv-lol-pro_ad-log.json",
|
|
||||||
JSON.stringify(store.state.adLog),
|
|
||||||
"application/json;charset=utf-8"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
adLogClearButtonElement.addEventListener("click", () => {
|
|
||||||
const confirmation = confirm(
|
|
||||||
"Are you sure you want to clear the ad log? This cannot be undone."
|
|
||||||
);
|
|
||||||
if (!confirmation) return;
|
|
||||||
store.state.adLog = [];
|
|
||||||
});
|
|
||||||
|
|
||||||
exportButtonElement.addEventListener("click", () => {
|
exportButtonElement.addEventListener("click", () => {
|
||||||
saveFile(
|
saveFile(
|
||||||
"ttv-lol-pro_backup.json",
|
"ttv-lol-pro_backup.json",
|
||||||
@ -461,8 +533,7 @@ exportButtonElement.addEventListener("click", () => {
|
|||||||
normalProxies: store.state.normalProxies,
|
normalProxies: store.state.normalProxies,
|
||||||
optimizedProxies: store.state.optimizedProxies,
|
optimizedProxies: store.state.optimizedProxies,
|
||||||
optimizedProxiesEnabled: store.state.optimizedProxiesEnabled,
|
optimizedProxiesEnabled: store.state.optimizedProxiesEnabled,
|
||||||
proxyTwitchWebpage: store.state.proxyTwitchWebpage,
|
passportLevel: store.state.passportLevel,
|
||||||
proxyUsherRequests: store.state.proxyUsherRequests,
|
|
||||||
whitelistedChannels: store.state.whitelistedChannels,
|
whitelistedChannels: store.state.whitelistedChannels,
|
||||||
} as Partial<State>),
|
} as Partial<State>),
|
||||||
"application/json;charset=utf-8"
|
"application/json;charset=utf-8"
|
||||||
@ -495,6 +566,13 @@ importButtonElement.addEventListener("click", async () => {
|
|||||||
item != null ? isNormalProxyUrlAllowed(item.toString())[0] : false
|
item != null ? isNormalProxyUrlAllowed(item.toString())[0] : false
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (key === "passportLevel") {
|
||||||
|
if (typeof value !== "number") {
|
||||||
|
filteredValue = DEFAULT_STATE.passportLevel;
|
||||||
|
} else {
|
||||||
|
filteredValue = Math.min(Math.max(value, 0), 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
store.state[key] = filteredValue;
|
store.state[key] = filteredValue;
|
||||||
}
|
}
|
||||||
@ -513,6 +591,163 @@ resetButtonElement.addEventListener("click", () => {
|
|||||||
window.location.reload(); // Reload page to update UI.
|
window.location.reload(); // Reload page to update UI.
|
||||||
});
|
});
|
||||||
|
|
||||||
|
adLogSendButtonElement.addEventListener("click", async () => {
|
||||||
|
const success = await sendAdLog();
|
||||||
|
if (success === null) {
|
||||||
|
return alert("No log entries to send.");
|
||||||
|
}
|
||||||
|
if (!success) {
|
||||||
|
return alert("Failed to send log.");
|
||||||
|
}
|
||||||
|
alert("Log sent successfully.");
|
||||||
|
});
|
||||||
|
|
||||||
|
adLogExportButtonElement.addEventListener("click", () => {
|
||||||
|
saveFile(
|
||||||
|
"ttv-lol-pro_ad-log.json",
|
||||||
|
JSON.stringify(store.state.adLog),
|
||||||
|
"application/json;charset=utf-8"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
adLogClearButtonElement.addEventListener("click", () => {
|
||||||
|
const confirmation = confirm(
|
||||||
|
"Are you sure you want to clear the ad log? This cannot be undone."
|
||||||
|
);
|
||||||
|
if (!confirmation) return;
|
||||||
|
store.state.adLog = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
twitchTabsReportButtonElement.addEventListener("click", async () => {
|
||||||
|
let report = "**Twitch Tabs Report**\n\n";
|
||||||
|
|
||||||
|
const extensionInfo = await browser.management.getSelf();
|
||||||
|
const userAgentParser = Bowser.getParser(window.navigator.userAgent);
|
||||||
|
report += `Extension: ${extensionInfo.name} v${extensionInfo.version} (${extensionInfo.installType})\n`;
|
||||||
|
report += `Browser: ${userAgentParser.getBrowserName()} ${userAgentParser.getBrowserVersion()} (${userAgentParser.getOSName()} ${userAgentParser.getOSVersion()})\n\n`;
|
||||||
|
|
||||||
|
const openedTabs = await browser.tabs.query({
|
||||||
|
url: ["https://www.twitch.tv/*", "https://m.twitch.tv/*"],
|
||||||
|
});
|
||||||
|
const detectedTabs = store.state.openedTwitchTabs;
|
||||||
|
|
||||||
|
// Print all opened tabs.
|
||||||
|
report += `Opened Twitch tabs (${openedTabs.length}):\n`;
|
||||||
|
for (const tab of openedTabs) {
|
||||||
|
report += `- ${tab.url || tab.pendingUrl} (id: ${tab.id}, windowId: ${
|
||||||
|
tab.windowId
|
||||||
|
})\n`;
|
||||||
|
}
|
||||||
|
report += "\n";
|
||||||
|
|
||||||
|
// Whitelisted tabs in `openedTabs`.
|
||||||
|
const openedWhitelistedTabs = openedTabs.filter(tab => {
|
||||||
|
const url = tab.url || tab.pendingUrl;
|
||||||
|
if (!url) return false;
|
||||||
|
const channelName = findChannelFromTwitchTvUrl(url);
|
||||||
|
const isWhitelisted = channelName
|
||||||
|
? isChannelWhitelisted(channelName)
|
||||||
|
: false;
|
||||||
|
return isWhitelisted;
|
||||||
|
});
|
||||||
|
report += `Out of the ${openedTabs.length} opened Twitch tabs, ${
|
||||||
|
openedWhitelistedTabs.length
|
||||||
|
} ${openedWhitelistedTabs.length === 1 ? "is" : "are"} whitelisted:\n`;
|
||||||
|
for (const tab of openedWhitelistedTabs) {
|
||||||
|
report += `- ${tab.url || tab.pendingUrl} (id: ${tab.id}, windowId: ${
|
||||||
|
tab.windowId
|
||||||
|
})\n`;
|
||||||
|
}
|
||||||
|
report += "\n";
|
||||||
|
|
||||||
|
// Check for missing tabs in `detectedTabs`.
|
||||||
|
const missingTabs = openedTabs.filter(
|
||||||
|
tab => !detectedTabs.some(extensionTab => extensionTab.id === tab.id)
|
||||||
|
);
|
||||||
|
if (missingTabs.length > 0) {
|
||||||
|
report += `The following Twitch tabs are missing from \`store.state.openedTwitchTabs\`:\n`;
|
||||||
|
for (const tab of missingTabs) {
|
||||||
|
report += `- ${tab.url || tab.pendingUrl} (id: ${tab.id}, windowId: ${
|
||||||
|
tab.windowId
|
||||||
|
})\n`;
|
||||||
|
}
|
||||||
|
report += "\n";
|
||||||
|
} else {
|
||||||
|
report +=
|
||||||
|
"All opened Twitch tabs are present in `store.state.openedTwitchTabs`.\n\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for extra tabs in `detectedTabs`.
|
||||||
|
const extraTabs = detectedTabs.filter(
|
||||||
|
extensionTab => !openedTabs.some(tab => tab.id === extensionTab.id)
|
||||||
|
);
|
||||||
|
if (extraTabs.length > 0) {
|
||||||
|
report += `The following Twitch tabs are extra in \`store.state.openedTwitchTabs\`:\n`;
|
||||||
|
for (const tab of extraTabs) {
|
||||||
|
report += `- ${tab.url || tab.pendingUrl} (id: ${tab.id}, windowId: ${
|
||||||
|
tab.windowId
|
||||||
|
})\n`;
|
||||||
|
}
|
||||||
|
report += "\n";
|
||||||
|
} else {
|
||||||
|
report += "No extra Twitch tabs in `store.state.openedTwitchTabs`.\n\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whitelisted tabs in `detectedTabs`.
|
||||||
|
const detectedWhitelistedTabs = detectedTabs.filter(tab => {
|
||||||
|
const url = tab.url || tab.pendingUrl;
|
||||||
|
if (!url) return false;
|
||||||
|
const channelName = findChannelFromTwitchTvUrl(url);
|
||||||
|
const isWhitelisted = channelName
|
||||||
|
? isChannelWhitelisted(channelName)
|
||||||
|
: false;
|
||||||
|
return isWhitelisted;
|
||||||
|
});
|
||||||
|
report += `Out of the ${
|
||||||
|
detectedTabs.length
|
||||||
|
} Twitch tabs in \`store.state.openedTwitchTabs\`, ${
|
||||||
|
detectedWhitelistedTabs.length
|
||||||
|
} ${detectedWhitelistedTabs.length === 1 ? "is" : "are"} whitelisted:\n`;
|
||||||
|
for (const tab of detectedWhitelistedTabs) {
|
||||||
|
report += `- ${tab.url || tab.pendingUrl} (id: ${tab.id}, windowId: ${
|
||||||
|
tab.windowId
|
||||||
|
})\n`;
|
||||||
|
}
|
||||||
|
report += "\n";
|
||||||
|
|
||||||
|
// Should the PAC script be set?
|
||||||
|
const allTabsAreWhitelisted =
|
||||||
|
openedWhitelistedTabs.length === openedTabs.length;
|
||||||
|
const shouldSetPacScript = openedTabs.length > 0 && !allTabsAreWhitelisted;
|
||||||
|
report += `Should the PAC script be set? ${
|
||||||
|
shouldSetPacScript ? "Yes" : "No"
|
||||||
|
}\n`;
|
||||||
|
report += `Is the PAC script set? ${
|
||||||
|
store.state.chromiumProxyActive ? "Yes" : "No"
|
||||||
|
}\n`;
|
||||||
|
report += "\n";
|
||||||
|
|
||||||
|
let fixed = false;
|
||||||
|
if (shouldSetPacScript && !store.state.chromiumProxyActive) {
|
||||||
|
store.state.openedTwitchTabs = openedTabs;
|
||||||
|
updateProxySettings();
|
||||||
|
fixed = true;
|
||||||
|
report += "Fixed issue by setting the PAC script.\n";
|
||||||
|
} else if (!shouldSetPacScript && store.state.chromiumProxyActive) {
|
||||||
|
store.state.openedTwitchTabs = openedTabs;
|
||||||
|
clearProxySettings();
|
||||||
|
fixed = true;
|
||||||
|
report += "Fixed issue by unsetting the PAC script.\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
saveFile("ttv-lol-pro_tabs-report.txt", report, "text/plain;charset=utf-8");
|
||||||
|
alert(
|
||||||
|
`Report saved ${
|
||||||
|
fixed ? "and issue fixed " : ""
|
||||||
|
}successfully. Please send the report to the developer (on Discord or GitHub).`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
unsetPacScriptButtonElement.addEventListener("click", () => {
|
unsetPacScriptButtonElement.addEventListener("click", () => {
|
||||||
if (isChromium) {
|
if (isChromium) {
|
||||||
clearProxySettings();
|
clearProxySettings();
|
||||||
|
@ -5,191 +5,239 @@
|
|||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Options - TTV LOL PRO</title>
|
<title>Options - TTV LOL PRO</title>
|
||||||
<link rel="icon" href="../images/brand/favicon.ico" />
|
<link rel="icon" href="../common/images/brand/favicon.ico" />
|
||||||
<link rel="stylesheet" href="../common/css/boilerplate.css" />
|
<link rel="stylesheet" href="../common/css/boilerplate.css" />
|
||||||
<link rel="stylesheet" href="./style.css" />
|
<link rel="stylesheet" href="./style.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<div class="wrapper">
|
||||||
<img src="../images/brand/icon.png" alt="Icon of TTV LOL PRO" />
|
<header>
|
||||||
<h1>Options</h1>
|
<div class="title-container">
|
||||||
</header>
|
<img
|
||||||
|
src="../common/images/brand/icon.png"
|
||||||
|
alt="Icon of TTV LOL PRO"
|
||||||
|
class="icon"
|
||||||
|
/>
|
||||||
|
<h1 class="title">Options</h1>
|
||||||
|
</div>
|
||||||
|
<div id="buttons-container">
|
||||||
|
<button id="export-button">Back up to file…</button>
|
||||||
|
<button id="import-button">Restore from file…</button>
|
||||||
|
<button id="reset-button">Reset to default settings…</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<!-- Proxy Usher requests -->
|
<!-- Passport -->
|
||||||
<section class="section">
|
<section id="passport" class="section">
|
||||||
<h2>Passport</h2>
|
<h2>Passport</h2>
|
||||||
<div id="passport-container">
|
<div id="passport-level-container">
|
||||||
<img src="../images/passport.png" alt="TTV LOL PRO passport" />
|
<img
|
||||||
|
src="../common/images/passport.png"
|
||||||
|
alt="TTV LOL PRO passport"
|
||||||
|
id="passport-level-image"
|
||||||
|
/>
|
||||||
|
<div id="passport-level-slider-container">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
name="passport-level-slider"
|
||||||
|
id="passport-level-slider"
|
||||||
|
min="0"
|
||||||
|
max="2"
|
||||||
|
step="1"
|
||||||
|
list="passport-level-slider-datalist"
|
||||||
|
/>
|
||||||
|
<datalist id="passport-level-slider-datalist">
|
||||||
|
<option value="0" label="Ordinary"></option>
|
||||||
|
<option value="1" label="Official"></option>
|
||||||
|
<option value="2" label="Diplomatic"></option>
|
||||||
|
</datalist>
|
||||||
|
</div>
|
||||||
|
<details id="passport-level-proxy-usage">
|
||||||
|
<summary id="passport-level-proxy-usage-summary"></summary>
|
||||||
|
<table id="passport-level-proxy-usage-table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>passport.twitch.tv</td>
|
||||||
|
<td id="passport-level-proxy-usage-passport"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>usher.ttvnw.net</td>
|
||||||
|
<td id="passport-level-proxy-usage-usher"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>video-weaver.*.hls.ttvnw.net</td>
|
||||||
|
<td id="passport-level-proxy-usage-video-weaver"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>gql.twitch.tv</td>
|
||||||
|
<td id="passport-level-proxy-usage-gql"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>www.twitch.tv</td>
|
||||||
|
<td id="passport-level-proxy-usage-www"></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</details>
|
||||||
|
<small id="passport-level-warning">
|
||||||
|
<b>WARNING:</b>
|
||||||
|
Enabling this option could open up a range of features that are
|
||||||
|
not accessible in your region, including Predictions, Prime
|
||||||
|
Subscriptions, or currency changes, among others. Please be aware
|
||||||
|
that you take full responsibility for the content passing through
|
||||||
|
your proxy or public ones. It's important to note that in certain
|
||||||
|
countries, features like Predictions might be categorized as
|
||||||
|
gambling, making them inappropriate for minors. If your country
|
||||||
|
doesn't support these features, there are legitimate reasons for
|
||||||
|
it. Stay informed and conduct online research accordingly.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
<ul class="options-list">
|
<ul class="options-list">
|
||||||
<li>
|
<li>
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
name="proxy-usher-requests-checkbox"
|
|
||||||
id="proxy-usher-requests-checkbox"
|
|
||||||
/>
|
|
||||||
<label for="proxy-usher-requests-checkbox">
|
|
||||||
Use my TTV LOL PRO passport
|
|
||||||
</label>
|
|
||||||
<br />
|
|
||||||
<small>
|
|
||||||
Browse Twitch as a TTV LOL PRO citizen! This option enables
|
|
||||||
proxying of <code>passport.twitch.tv</code> and
|
|
||||||
<code>usher.ttvnw.net</code> requests.
|
|
||||||
</small>
|
|
||||||
<br />
|
|
||||||
<small>
|
|
||||||
This option is not an on/off switch. TTV LOL PRO will still
|
|
||||||
proxy <code>video-weaver.*.hls.ttvnw.net</code> requests even if
|
|
||||||
this option is disabled.
|
|
||||||
</small>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
name="proxy-twitch-webpage-checkbox"
|
|
||||||
id="proxy-twitch-webpage-checkbox"
|
|
||||||
/>
|
|
||||||
<label for="proxy-twitch-webpage-checkbox">
|
|
||||||
Make the passport a
|
|
||||||
<a
|
|
||||||
href="https://en.wikipedia.org/wiki/Laissez-passer"
|
|
||||||
target="_blank"
|
|
||||||
>laissez-passer</a
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
<br />
|
|
||||||
<small>
|
|
||||||
This option enables proxying of <code>www.twitch.tv</code> and
|
|
||||||
<code>gql.twitch.tv</code> requests.
|
|
||||||
</small>
|
|
||||||
<br />
|
|
||||||
<small>
|
|
||||||
<b>WARNING:</b>
|
|
||||||
Enabling this option may unlock unavailable features in your
|
|
||||||
country, like Predictions, Prime Subscriptions, or currency
|
|
||||||
changes, among others. You assume full responsibility for the
|
|
||||||
content passing through your proxy or public ones, and note that
|
|
||||||
some features, like Predictions, might be considered gambling in
|
|
||||||
certain countries, making them illegal for minors. If your
|
|
||||||
country lacks these features, there are valid reasons for it.
|
|
||||||
Stay informed and conduct online research accordingly.
|
|
||||||
</small>
|
|
||||||
<br />
|
|
||||||
<small>
|
|
||||||
<b>
|
|
||||||
ONLY ENABLE THIS OPTION IF YOU ARE EXPERIENCING ISSUES WITH
|
|
||||||
TTV LOL PRO'S PASSPORT!
|
|
||||||
</b>
|
|
||||||
</small>
|
|
||||||
</li>
|
|
||||||
<li id="anonymous-mode-li">
|
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
name="anonymous-mode-checkbox"
|
name="anonymous-mode-checkbox"
|
||||||
id="anonymous-mode-checkbox"
|
id="anonymous-mode-checkbox"
|
||||||
/>
|
/>
|
||||||
<label for="anonymous-mode-checkbox">
|
<label for="anonymous-mode-checkbox">Anonymous mode</label>
|
||||||
Redact my passport information
|
|
||||||
</label>
|
|
||||||
<span class="tag">Recommended</span>
|
|
||||||
<br />
|
<br />
|
||||||
<small>
|
<small>
|
||||||
Watch streams as if you were logged out. This option removes
|
Watch streams as if you were logged out. This option might help
|
||||||
authentication headers from requests to Twitch.
|
reduce the number of "Commercial break in progress" ads.
|
||||||
</small>
|
</small>
|
||||||
</li>
|
</li>
|
||||||
<small><b>Expiration date:</b> 2038-01-19T03:14:07.000Z</small>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</section>
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Whitelisted channels -->
|
<!-- Whitelisted channels -->
|
||||||
<section id="whitelisted-channels-section" class="section">
|
<section id="whitelisted-channels" class="section">
|
||||||
<h2>Whitelisted channels</h2>
|
<h2>Whitelisted channels</h2>
|
||||||
<small>
|
<small>
|
||||||
Support your favorite content creators by whitelisting their channels.
|
Support your favorite content creators by whitelisting their
|
||||||
On Chromium-based browsers, whitelisting only works when all opened
|
channels.
|
||||||
Twitch tabs are whitelisted channels.
|
</small>
|
||||||
</small>
|
<br class="chromium-only" />
|
||||||
<ul id="whitelisted-channels-list" class="store-list"></ul>
|
<small class="chromium-only">
|
||||||
</section>
|
On Chromium-based browsers, whitelisting only works when all opened
|
||||||
|
Twitch tabs are whitelisted channels.
|
||||||
|
</small>
|
||||||
|
<ul id="whitelisted-channels-list" class="store-list"></ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Proxies -->
|
<!-- Proxies -->
|
||||||
<section class="section">
|
<section id="proxies" class="section">
|
||||||
<h2>Proxies</h2>
|
<h2>Proxies</h2>
|
||||||
<small>
|
<small>
|
||||||
Proxies listed below must be HTTP proxies in the format
|
Proxies listed below must be HTTP proxies in the format
|
||||||
<code>hostname:port</code>. To provide authentication credentials, use
|
<code>hostname:port</code>
|
||||||
the format <code>username:password@hostname:port</code>.
|
</small>
|
||||||
</small>
|
<br />
|
||||||
<br />
|
<small>
|
||||||
<small>
|
To provide authentication credentials, use the format
|
||||||
IPv6 addresses must be enclosed in square brackets, for example
|
<code>username:password@hostname:port</code>
|
||||||
<code>[::1]:8080</code>.
|
</small>
|
||||||
</small>
|
<br />
|
||||||
<br />
|
<fieldset>
|
||||||
<fieldset>
|
<div>
|
||||||
<div id="optimized-proxies-div">
|
<input
|
||||||
<input
|
type="radio"
|
||||||
type="radio"
|
name="proxy-mode"
|
||||||
name="proxy-mode"
|
id="optimized"
|
||||||
id="optimized"
|
value="optimized"
|
||||||
value="optimized"
|
checked
|
||||||
checked
|
/>
|
||||||
/>
|
<label for="optimized">Proxy ad requests only</label>
|
||||||
<label for="optimized">Proxy ad requests only</label>
|
<ol id="optimized-proxies-list" class="store-list" start="0"></ol>
|
||||||
<ol id="optimized-proxies-list" class="store-list" start="0"></ol>
|
</div>
|
||||||
</div>
|
<div>
|
||||||
<div>
|
<input
|
||||||
<input type="radio" name="proxy-mode" id="normal" value="normal" />
|
type="radio"
|
||||||
<label for="normal">Proxy all requests</label>
|
name="proxy-mode"
|
||||||
<ol id="normal-proxies-list" class="store-list" start="0"></ol>
|
id="normal"
|
||||||
</div>
|
value="normal"
|
||||||
</fieldset>
|
/>
|
||||||
<small>
|
<label for="normal">Proxy all requests</label>
|
||||||
Looking for other proxies? Check out the "<a
|
<ol id="normal-proxies-list" class="store-list" start="0"></ol>
|
||||||
href="https://github.com/younesaassila/ttv-lol-pro/discussions/130"
|
</div>
|
||||||
target="_blank"
|
</fieldset>
|
||||||
>List of other proxies</a
|
<small>
|
||||||
>" discussion on TTV LOL PRO's GitHub repository.
|
Looking for other proxies? Check out the "<a
|
||||||
</small>
|
href="https://github.com/younesaassila/ttv-lol-pro/discussions/130"
|
||||||
</section>
|
target="_blank"
|
||||||
|
>List of other proxies</a
|
||||||
|
>" discussion on TTV LOL PRO's GitHub repository.
|
||||||
|
</small>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Ad log -->
|
<!-- Ad log -->
|
||||||
<section id="ad-log-section" class="section">
|
<section id="ad-log" class="firefox-only section">
|
||||||
<h2>Ad log</h2>
|
<h2>Ad log</h2>
|
||||||
<small>
|
<small>
|
||||||
If enabled, TTV LOL PRO will log all ads that did not get blocked for
|
If enabled, TTV LOL PRO will log all ads that did not get blocked
|
||||||
debugging purposes. Entries are automatically removed after 7 days.
|
for debugging purposes. Entries are automatically removed after 7
|
||||||
</small>
|
days.
|
||||||
<ul class="options-list">
|
</small>
|
||||||
<li>
|
<ul class="options-list">
|
||||||
<input
|
<li>
|
||||||
type="checkbox"
|
<input
|
||||||
name="ad-log-enabled-checkbox"
|
type="checkbox"
|
||||||
id="ad-log-enabled-checkbox"
|
name="ad-log-enabled-checkbox"
|
||||||
/>
|
id="ad-log-enabled-checkbox"
|
||||||
<label for="ad-log-enabled-checkbox">Enable ad log</label>
|
/>
|
||||||
</li>
|
<label for="ad-log-enabled-checkbox">Enable ad log</label>
|
||||||
</ul>
|
</li>
|
||||||
<button id="ad-log-send-button" class="btn-primary">
|
</ul>
|
||||||
Send ad log to developer…
|
<button id="ad-log-send-button" class="btn-primary">
|
||||||
</button>
|
Send ad log to developer…
|
||||||
<button id="ad-log-export-button">Export ad log…</button>
|
</button>
|
||||||
<button id="ad-log-clear-button">Clear ad log</button>
|
<button id="ad-log-export-button">Export ad log…</button>
|
||||||
</section>
|
<button id="ad-log-clear-button">Clear ad log</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
<hr />
|
<!-- Troubleshooting -->
|
||||||
|
<section id="troubleshooting" class="chromium-only section">
|
||||||
|
<h2>Troubleshooting</h2>
|
||||||
|
<br />
|
||||||
|
<button id="twitch-tabs-report-button">
|
||||||
|
Generate Twitch tabs report…
|
||||||
|
</button>
|
||||||
|
<button id="unset-pac-script-button">Unset PAC script</button>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
<!-- Backup and restore -->
|
<footer>
|
||||||
<section class="section">
|
<nav>
|
||||||
<button id="export-button">Back up to file…</button>
|
<ul>
|
||||||
<button id="import-button">Restore from file…</button>
|
<li>
|
||||||
<button id="reset-button">Reset to default settings…</button>
|
<a
|
||||||
<button id="unset-pac-script-button">Unset PAC script</button>
|
href="https://github.com/younesaassila/ttv-lol-pro"
|
||||||
</section>
|
target="_blank"
|
||||||
</main>
|
rel="noopener noreferrer"
|
||||||
|
>Source code</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="https://github.com/younesaassila/ttv-lol-pro/releases"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>Changelog</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="https://github.com/younesaassila/ttv-lol-pro/blob/v2/PRIVACY.md"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>Privacy policy</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<small id="version"></small>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script type="module" src="./options.ts"></script>
|
<script type="module" src="./options.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
|
@ -1,14 +1,17 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
src: url("../fonts/Inter-VariableFont_slnt\,wght.ttf");
|
src: url("../common/fonts/Inter-VariableFont_slnt\,wght.ttf");
|
||||||
font-family: "Inter";
|
font-family: "Inter";
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
--wrapper-width: 1100px;
|
||||||
--font-primary: "Inter", "Roobert", "Helvetica Neue", Helvetica, Arial,
|
--font-primary: "Inter", "Roobert", "Helvetica Neue", Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
|
|
||||||
--brand-color: #aa51b8;
|
--brand-color: #aa51b8;
|
||||||
--ui-background-color: #151619;
|
--wrapper-box-shadow-color: #0c0c0e;
|
||||||
|
--wrapper-background-color: #151619;
|
||||||
|
--body-background-color: #0e0f11;
|
||||||
|
|
||||||
--text-primary: #e4e6e7;
|
--text-primary: #e4e6e7;
|
||||||
--text-secondary: #8d9296;
|
--text-secondary: #8d9296;
|
||||||
@ -18,6 +21,7 @@
|
|||||||
--input-border-color: #353840;
|
--input-border-color: #353840;
|
||||||
--input-text-primary: #c3c4ca;
|
--input-text-primary: #c3c4ca;
|
||||||
--input-text-secondary: #7a8085;
|
--input-text-secondary: #7a8085;
|
||||||
|
--input-max-width: 450px;
|
||||||
|
|
||||||
--button-background-color: #353840;
|
--button-background-color: #353840;
|
||||||
--button-background-color-hover: #464953;
|
--button-background-color-hover: #464953;
|
||||||
@ -26,20 +30,20 @@
|
|||||||
--link: #be68ce;
|
--link: #be68ce;
|
||||||
--link-hover: #cc88d8;
|
--link-hover: #cc88d8;
|
||||||
|
|
||||||
--header-height: 2.5rem;
|
--logo-height: 2.5rem;
|
||||||
|
|
||||||
|
--low-color: #06c157;
|
||||||
|
--low-bg-color: #1e2421;
|
||||||
|
--medium-color: #f9c643;
|
||||||
|
--medium-bg-color: #24221e;
|
||||||
|
--high-color: #f93e3e;
|
||||||
|
--high-bg-color: #241e1e;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
*,
|
||||||
margin: 1.5rem;
|
*::before,
|
||||||
background-color: var(--ui-background-color);
|
*::after {
|
||||||
color: var(--text-primary);
|
box-sizing: border-box;
|
||||||
accent-color: var(--brand-color);
|
|
||||||
font-size: 100%;
|
|
||||||
font-family: var(--font-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
::-moz-selection,
|
::-moz-selection,
|
||||||
@ -48,13 +52,79 @@ main {
|
|||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.font-weight-bold {
|
body {
|
||||||
font-weight: bold;
|
margin: 0;
|
||||||
|
background-image: url("../common/images/options_bg.png");
|
||||||
|
background-repeat: repeat;
|
||||||
|
background-color: var(--body-background-color);
|
||||||
|
color: var(--text-primary);
|
||||||
|
accent-color: var(--brand-color);
|
||||||
|
font-size: 100%;
|
||||||
|
font-family: var(--font-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldset {
|
.wrapper {
|
||||||
margin-top: 1rem;
|
position: relative;
|
||||||
border: 0;
|
left: 50%;
|
||||||
|
width: min(100%, var(--wrapper-width));
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background-color: var(--wrapper-background-color);
|
||||||
|
box-shadow: 0 0 32px var(--wrapper-box-shadow-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 1.5rem;
|
||||||
|
gap: 1rem;
|
||||||
|
border-bottom: 1px solid var(--input-border-color);
|
||||||
|
background-color: var(--wrapper-background-color);
|
||||||
|
}
|
||||||
|
header > .title-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
header > .title-container > .icon {
|
||||||
|
width: var(--logo-height);
|
||||||
|
height: var(--logo-height);
|
||||||
|
}
|
||||||
|
header > .title-container > .title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.75rem;
|
||||||
|
}
|
||||||
|
header > #buttons-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-top: 1px solid var(--input-border-color);
|
||||||
|
font-size: 9pt;
|
||||||
|
}
|
||||||
|
footer > nav > ul {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
gap: 1.5rem;
|
||||||
|
list-style-type: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
a,
|
a,
|
||||||
@ -63,7 +133,6 @@ a:visited {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition: color 100ms ease-in-out;
|
transition: color 100ms ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover,
|
a:hover,
|
||||||
a:visited:hover {
|
a:visited:hover {
|
||||||
color: var(--link-hover);
|
color: var(--link-hover);
|
||||||
@ -79,12 +148,10 @@ select {
|
|||||||
color: var(--input-text-primary);
|
color: var(--input-text-primary);
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="text"]:disabled {
|
input[type="text"]:disabled {
|
||||||
background-color: var(--input-background-color-disabled);
|
background-color: var(--input-background-color-disabled);
|
||||||
color: var(--input-text-secondary);
|
color: var(--input-text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="text"]::placeholder {
|
input[type="text"]::placeholder {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
@ -100,7 +167,6 @@ button {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 100ms ease-in-out;
|
transition: background-color 100ms ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="button"]:hover,
|
input[type="button"]:hover,
|
||||||
button:hover {
|
button:hover {
|
||||||
background-color: var(--button-background-color-hover);
|
background-color: var(--button-background-color-hover);
|
||||||
@ -110,7 +176,6 @@ button:hover {
|
|||||||
background-color: var(--brand-color);
|
background-color: var(--brand-color);
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary:hover {
|
.btn-primary:hover {
|
||||||
background-color: var(--link-hover);
|
background-color: var(--link-hover);
|
||||||
}
|
}
|
||||||
@ -119,32 +184,34 @@ input[type="checkbox"]:disabled + label {
|
|||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
margin-top: 1rem;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
small {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 9pt;
|
||||||
|
}
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
margin: 2.5rem 0;
|
margin: 2.5rem 0;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-top: 1px solid var(--input-border-color);
|
border-top: 1px solid var(--input-border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
header {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-start;
|
|
||||||
height: var(--header-height);
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
header > img {
|
|
||||||
max-height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section {
|
.section {
|
||||||
margin-top: 1.5rem;
|
margin: 0 0 3rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section > h2 {
|
.section > h2 {
|
||||||
|
margin-top: 0;
|
||||||
margin-bottom: 0.25rem;
|
margin-bottom: 0.25rem;
|
||||||
font-size: 1.15rem;
|
font-size: 1.3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag {
|
.tag {
|
||||||
@ -158,13 +225,9 @@ header > img {
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
small {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 9pt;
|
|
||||||
}
|
|
||||||
|
|
||||||
.store-list > li > input {
|
.store-list > li > input {
|
||||||
min-width: 400px;
|
width: 100%;
|
||||||
|
max-width: var(--input-max-width);
|
||||||
margin-bottom: 0.25rem;
|
margin-bottom: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -177,23 +240,162 @@ li.hide-marker::marker {
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.options-list > li {
|
.options-list > li {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.options-list > li > input[type="checkbox"] {
|
.options-list > li > input[type="checkbox"] {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: -1.6rem;
|
left: -1.6rem;
|
||||||
margin-top: 0.3rem;
|
margin-top: 0.3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#passport-container {
|
#passport-level-container {
|
||||||
display: flex;
|
display: grid;
|
||||||
|
grid-template-rows: auto auto auto;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
grid-template-areas:
|
||||||
|
"image slider"
|
||||||
|
". usage"
|
||||||
|
". warning";
|
||||||
|
column-gap: 1.25rem;
|
||||||
|
row-gap: 0;
|
||||||
|
align-items: center;
|
||||||
|
margin: 1rem 0 1.5rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#passport-container > img {
|
#passport-level-image {
|
||||||
height: 80px;
|
grid-area: image;
|
||||||
margin-top: 1rem;
|
height: 55px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#passport-level-slider-container {
|
||||||
|
grid-area: slider;
|
||||||
|
}
|
||||||
|
|
||||||
|
#passport-level-slider {
|
||||||
|
width: 100%;
|
||||||
|
max-width: var(--input-max-width);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#passport-level-slider-datalist {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
width: 100%;
|
||||||
|
max-width: var(--input-max-width);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
#passport-level-slider-datalist > option:first-child {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
#passport-level-slider-datalist > option:last-child {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
#passport-level-proxy-usage {
|
||||||
|
grid-area: usage;
|
||||||
|
width: 100%;
|
||||||
|
max-width: var(--input-max-width);
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--input-border-color);
|
||||||
|
border-radius: 18px;
|
||||||
|
background-color: var(--input-background-color);
|
||||||
|
}
|
||||||
|
#passport-level-proxy-usage[data-usage="low"] {
|
||||||
|
border-color: var(--low-color);
|
||||||
|
background-color: var(--low-bg-color);
|
||||||
|
}
|
||||||
|
#passport-level-proxy-usage[data-usage="medium"] {
|
||||||
|
border-color: var(--medium-color);
|
||||||
|
background-color: var(--medium-bg-color);
|
||||||
|
}
|
||||||
|
#passport-level-proxy-usage[data-usage="high"] {
|
||||||
|
border-color: var(--high-color);
|
||||||
|
background-color: var(--high-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#passport-level-proxy-usage-summary {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 100ms ease-in-out, color 100ms ease-in-out;
|
||||||
|
}
|
||||||
|
#passport-level-proxy-usage-summary::marker {
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
#passport-level-proxy-usage-summary::after {
|
||||||
|
display: block;
|
||||||
|
float: right;
|
||||||
|
transform: translateY(-15%) rotate(-45deg);
|
||||||
|
content: "∟";
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
#passport-level-proxy-usage[open] #passport-level-proxy-usage-summary::after {
|
||||||
|
display: block;
|
||||||
|
float: right;
|
||||||
|
transform: translateY(30%) rotate(135deg);
|
||||||
|
content: "∟";
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
#passport-level-proxy-usage[data-usage="low"]
|
||||||
|
#passport-level-proxy-usage-summary:hover {
|
||||||
|
background-color: var(--low-color);
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
#passport-level-proxy-usage[data-usage="medium"]
|
||||||
|
#passport-level-proxy-usage-summary:hover {
|
||||||
|
background-color: var(--medium-color);
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
#passport-level-proxy-usage[data-usage="high"]
|
||||||
|
#passport-level-proxy-usage-summary:hover {
|
||||||
|
background-color: var(--high-color);
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#passport-level-proxy-usage-table {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.5rem;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
#passport-level-proxy-usage-table > tbody > tr > td:nth-child(2) {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
#passport-level-warning {
|
||||||
|
display: none;
|
||||||
|
grid-area: warning;
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 800px) {
|
||||||
|
header {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 600px) {
|
||||||
|
main {
|
||||||
|
padding: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
header > #buttons-container {
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
header > #buttons-container > button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer > nav > ul {
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,36 +1,94 @@
|
|||||||
|
import * as m3u8Parser from "m3u8-parser";
|
||||||
import acceptFlag from "../common/ts/acceptFlag";
|
import acceptFlag from "../common/ts/acceptFlag";
|
||||||
|
import findChannelFromTwitchTvUrl from "../common/ts/findChannelFromTwitchTvUrl";
|
||||||
import findChannelFromUsherUrl from "../common/ts/findChannelFromUsherUrl";
|
import findChannelFromUsherUrl from "../common/ts/findChannelFromUsherUrl";
|
||||||
import generateRandomString from "../common/ts/generateRandomString";
|
import generateRandomString from "../common/ts/generateRandomString";
|
||||||
import getHostFromUrl from "../common/ts/getHostFromUrl";
|
import getHostFromUrl from "../common/ts/getHostFromUrl";
|
||||||
|
import isRequestTypeProxied from "../common/ts/isRequestTypeProxied";
|
||||||
import {
|
import {
|
||||||
twitchGqlHostRegex,
|
twitchGqlHostRegex,
|
||||||
usherHostRegex,
|
usherHostRegex,
|
||||||
videoWeaverHostRegex,
|
videoWeaverHostRegex,
|
||||||
videoWeaverUrlRegex,
|
|
||||||
} from "../common/ts/regexes";
|
} from "../common/ts/regexes";
|
||||||
import { State } from "../store/types";
|
import { MessageType, ProxyRequestType } from "../types";
|
||||||
|
import type { PageState, PlaybackAccessToken, UsherManifest } from "./types";
|
||||||
|
|
||||||
|
const IS_DEVELOPMENT = process.env.NODE_ENV == "development";
|
||||||
const NATIVE_FETCH = self.fetch;
|
const NATIVE_FETCH = self.fetch;
|
||||||
const IS_CHROMIUM = !!self.chrome;
|
|
||||||
|
|
||||||
export interface FetchOptions {
|
export function getFetch(pageState: PageState): typeof fetch {
|
||||||
scope: "page" | "worker";
|
let usherManifests: UsherManifest[] = [];
|
||||||
shouldWaitForStore: boolean;
|
let videoWeaverUrlsProxiedCount = new Map<string, number>(); // Used to count how many times each Video Weaver URL was proxied.
|
||||||
state?: State;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getFetch(options: FetchOptions): typeof fetch {
|
let cachedPlaybackTokenRequestHeaders: Map<string, string> | null = null; // Cached by page script.
|
||||||
// TODO: Clear variables on navigation.
|
let cachedPlaybackTokenRequestBody: string | null = null; // Cached by page script.
|
||||||
const knownVideoWeaverUrls = new Set<string>();
|
let cachedUsherRequestUrl: string | null = null; // Cached by worker script.
|
||||||
const videoWeaverUrlsToFlag = new Map<string, number>(); // Video Weaver URLs to flag -> number of times flagged.
|
|
||||||
const videoWeaverUrlsToIgnore = new Set<string>(); // No response check.
|
|
||||||
|
|
||||||
if (options.shouldWaitForStore) {
|
// Listen for NewPlaybackAccessToken messages from the worker script.
|
||||||
setTimeout(() => {
|
if (pageState.scope === "page") {
|
||||||
options.shouldWaitForStore = false;
|
self.addEventListener("message", async event => {
|
||||||
}, 5000);
|
if (event.data?.type !== MessageType.PageScriptMessage) return;
|
||||||
|
|
||||||
|
const message = event.data?.message;
|
||||||
|
if (!message) return;
|
||||||
|
|
||||||
|
switch (message.type) {
|
||||||
|
case MessageType.NewPlaybackAccessToken:
|
||||||
|
await waitForStore(pageState);
|
||||||
|
const newPlaybackAccessToken =
|
||||||
|
await fetchReplacementPlaybackAccessToken(
|
||||||
|
pageState,
|
||||||
|
cachedPlaybackTokenRequestHeaders,
|
||||||
|
cachedPlaybackTokenRequestBody
|
||||||
|
);
|
||||||
|
const message = {
|
||||||
|
type: MessageType.NewPlaybackAccessTokenResponse,
|
||||||
|
newPlaybackAccessToken,
|
||||||
|
};
|
||||||
|
pageState.twitchWorker?.postMessage({
|
||||||
|
type: MessageType.WorkerScriptMessage,
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Listen for ClearStats messages from the page script.
|
||||||
|
self.addEventListener("message", event => {
|
||||||
|
if (
|
||||||
|
event.data?.type !== MessageType.PageScriptMessage &&
|
||||||
|
event.data?.type !== MessageType.WorkerScriptMessage
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = event.data?.message;
|
||||||
|
if (!message) return;
|
||||||
|
|
||||||
|
switch (message.type) {
|
||||||
|
case MessageType.ClearStats:
|
||||||
|
console.log("[TTV LOL PRO] Cleared stats (getFetch).");
|
||||||
|
usherManifests = [];
|
||||||
|
cachedPlaybackTokenRequestHeaders = null;
|
||||||
|
cachedPlaybackTokenRequestBody = null;
|
||||||
|
cachedUsherRequestUrl = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// // Test Video Weaver URL replacement.
|
||||||
|
// if (IS_DEVELOPMENT && pageState.scope === "worker") {
|
||||||
|
// setTimeout(async () => {
|
||||||
|
// await waitForStore(pageState);
|
||||||
|
// updateVideoWeaverReplacementMap(
|
||||||
|
// pageState,
|
||||||
|
// cachedUsherRequestUrl,
|
||||||
|
// usherManifests[usherManifests.length - 1]
|
||||||
|
// );
|
||||||
|
// }, 30000);
|
||||||
|
// }
|
||||||
|
|
||||||
return async function fetch(
|
return async function fetch(
|
||||||
input: RequestInfo | URL,
|
input: RequestInfo | URL,
|
||||||
init?: RequestInit
|
init?: RequestInit
|
||||||
@ -52,6 +110,10 @@ export function getFetch(options: FetchOptions): typeof fetch {
|
|||||||
const host = getHostFromUrl(url);
|
const host = getHostFromUrl(url);
|
||||||
const headersMap = getHeadersMap(input, init);
|
const headersMap = getHeadersMap(input, init);
|
||||||
|
|
||||||
|
let isFlaggedRequest = false; // Whether or not the request should be proxied.
|
||||||
|
let request: Request | null = null; // Request can be overwritten.
|
||||||
|
let requestType: ProxyRequestType | null = null;
|
||||||
|
|
||||||
// Reading the request body can be expensive, so we only do it if we need to.
|
// Reading the request body can be expensive, so we only do it if we need to.
|
||||||
let requestBody: string | null | undefined = undefined;
|
let requestBody: string | null | undefined = undefined;
|
||||||
const readRequestBody = async (): Promise<string | null> => {
|
const readRequestBody = async (): Promise<string | null> => {
|
||||||
@ -62,107 +124,280 @@ export function getFetch(options: FetchOptions): typeof fetch {
|
|||||||
//#region Requests
|
//#region Requests
|
||||||
|
|
||||||
// Twitch GraphQL requests.
|
// Twitch GraphQL requests.
|
||||||
if (host != null && twitchGqlHostRegex.test(host)) {
|
graphql: if (host != null && twitchGqlHostRegex.test(host)) {
|
||||||
requestBody = await readRequestBody();
|
requestType = ProxyRequestType.GraphQL;
|
||||||
// Integrity requests.
|
|
||||||
if (url === "https://gql.twitch.tv/integrity") {
|
|
||||||
console.debug(
|
|
||||||
"[TTV LOL PRO] 🥅 Caught GraphQL integrity request. Flagging…"
|
|
||||||
);
|
|
||||||
flagRequest(headersMap);
|
|
||||||
}
|
|
||||||
// Requests with Client-Integrity header.
|
|
||||||
const integrityHeader = getHeaderFromMap(headersMap, "Client-Integrity");
|
|
||||||
if (integrityHeader != null) {
|
|
||||||
console.debug(
|
|
||||||
"[TTV LOL PRO] 🥅 Caught GraphQL request with Client-Integrity header. Flagging…"
|
|
||||||
);
|
|
||||||
flagRequest(headersMap);
|
|
||||||
}
|
|
||||||
// PlaybackAccessToken requests.
|
|
||||||
if (
|
|
||||||
requestBody != null &&
|
|
||||||
requestBody.includes("PlaybackAccessToken_Template")
|
|
||||||
) {
|
|
||||||
console.debug(
|
|
||||||
"[TTV LOL PRO] 🥅 Caught GraphQL PlaybackAccessToken_Template request. Flagging…"
|
|
||||||
);
|
|
||||||
|
|
||||||
while (options.shouldWaitForStore) await sleep(100);
|
//#region GraphQL integrity requests.
|
||||||
|
const integrityHeader = getHeaderFromMap(headersMap, "Client-Integrity");
|
||||||
|
const isIntegrityRequest = url === "https://gql.twitch.tv/integrity";
|
||||||
|
const isIntegrityHeaderRequest = integrityHeader != null;
|
||||||
|
if (isIntegrityRequest || isIntegrityHeaderRequest) {
|
||||||
|
await waitForStore(pageState);
|
||||||
|
const shouldFlagRequest = isRequestTypeProxied(
|
||||||
|
ProxyRequestType.GraphQLIntegrity,
|
||||||
|
{
|
||||||
|
isChromium: pageState.isChromium,
|
||||||
|
optimizedProxiesEnabled:
|
||||||
|
pageState.state?.optimizedProxiesEnabled ?? true,
|
||||||
|
passportLevel: pageState.state?.passportLevel ?? 0,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (shouldFlagRequest) {
|
||||||
|
if (isIntegrityRequest) {
|
||||||
|
console.debug("[TTV LOL PRO] Flagging GraphQL integrity request…");
|
||||||
|
isFlaggedRequest = true;
|
||||||
|
} else if (isIntegrityHeaderRequest) {
|
||||||
|
console.debug(
|
||||||
|
"[TTV LOL PRO] Flagging GraphQL request with Client-Integrity header…"
|
||||||
|
);
|
||||||
|
isFlaggedRequest = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break graphql;
|
||||||
|
}
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region GraphQL PlaybackAccessToken requests.
|
||||||
|
requestBody ??= await readRequestBody();
|
||||||
|
if (requestBody != null && requestBody.includes("PlaybackAccessToken")) {
|
||||||
|
// Cache the request headers and body for later use.
|
||||||
|
cachedPlaybackTokenRequestHeaders = headersMap;
|
||||||
|
cachedPlaybackTokenRequestBody = requestBody;
|
||||||
|
|
||||||
|
// Check if this is a livestream and if it's whitelisted.
|
||||||
let graphQlBody = null;
|
let graphQlBody = null;
|
||||||
try {
|
try {
|
||||||
graphQlBody = JSON.parse(requestBody);
|
graphQlBody = JSON.parse(requestBody);
|
||||||
} catch {}
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"[TTV LOL PRO] Failed to parse GraphQL request body:",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await waitForStore(pageState);
|
||||||
const channelName = graphQlBody?.variables?.login as string | undefined;
|
const channelName = graphQlBody?.variables?.login as string | undefined;
|
||||||
const whitelistedChannelsLower = options.state?.whitelistedChannels.map(
|
const isLivestream = graphQlBody?.variables?.isLive as
|
||||||
channel => channel.toLowerCase()
|
| boolean
|
||||||
);
|
| undefined;
|
||||||
|
const whitelistedChannelsLower =
|
||||||
|
pageState.state?.whitelistedChannels.map(channel =>
|
||||||
|
channel.toLowerCase()
|
||||||
|
);
|
||||||
const isWhitelisted =
|
const isWhitelisted =
|
||||||
channelName != null &&
|
channelName != null &&
|
||||||
whitelistedChannelsLower != null &&
|
whitelistedChannelsLower != null &&
|
||||||
whitelistedChannelsLower.includes(channelName.toLowerCase());
|
whitelistedChannelsLower.includes(channelName.toLowerCase());
|
||||||
|
|
||||||
if (options.state?.anonymousMode === true) {
|
// Check if we should flag this request.
|
||||||
if (!isWhitelisted) {
|
const shouldFlagRequest = isRequestTypeProxied(
|
||||||
console.log("[TTV LOL PRO] 🕵️ Anonymous mode is enabled.");
|
ProxyRequestType.GraphQLToken,
|
||||||
setHeaderToMap(headersMap, "Authorization", "undefined");
|
{
|
||||||
removeHeaderFromMap(headersMap, "Client-Session-Id");
|
isChromium: pageState.isChromium,
|
||||||
removeHeaderFromMap(headersMap, "Client-Version");
|
optimizedProxiesEnabled:
|
||||||
setHeaderToMap(headersMap, "Device-ID", generateRandomString(32));
|
pageState.state?.optimizedProxiesEnabled ?? true,
|
||||||
removeHeaderFromMap(headersMap, "Sec-GPC");
|
passportLevel: pageState.state?.passportLevel ?? 0,
|
||||||
removeHeaderFromMap(headersMap, "X-Device-Id");
|
}
|
||||||
} else {
|
);
|
||||||
|
if (!isLivestream || isWhitelisted) {
|
||||||
|
console.log(
|
||||||
|
"[TTV LOL PRO] Not flagging PlaybackAccessToken request: not a livestream or is whitelisted."
|
||||||
|
);
|
||||||
|
break graphql;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isTemplateRequest = requestBody.includes(
|
||||||
|
"PlaybackAccessToken_Template"
|
||||||
|
);
|
||||||
|
const areIntegrityRequestsProxied = isRequestTypeProxied(
|
||||||
|
ProxyRequestType.GraphQLIntegrity,
|
||||||
|
{
|
||||||
|
isChromium: pageState.isChromium,
|
||||||
|
optimizedProxiesEnabled:
|
||||||
|
pageState.state?.optimizedProxiesEnabled ?? true,
|
||||||
|
passportLevel: pageState.state?.passportLevel ?? 0,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// "PlaybackAccessToken" requests contain a Client-Integrity header.
|
||||||
|
// Thus, if integrity requests are not proxied, we can't proxy this request.
|
||||||
|
const willFailIntegrityCheckIfProxied =
|
||||||
|
!isTemplateRequest && !areIntegrityRequestsProxied;
|
||||||
|
const shouldOverrideRequest =
|
||||||
|
pageState.state?.anonymousMode === true ||
|
||||||
|
willFailIntegrityCheckIfProxied;
|
||||||
|
|
||||||
|
if (shouldOverrideRequest) {
|
||||||
|
const newRequest = await getDefaultPlaybackAccessTokenRequest(
|
||||||
|
channelName,
|
||||||
|
pageState.state?.anonymousMode === true
|
||||||
|
);
|
||||||
|
if (newRequest) {
|
||||||
console.log(
|
console.log(
|
||||||
"[TTV LOL PRO] 🕵️✋ Anonymous mode is enabled but channel is whitelisted."
|
"[TTV LOL PRO] Overriding PlaybackAccessToken request…"
|
||||||
|
);
|
||||||
|
request = newRequest;
|
||||||
|
// Since this is a template request, whether or not integrity requests are proxied doesn't matter.
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
"[TTV LOL PRO] Failed to override PlaybackAccessToken request."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
flagRequest(headersMap);
|
// Notice that if anonymous mode fails, we still flag the request to avoid ads.
|
||||||
} else if (
|
if (shouldFlagRequest && !willFailIntegrityCheckIfProxied) {
|
||||||
requestBody != null &&
|
console.log("[TTV LOL PRO] Flagging PlaybackAccessToken request…");
|
||||||
requestBody.includes("PlaybackAccessToken")
|
isFlaggedRequest = true;
|
||||||
) {
|
}
|
||||||
console.debug(
|
break graphql;
|
||||||
"[TTV LOL PRO] 🥅 Caught GraphQL PlaybackAccessToken request. Flagging…"
|
|
||||||
);
|
|
||||||
flagRequest(headersMap);
|
|
||||||
}
|
}
|
||||||
|
//#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
// Usher requests.
|
// Twitch Usher requests.
|
||||||
if (host != null && usherHostRegex.test(host)) {
|
usher: if (host != null && usherHostRegex.test(host)) {
|
||||||
console.debug("[TTV LOL PRO] 🥅 Caught Usher request.");
|
cachedUsherRequestUrl = url; // Cache the URL for later use.
|
||||||
}
|
requestType = ProxyRequestType.Usher;
|
||||||
|
await waitForStore(pageState);
|
||||||
// Video Weaver requests.
|
const channelName = findChannelFromUsherUrl(url);
|
||||||
if (host != null && videoWeaverHostRegex.test(host)) {
|
const isLivestream = !url.includes("/vod/");
|
||||||
const isIgnoredUrl = videoWeaverUrlsToIgnore.has(url);
|
const whitelistedChannelsLower = pageState.state?.whitelistedChannels.map(
|
||||||
const isNewUrl = !knownVideoWeaverUrls.has(url);
|
channel => channel.toLowerCase()
|
||||||
const isFlaggedUrl = videoWeaverUrlsToFlag.has(url);
|
);
|
||||||
|
const isWhitelisted =
|
||||||
if (!isIgnoredUrl && (isNewUrl || isFlaggedUrl)) {
|
channelName != null &&
|
||||||
|
whitelistedChannelsLower != null &&
|
||||||
|
whitelistedChannelsLower.includes(channelName.toLowerCase());
|
||||||
|
const shouldFlagRequest = isRequestTypeProxied(ProxyRequestType.Usher, {
|
||||||
|
isChromium: pageState.isChromium,
|
||||||
|
optimizedProxiesEnabled:
|
||||||
|
pageState.state?.optimizedProxiesEnabled ?? true,
|
||||||
|
passportLevel: pageState.state?.passportLevel ?? 0,
|
||||||
|
});
|
||||||
|
if (!isLivestream || isWhitelisted) {
|
||||||
console.log(
|
console.log(
|
||||||
`[TTV LOL PRO] 🥅 Caught ${
|
"[TTV LOL PRO] Not flagging Usher request: not a livestream or is whitelisted."
|
||||||
isNewUrl
|
|
||||||
? "first request to Video Weaver URL"
|
|
||||||
: "Video Weaver request to flag"
|
|
||||||
}. Flagging…`
|
|
||||||
);
|
);
|
||||||
flagRequest(headersMap);
|
break usher;
|
||||||
videoWeaverUrlsToFlag.set(
|
|
||||||
url,
|
|
||||||
(videoWeaverUrlsToFlag.get(url) ?? 0) + 1
|
|
||||||
);
|
|
||||||
if (isNewUrl) knownVideoWeaverUrls.add(url);
|
|
||||||
}
|
}
|
||||||
|
if (shouldFlagRequest) {
|
||||||
|
console.debug("[TTV LOL PRO] Flagging Usher request…");
|
||||||
|
isFlaggedRequest = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Twitch Video Weaver requests.
|
||||||
|
weaver: if (host != null && videoWeaverHostRegex.test(host)) {
|
||||||
|
requestType = ProxyRequestType.VideoWeaver;
|
||||||
|
|
||||||
|
//#region Video Weaver requests.
|
||||||
|
const manifest = usherManifests.find(manifest =>
|
||||||
|
[...manifest.assignedMap.values()].includes(url)
|
||||||
|
);
|
||||||
|
if (manifest == null) {
|
||||||
|
console.warn(
|
||||||
|
"[TTV LOL PRO] No associated Usher manifest found for Video Weaver request."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await waitForStore(pageState);
|
||||||
|
const channelName =
|
||||||
|
manifest?.channelName ?? findChannelFromTwitchTvUrl(location.href);
|
||||||
|
const whitelistedChannelsLower = pageState.state?.whitelistedChannels.map(
|
||||||
|
channel => channel.toLowerCase()
|
||||||
|
);
|
||||||
|
const isWhitelisted =
|
||||||
|
channelName != null &&
|
||||||
|
whitelistedChannelsLower != null &&
|
||||||
|
whitelistedChannelsLower.includes(channelName.toLowerCase());
|
||||||
|
const shouldFlagRequest = isRequestTypeProxied(
|
||||||
|
ProxyRequestType.VideoWeaver,
|
||||||
|
{
|
||||||
|
isChromium: pageState.isChromium,
|
||||||
|
optimizedProxiesEnabled:
|
||||||
|
pageState.state?.optimizedProxiesEnabled ?? true,
|
||||||
|
passportLevel: pageState.state?.passportLevel ?? 0,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (isWhitelisted) {
|
||||||
|
if (IS_DEVELOPMENT) {
|
||||||
|
console.debug(
|
||||||
|
"[TTV LOL PRO] Not flagging Video Weaver request: is whitelisted."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break weaver;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we should replace the Video Weaver URL.
|
||||||
|
let videoWeaverUrl = url;
|
||||||
|
if (manifest?.replacementMap != null) {
|
||||||
|
const videoQuality = [...manifest.assignedMap].find(
|
||||||
|
([, url]) => url === videoWeaverUrl
|
||||||
|
)?.[0];
|
||||||
|
if (videoQuality != null && manifest.replacementMap.has(videoQuality)) {
|
||||||
|
videoWeaverUrl = manifest.replacementMap.get(videoQuality)!;
|
||||||
|
if (IS_DEVELOPMENT) {
|
||||||
|
console.debug(
|
||||||
|
`[TTV LOL PRO] Replaced Video Weaver URL '${url}' with '${videoWeaverUrl}'.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (manifest.replacementMap.size > 0) {
|
||||||
|
videoWeaverUrl = [...manifest.replacementMap.values()][0];
|
||||||
|
console.warn(
|
||||||
|
`[TTV LOL PRO] Replacement Video Weaver URL not found for '${url}'. Using first replacement URL '${videoWeaverUrl}'.`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
`[TTV LOL PRO] Replacement Video Weaver URL not found for '${url}'.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flag first request to each Video Weaver URL.
|
||||||
|
const proxiedCount = videoWeaverUrlsProxiedCount.get(videoWeaverUrl) ?? 0;
|
||||||
|
if (shouldFlagRequest && proxiedCount < 1) {
|
||||||
|
videoWeaverUrlsProxiedCount.set(videoWeaverUrl, proxiedCount + 1);
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/PluralRules#using_options
|
||||||
|
const pr = new Intl.PluralRules("en-US", { type: "ordinal" });
|
||||||
|
const suffixes = new Map([
|
||||||
|
["one", "st"],
|
||||||
|
["two", "nd"],
|
||||||
|
["few", "rd"],
|
||||||
|
["other", "th"],
|
||||||
|
]);
|
||||||
|
const formatOrdinals = (n: number) => {
|
||||||
|
const rule = pr.select(n);
|
||||||
|
const suffix = suffixes.get(rule);
|
||||||
|
return `${n}${suffix}`;
|
||||||
|
};
|
||||||
|
console.log(
|
||||||
|
`[TTV LOL PRO] Flagging ${formatOrdinals(
|
||||||
|
proxiedCount + 1
|
||||||
|
)} request to Video Weaver URL '${videoWeaverUrl}'…`
|
||||||
|
);
|
||||||
|
isFlaggedRequest = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (videoWeaverUrl !== url) {
|
||||||
|
request ??= new Request(videoWeaverUrl, {
|
||||||
|
...init,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
const response = await NATIVE_FETCH(input, {
|
request ??= new Request(input, {
|
||||||
...init,
|
...init,
|
||||||
headers: Object.fromEntries(headersMap),
|
headers: Object.fromEntries(headersMap),
|
||||||
});
|
});
|
||||||
|
if (isFlaggedRequest) {
|
||||||
|
await waitForStore(pageState);
|
||||||
|
request = await flagRequest(request, requestType!, pageState);
|
||||||
|
}
|
||||||
|
const response = await NATIVE_FETCH(request);
|
||||||
|
if (isFlaggedRequest) {
|
||||||
|
flagRequestCleanup(requestType!, pageState);
|
||||||
|
}
|
||||||
|
|
||||||
// Reading the response body can be expensive, so we only do it if we need to.
|
// Reading the response body can be expensive, so we only do it if we need to.
|
||||||
let responseBody: string | undefined = undefined;
|
let responseBody: string | undefined = undefined;
|
||||||
@ -174,58 +409,86 @@ export function getFetch(options: FetchOptions): typeof fetch {
|
|||||||
|
|
||||||
//#region Responses
|
//#region Responses
|
||||||
|
|
||||||
// Usher responses.
|
// Twitch Usher responses.
|
||||||
if (host != null && usherHostRegex.test(host)) {
|
if (host != null && usherHostRegex.test(host) && response.status < 400) {
|
||||||
responseBody = await readResponseBody();
|
responseBody ??= await readResponseBody();
|
||||||
console.debug("[TTV LOL PRO] 🥅 Caught Usher response.");
|
const channelName = findChannelFromUsherUrl(url);
|
||||||
const videoWeaverUrls = responseBody
|
const assignedMap = parseUsherManifest(responseBody);
|
||||||
.split("\n")
|
if (assignedMap != null) {
|
||||||
.filter(line => videoWeaverUrlRegex.test(line));
|
console.debug(
|
||||||
|
"[TTV LOL PRO] Received Usher response:",
|
||||||
|
Object.fromEntries(assignedMap)
|
||||||
|
);
|
||||||
|
usherManifests.push({
|
||||||
|
channelName,
|
||||||
|
assignedMap: assignedMap,
|
||||||
|
replacementMap: null,
|
||||||
|
consecutiveMidrollResponses: 0,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.debug("[TTV LOL PRO] Received Usher response.");
|
||||||
|
}
|
||||||
// Send Video Weaver URLs to content script.
|
// Send Video Weaver URLs to content script.
|
||||||
sendMessageToContentScript(options.scope, {
|
const videoWeaverUrls = [...(assignedMap?.values() ?? [])];
|
||||||
type: "UsherResponse",
|
videoWeaverUrls.forEach(url => videoWeaverUrlsProxiedCount.delete(url)); // Shouldn't be necessary, but just in case.
|
||||||
channel: findChannelFromUsherUrl(url),
|
pageState.sendMessageToContentScript({
|
||||||
|
type: MessageType.UsherResponse,
|
||||||
|
channel: channelName,
|
||||||
videoWeaverUrls,
|
videoWeaverUrls,
|
||||||
proxyCountry:
|
proxyCountry:
|
||||||
/USER-COUNTRY="([A-Z]+)"/i.exec(responseBody)?.[1] || null,
|
/USER-COUNTRY="([A-Z]+)"/i.exec(responseBody)?.[1] || undefined,
|
||||||
});
|
});
|
||||||
// Remove all Video Weaver URLs from known URLs.
|
|
||||||
videoWeaverUrls.forEach(url => knownVideoWeaverUrls.delete(url));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Video Weaver responses.
|
// Twitch Video Weaver responses.
|
||||||
if (host != null && videoWeaverHostRegex.test(host)) {
|
if (
|
||||||
responseBody = await readResponseBody();
|
host != null &&
|
||||||
// Check if response contains ad.
|
videoWeaverHostRegex.test(host) &&
|
||||||
if (responseBody.includes("stitched-ad")) {
|
response.status < 400
|
||||||
console.log(
|
) {
|
||||||
"[TTV LOL PRO] 🥅 Caught Video Weaver response containing ad."
|
const manifest = usherManifests.find(manifest =>
|
||||||
|
[...manifest.assignedMap.values()].includes(url)
|
||||||
|
);
|
||||||
|
if (manifest == null) {
|
||||||
|
console.warn(
|
||||||
|
"[TTV LOL PRO] No associated Usher manifest found for Video Weaver response."
|
||||||
);
|
);
|
||||||
if (videoWeaverUrlsToIgnore.has(url)) return response;
|
return response;
|
||||||
if (!videoWeaverUrlsToFlag.has(url)) {
|
}
|
||||||
// Let's proxy the next request for this URL, 2 attempts left.
|
|
||||||
videoWeaverUrlsToFlag.set(url, 0);
|
// Check if response contains midroll ad.
|
||||||
cancelRequest();
|
responseBody ??= await readResponseBody();
|
||||||
}
|
if (
|
||||||
// FIXME: This workaround doesn't work. Let's find another way.
|
responseBody.includes("stitched-ad") &&
|
||||||
// 0: First attempt, not proxied, cancelled.
|
responseBody.toLowerCase().includes("midroll")
|
||||||
// 1: Second attempt, proxied, cancelled.
|
) {
|
||||||
// 2: Third attempt, proxied, last attempt by Twitch client.
|
console.log("[TTV LOL PRO] Midroll ad detected.");
|
||||||
// If the third attempt contains an ad, we have to let it through.
|
manifest.consecutiveMidrollResponses += 1;
|
||||||
const isCancellable = videoWeaverUrlsToFlag.get(url)! < 2;
|
await waitForStore(pageState);
|
||||||
if (isCancellable) {
|
const whitelistedChannelsLower =
|
||||||
cancelRequest();
|
pageState.state?.whitelistedChannels.map(channel =>
|
||||||
} else {
|
channel.toLowerCase()
|
||||||
console.error(
|
|
||||||
"[TTV LOL PRO] ❌ Could not cancel Video Weaver response containing ad. All attempts used."
|
|
||||||
);
|
);
|
||||||
videoWeaverUrlsToFlag.delete(url); // Clear attempts.
|
const isWhitelisted =
|
||||||
videoWeaverUrlsToIgnore.add(url); // Ignore this URL, there's nothing we can do.
|
manifest.channelName != null &&
|
||||||
|
whitelistedChannelsLower != null &&
|
||||||
|
whitelistedChannelsLower.includes(manifest.channelName.toLowerCase());
|
||||||
|
if (
|
||||||
|
pageState.state?.optimizedProxiesEnabled === true &&
|
||||||
|
manifest.consecutiveMidrollResponses <= 2 && // Avoid infinite loop.
|
||||||
|
!isWhitelisted
|
||||||
|
) {
|
||||||
|
const success = await updateVideoWeaverReplacementMap(
|
||||||
|
pageState,
|
||||||
|
cachedUsherRequestUrl,
|
||||||
|
manifest
|
||||||
|
);
|
||||||
|
if (success) cancelRequest();
|
||||||
}
|
}
|
||||||
|
manifest.replacementMap = null;
|
||||||
} else {
|
} else {
|
||||||
// No ad, remove from flagged list.
|
// No ad, clear attempts.
|
||||||
videoWeaverUrlsToFlag.delete(url);
|
manifest.consecutiveMidrollResponses = 0;
|
||||||
videoWeaverUrlsToIgnore.delete(url);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -237,7 +500,8 @@ export function getFetch(options: FetchOptions): typeof fetch {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a HeadersInit to a map.
|
* Converts a HeadersInit to a map.
|
||||||
* @param headers
|
* @param input
|
||||||
|
* @param init
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
function getHeadersMap(
|
function getHeadersMap(
|
||||||
@ -257,7 +521,8 @@ function getHeadersMap(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a BodyInit to a string.
|
* Converts a BodyInit to a string.
|
||||||
* @param body
|
* @param input
|
||||||
|
* @param init
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
async function getRequestBodyText(
|
async function getRequestBodyText(
|
||||||
@ -316,32 +581,334 @@ function removeHeaderFromMap(headersMap: Map<string, string>, name: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendMessageToContentScript(scope: "page" | "worker", message: any) {
|
async function waitForStore(pageState: PageState) {
|
||||||
if (scope === "page") {
|
if (pageState.state != null) return;
|
||||||
self.postMessage(message);
|
try {
|
||||||
|
const message =
|
||||||
|
await pageState.sendMessageToContentScriptAndWaitForResponse(
|
||||||
|
pageState.scope,
|
||||||
|
{
|
||||||
|
type: MessageType.GetStoreState,
|
||||||
|
},
|
||||||
|
MessageType.GetStoreStateResponse
|
||||||
|
);
|
||||||
|
pageState.state = message.state;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[TTV LOL PRO] Failed to get store state:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function flagRequest(
|
||||||
|
request: Request,
|
||||||
|
requestType: ProxyRequestType,
|
||||||
|
pageState: PageState
|
||||||
|
): Promise<Request> {
|
||||||
|
if (pageState.isChromium) {
|
||||||
|
if (!pageState.state?.optimizedProxiesEnabled) return request;
|
||||||
|
try {
|
||||||
|
await pageState.sendMessageToContentScriptAndWaitForResponse(
|
||||||
|
pageState.scope,
|
||||||
|
{
|
||||||
|
type: MessageType.EnableFullMode,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
requestType,
|
||||||
|
},
|
||||||
|
MessageType.EnableFullModeResponse
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[TTV LOL PRO] Failed to flag request:", error);
|
||||||
|
}
|
||||||
|
return request;
|
||||||
} else {
|
} else {
|
||||||
self.postMessage({
|
// Change the Accept header to include the flag.
|
||||||
type: "ContentScriptMessage",
|
const headersMap = getHeadersMap(request);
|
||||||
message,
|
const accept = getHeaderFromMap(headersMap, "Accept");
|
||||||
|
if (accept != null && accept.includes(acceptFlag)) return request;
|
||||||
|
setHeaderToMap(headersMap, "Accept", `${accept || ""}${acceptFlag}`);
|
||||||
|
return new Request(request, {
|
||||||
|
headers: Object.fromEntries(headersMap),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function flagRequest(headersMap: Map<string, string>) {
|
function flagRequestCleanup(
|
||||||
if (IS_CHROMIUM) {
|
requestType: ProxyRequestType,
|
||||||
console.debug(
|
pageState: PageState
|
||||||
"[TTV LOL PRO] 🚩 Request flagging is not supported on Chromium. Ignoring…"
|
) {
|
||||||
);
|
if (pageState.isChromium && pageState.state?.optimizedProxiesEnabled) {
|
||||||
return;
|
pageState.sendMessageToContentScript({
|
||||||
|
type: MessageType.DisableFullMode,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
requestType,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const accept = getHeaderFromMap(headersMap, "Accept");
|
|
||||||
setHeaderToMap(headersMap, "Accept", `${accept || ""}${acceptFlag}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelRequest(): never {
|
function cancelRequest(): never {
|
||||||
throw new Error();
|
throw new Error();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sleep(ms: number) {
|
async function sleep(ms: number): Promise<void> {
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//#region Video Weaver URL replacement
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a PlaybackAccessToken request that can be used when Twitch doesn't send one.
|
||||||
|
* @param channel
|
||||||
|
* @param anonymousMode
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async 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;
|
||||||
|
const isVod = /^\d+$/.test(channelName); // VODs have numeric IDs.
|
||||||
|
|
||||||
|
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") && !anonymousMode
|
||||||
|
? `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: "PlaybackAccessToken_Template",
|
||||||
|
query:
|
||||||
|
'query PlaybackAccessToken_Template($login: String!, $isLive: Boolean!, $vodID: ID!, $isVod: Boolean!, $playerType: String!) { streamPlaybackAccessToken(channelName: $login, params: {platform: "web", playerBackend: "mediaplayer", playerType: $playerType}) @include(if: $isLive) { value signature authorization { isForbidden forbiddenReasonCode } __typename } videoPlaybackAccessToken(id: $vodID, params: {platform: "web", playerBackend: "mediaplayer", playerType: $playerType}) @include(if: $isVod) { value signature __typename }}',
|
||||||
|
variables: {
|
||||||
|
isLive: !isVod,
|
||||||
|
login: isVod ? "" : channelName,
|
||||||
|
isVod: isVod,
|
||||||
|
vodID: isVod ? channelName : "",
|
||||||
|
playerType: "site",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches a new PlaybackAccessToken from Twitch.
|
||||||
|
* @param pageState
|
||||||
|
* @param cachedPlaybackTokenRequestHeaders
|
||||||
|
* @param cachedPlaybackTokenRequestBody
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async function fetchReplacementPlaybackAccessToken(
|
||||||
|
pageState: PageState,
|
||||||
|
cachedPlaybackTokenRequestHeaders: Map<string, string> | null,
|
||||||
|
cachedPlaybackTokenRequestBody: string | null
|
||||||
|
): Promise<PlaybackAccessToken | null> {
|
||||||
|
// Not using the cached request because we'd need to check if integrity requests are proxied.
|
||||||
|
try {
|
||||||
|
let request = await getDefaultPlaybackAccessTokenRequest(
|
||||||
|
null,
|
||||||
|
pageState.state?.anonymousMode === true
|
||||||
|
);
|
||||||
|
if (request == null) return null;
|
||||||
|
const isFlaggedRequest = isRequestTypeProxied(
|
||||||
|
ProxyRequestType.GraphQLToken,
|
||||||
|
{
|
||||||
|
isChromium: pageState.isChromium,
|
||||||
|
optimizedProxiesEnabled:
|
||||||
|
pageState.state?.optimizedProxiesEnabled ?? true,
|
||||||
|
passportLevel: pageState.state?.passportLevel ?? 0,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (isFlaggedRequest) {
|
||||||
|
request = await flagRequest(request, ProxyRequestType.GraphQL, pageState);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await NATIVE_FETCH(request);
|
||||||
|
if (isFlaggedRequest) {
|
||||||
|
flagRequestCleanup(ProxyRequestType.GraphQL, pageState);
|
||||||
|
}
|
||||||
|
const json = await response.json();
|
||||||
|
const newPlaybackAccessToken = json?.data?.streamPlaybackAccessToken;
|
||||||
|
if (newPlaybackAccessToken == null) return null;
|
||||||
|
return newPlaybackAccessToken;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new Usher URL with the new playback access token.
|
||||||
|
* @param cachedUsherRequestUrl
|
||||||
|
* @param playbackAccessToken
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function getReplacementUsherUrl(
|
||||||
|
cachedUsherRequestUrl: string | null,
|
||||||
|
playbackAccessToken: PlaybackAccessToken
|
||||||
|
): string | null {
|
||||||
|
if (cachedUsherRequestUrl == null) return null; // Very unlikely.
|
||||||
|
try {
|
||||||
|
const newUsherUrl = new URL(cachedUsherRequestUrl);
|
||||||
|
newUsherUrl.searchParams.delete("acmb");
|
||||||
|
newUsherUrl.searchParams.set("play_session_id", generateRandomString(32));
|
||||||
|
newUsherUrl.searchParams.set("sig", playbackAccessToken.signature);
|
||||||
|
newUsherUrl.searchParams.set("token", playbackAccessToken.value);
|
||||||
|
return newUsherUrl.toString();
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches a new Usher manifest from Twitch.
|
||||||
|
* @param pageState
|
||||||
|
* @param cachedUsherRequestUrl
|
||||||
|
* @param playbackAccessToken
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async function fetchReplacementUsherManifest(
|
||||||
|
pageState: PageState,
|
||||||
|
cachedUsherRequestUrl: string | null,
|
||||||
|
playbackAccessToken: PlaybackAccessToken
|
||||||
|
): Promise<string | null> {
|
||||||
|
if (cachedUsherRequestUrl == null) return null; // Very unlikely.
|
||||||
|
try {
|
||||||
|
const newUsherUrl = getReplacementUsherUrl(
|
||||||
|
cachedUsherRequestUrl,
|
||||||
|
playbackAccessToken
|
||||||
|
);
|
||||||
|
if (newUsherUrl == null) return null;
|
||||||
|
let request = new Request(newUsherUrl);
|
||||||
|
const isFlaggedRequest = isRequestTypeProxied(ProxyRequestType.Usher, {
|
||||||
|
isChromium: pageState.isChromium,
|
||||||
|
optimizedProxiesEnabled: pageState.state?.optimizedProxiesEnabled ?? true,
|
||||||
|
passportLevel: pageState.state?.passportLevel ?? 0,
|
||||||
|
});
|
||||||
|
if (isFlaggedRequest) {
|
||||||
|
request = await flagRequest(request, ProxyRequestType.Usher, pageState);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await NATIVE_FETCH(request);
|
||||||
|
if (isFlaggedRequest) {
|
||||||
|
flagRequestCleanup(ProxyRequestType.Usher, pageState);
|
||||||
|
}
|
||||||
|
if (response.status >= 400) return null;
|
||||||
|
const text = await response.text();
|
||||||
|
return text;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a Usher response and returns a map of video quality to URL.
|
||||||
|
* @param manifest
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function parseUsherManifest(manifest: string): Map<string, string> | null {
|
||||||
|
const parser = new m3u8Parser.Parser();
|
||||||
|
parser.push(manifest);
|
||||||
|
parser.end();
|
||||||
|
const parsedManifest = parser.manifest;
|
||||||
|
if (!parsedManifest.playlists || parsedManifest.playlists.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new Map(
|
||||||
|
parsedManifest.playlists.map(playlist => [
|
||||||
|
playlist.attributes.VIDEO,
|
||||||
|
playlist.uri,
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the replacement Video Weaver URLs.
|
||||||
|
* @param pageState
|
||||||
|
* @param cachedUsherRequestUrl
|
||||||
|
* @param manifest
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async function updateVideoWeaverReplacementMap(
|
||||||
|
pageState: PageState,
|
||||||
|
cachedUsherRequestUrl: string | null,
|
||||||
|
manifest: UsherManifest
|
||||||
|
): Promise<boolean> {
|
||||||
|
console.log("[TTV LOL PRO] Getting replacement Video Weaver URLs…");
|
||||||
|
try {
|
||||||
|
console.log("[TTV LOL PRO] (1/3) Getting new PlaybackAccessToken…");
|
||||||
|
const newPlaybackAccessTokenResponse =
|
||||||
|
await pageState.sendMessageToPageScriptAndWaitForResponse(
|
||||||
|
"worker",
|
||||||
|
{
|
||||||
|
type: MessageType.NewPlaybackAccessToken,
|
||||||
|
},
|
||||||
|
MessageType.NewPlaybackAccessTokenResponse
|
||||||
|
);
|
||||||
|
const newPlaybackAccessToken: PlaybackAccessToken | undefined =
|
||||||
|
newPlaybackAccessTokenResponse?.newPlaybackAccessToken;
|
||||||
|
if (newPlaybackAccessToken == null) {
|
||||||
|
console.error("[TTV LOL PRO] Failed to get new PlaybackAccessToken.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[TTV LOL PRO] (2/3) Fetching new Usher manifest…");
|
||||||
|
const newUsherManifest = await fetchReplacementUsherManifest(
|
||||||
|
pageState,
|
||||||
|
cachedUsherRequestUrl,
|
||||||
|
newPlaybackAccessToken
|
||||||
|
);
|
||||||
|
if (newUsherManifest == null) {
|
||||||
|
console.error("[TTV LOL PRO] Failed to fetch new Usher manifest.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[TTV LOL PRO] (3/3) Parsing new Usher manifest…");
|
||||||
|
const replacementMap = parseUsherManifest(newUsherManifest);
|
||||||
|
if (replacementMap == null || replacementMap.size === 0) {
|
||||||
|
console.error("[TTV LOL PRO] Failed to parse new Usher manifest.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"[TTV LOL PRO] Replacement Video Weaver URLs:",
|
||||||
|
Object.fromEntries(replacementMap)
|
||||||
|
);
|
||||||
|
manifest.replacementMap = replacementMap;
|
||||||
|
|
||||||
|
// Send replacement Video Weaver URLs to content script.
|
||||||
|
const videoWeaverUrls = [...replacementMap.values()];
|
||||||
|
if (cachedUsherRequestUrl != null && videoWeaverUrls.length > 0) {
|
||||||
|
pageState.sendMessageToContentScript({
|
||||||
|
type: MessageType.UsherResponse,
|
||||||
|
channel: findChannelFromUsherUrl(cachedUsherRequestUrl),
|
||||||
|
videoWeaverUrls,
|
||||||
|
proxyCountry:
|
||||||
|
/USER-COUNTRY="([A-Z]+)"/i.exec(newUsherManifest)?.[1] || undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"[TTV LOL PRO] Failed to get replacement Video Weaver URLs:",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
175
src/page/page.ts
@ -1,31 +1,65 @@
|
|||||||
import { FetchOptions, getFetch } from "./getFetch";
|
import findChannelFromTwitchTvUrl from "../common/ts/findChannelFromTwitchTvUrl";
|
||||||
|
import toAbsoluteUrl from "../common/ts/toAbsoluteUrl";
|
||||||
|
import { MessageType } from "../types";
|
||||||
|
import { getFetch } from "./getFetch";
|
||||||
|
import {
|
||||||
|
getSendMessageToContentScript,
|
||||||
|
getSendMessageToContentScriptAndWaitForResponse,
|
||||||
|
getSendMessageToPageScript,
|
||||||
|
getSendMessageToPageScriptAndWaitForResponse,
|
||||||
|
getSendMessageToWorkerScript,
|
||||||
|
getSendMessageToWorkerScriptAndWaitForResponse,
|
||||||
|
} from "./sendMessage";
|
||||||
|
import type { PageState } from "./types";
|
||||||
|
|
||||||
console.info("[TTV LOL PRO] 🚀 Page script running.");
|
console.info("[TTV LOL PRO] Page script running.");
|
||||||
|
|
||||||
const params = JSON.parse(document.currentScript!.dataset.params!);
|
const params = JSON.parse(document.currentScript!.dataset.params!);
|
||||||
const options: FetchOptions = {
|
|
||||||
|
const sendMessageToContentScript = getSendMessageToContentScript();
|
||||||
|
const sendMessageToContentScriptAndWaitForResponse =
|
||||||
|
getSendMessageToContentScriptAndWaitForResponse();
|
||||||
|
const sendMessageToPageScript = getSendMessageToPageScript();
|
||||||
|
const sendMessageToPageScriptAndWaitForResponse =
|
||||||
|
getSendMessageToPageScriptAndWaitForResponse();
|
||||||
|
const sendMessageToWorkerScript = getSendMessageToWorkerScript();
|
||||||
|
const sendMessageToWorkerScriptAndWaitForResponse =
|
||||||
|
getSendMessageToWorkerScriptAndWaitForResponse();
|
||||||
|
|
||||||
|
const pageState: PageState = {
|
||||||
|
isChromium: params.isChromium,
|
||||||
scope: "page",
|
scope: "page",
|
||||||
shouldWaitForStore: params.isChromium === false,
|
state: undefined,
|
||||||
|
twitchWorker: undefined,
|
||||||
|
sendMessageToContentScript,
|
||||||
|
sendMessageToContentScriptAndWaitForResponse,
|
||||||
|
sendMessageToPageScript,
|
||||||
|
sendMessageToPageScriptAndWaitForResponse,
|
||||||
|
sendMessageToWorkerScript,
|
||||||
|
sendMessageToWorkerScriptAndWaitForResponse,
|
||||||
};
|
};
|
||||||
|
|
||||||
window.fetch = getFetch(options);
|
window.fetch = getFetch(pageState);
|
||||||
|
|
||||||
window.Worker = class Worker extends window.Worker {
|
window.Worker = class Worker extends window.Worker {
|
||||||
constructor(scriptURL: string | URL, options?: WorkerOptions) {
|
constructor(scriptURL: string | URL, options?: WorkerOptions) {
|
||||||
const url = scriptURL.toString();
|
const fullUrl = toAbsoluteUrl(scriptURL.toString());
|
||||||
|
const isTwitchWorker = fullUrl.includes(".twitch.tv");
|
||||||
|
if (!isTwitchWorker) {
|
||||||
|
super(scriptURL, options);
|
||||||
|
return;
|
||||||
|
}
|
||||||
let script = "";
|
let script = "";
|
||||||
// Fetch Twitch's script, since Firefox Nightly errors out when trying to
|
// Fetch Twitch's script, since Firefox Nightly errors out when trying to
|
||||||
// import a blob URL directly.
|
// import a blob URL directly.
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
xhr.open("GET", url, false);
|
xhr.open("GET", fullUrl, false);
|
||||||
xhr.send();
|
xhr.send();
|
||||||
if (200 <= xhr.status && xhr.status < 300) {
|
if (200 <= xhr.status && xhr.status < 300) {
|
||||||
script = xhr.responseText;
|
script = xhr.responseText;
|
||||||
} else {
|
} else {
|
||||||
console.warn(
|
console.warn(`[TTV LOL PRO] Failed to fetch script: ${xhr.statusText}`);
|
||||||
`[TTV LOL PRO] ❌ Failed to fetch script: ${xhr.statusText}`
|
script = `importScripts("${fullUrl}");`; // Will fail on Firefox Nightly.
|
||||||
);
|
|
||||||
script = `importScripts("${url}");`; // Will fail on Firefox Nightly.
|
|
||||||
}
|
}
|
||||||
// ---------------------------------------
|
// ---------------------------------------
|
||||||
// 🦊 Attention Firefox Addon Reviewer 🦊
|
// 🦊 Attention Firefox Addon Reviewer 🦊
|
||||||
@ -33,10 +67,13 @@ window.Worker = class Worker extends window.Worker {
|
|||||||
// Please note that this does NOT involve remote code execution. The injected script is bundled
|
// Please note that this does NOT involve remote code execution. The injected script is bundled
|
||||||
// with the extension. Additionally, there is no custom Content Security Policy (CSP) in use.
|
// with the extension. Additionally, there is no custom Content Security Policy (CSP) in use.
|
||||||
const newScript = `
|
const newScript = `
|
||||||
|
var getParams = () => '${JSON.stringify(params)}';
|
||||||
try {
|
try {
|
||||||
importScripts("${params.workerScriptURL}");
|
importScripts("${params.workerScriptURL}");
|
||||||
} catch {
|
} catch (error) {
|
||||||
console.error("[TTV LOL PRO] ❌ Failed to load worker script: ${params.workerScriptURL}");
|
console.error("[TTV LOL PRO] Failed to load worker script: ${
|
||||||
|
params.workerScriptURL
|
||||||
|
}:", error);
|
||||||
}
|
}
|
||||||
${script}
|
${script}
|
||||||
`;
|
`;
|
||||||
@ -46,27 +83,113 @@ window.Worker = class Worker extends window.Worker {
|
|||||||
super(newScriptURL, options);
|
super(newScriptURL, options);
|
||||||
this.addEventListener("message", event => {
|
this.addEventListener("message", event => {
|
||||||
if (
|
if (
|
||||||
event.data?.type === "ContentScriptMessage" ||
|
event.data?.type === MessageType.ContentScriptMessage ||
|
||||||
event.data?.type === "PageScriptMessage"
|
event.data?.type === MessageType.PageScriptMessage
|
||||||
) {
|
) {
|
||||||
window.postMessage(event.data.message);
|
window.postMessage(event.data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
pageState.twitchWorker = this;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let sendStoreStateToWorker = false;
|
||||||
window.addEventListener("message", event => {
|
window.addEventListener("message", event => {
|
||||||
if (event.data?.type === "PageScriptMessage") {
|
// Relay messages from the content script to the worker script.
|
||||||
const message = event.data.message;
|
if (event.data?.type === MessageType.WorkerScriptMessage) {
|
||||||
if (message.type === "StoreReady") {
|
sendMessageToWorkerScript(pageState.twitchWorker, event.data.message);
|
||||||
console.log(
|
return;
|
||||||
"[TTV LOL PRO] 📦 Page received store state from content script."
|
}
|
||||||
);
|
|
||||||
// Mutate the options object.
|
if (event.data?.type !== MessageType.PageScriptMessage) return;
|
||||||
options.state = message.state;
|
|
||||||
options.shouldWaitForStore = false;
|
const message = event.data?.message;
|
||||||
}
|
if (!message) return;
|
||||||
|
|
||||||
|
switch (message.type) {
|
||||||
|
case MessageType.GetStoreState: // From Worker
|
||||||
|
if (pageState.state != null) {
|
||||||
|
sendMessageToWorkerScript(pageState.twitchWorker, {
|
||||||
|
type: MessageType.GetStoreStateResponse,
|
||||||
|
state: pageState.state,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
sendStoreStateToWorker = true;
|
||||||
|
break;
|
||||||
|
case MessageType.GetStoreStateResponse: // From Content
|
||||||
|
if (pageState.state == null) {
|
||||||
|
console.log("[TTV LOL PRO] Received store state from content script.");
|
||||||
|
} else {
|
||||||
|
console.debug(
|
||||||
|
"[TTV LOL PRO] Received store state from content script."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const state = message.state;
|
||||||
|
pageState.state = state;
|
||||||
|
if (sendStoreStateToWorker) {
|
||||||
|
sendMessageToWorkerScript(pageState.twitchWorker, {
|
||||||
|
type: MessageType.GetStoreStateResponse,
|
||||||
|
state,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function onChannelChange(callback: (channelName: string) => void) {
|
||||||
|
let channelName: string | null = findChannelFromTwitchTvUrl(location.href);
|
||||||
|
|
||||||
|
const NATIVE_PUSH_STATE = window.history.pushState;
|
||||||
|
function pushState(
|
||||||
|
data: any,
|
||||||
|
unused: string,
|
||||||
|
url?: string | URL | null | undefined
|
||||||
|
) {
|
||||||
|
if (!url) return NATIVE_PUSH_STATE.call(window.history, data, unused);
|
||||||
|
const fullUrl = toAbsoluteUrl(url.toString());
|
||||||
|
const newChannelName = findChannelFromTwitchTvUrl(fullUrl);
|
||||||
|
if (newChannelName != null && newChannelName !== channelName) {
|
||||||
|
channelName = newChannelName;
|
||||||
|
callback(channelName);
|
||||||
|
}
|
||||||
|
return NATIVE_PUSH_STATE.call(window.history, data, unused, url);
|
||||||
|
}
|
||||||
|
window.history.pushState = pushState;
|
||||||
|
|
||||||
|
const NATIVE_REPLACE_STATE = window.history.replaceState;
|
||||||
|
function replaceState(
|
||||||
|
data: any,
|
||||||
|
unused: string,
|
||||||
|
url?: string | URL | null | undefined
|
||||||
|
) {
|
||||||
|
if (!url) return NATIVE_REPLACE_STATE.call(window.history, data, unused);
|
||||||
|
const fullUrl = toAbsoluteUrl(url.toString());
|
||||||
|
const newChannelName = findChannelFromTwitchTvUrl(fullUrl);
|
||||||
|
if (newChannelName != null && newChannelName !== channelName) {
|
||||||
|
channelName = newChannelName;
|
||||||
|
callback(channelName);
|
||||||
|
}
|
||||||
|
return NATIVE_REPLACE_STATE.call(window.history, data, unused, url);
|
||||||
|
}
|
||||||
|
window.history.replaceState = replaceState;
|
||||||
|
|
||||||
|
window.addEventListener("popstate", () => {
|
||||||
|
const newChannelName = findChannelFromTwitchTvUrl(location.href);
|
||||||
|
if (newChannelName != null && newChannelName !== channelName) {
|
||||||
|
channelName = newChannelName;
|
||||||
|
callback(channelName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onChannelChange(() => {
|
||||||
|
sendMessageToContentScript({ type: MessageType.ClearStats });
|
||||||
|
sendMessageToPageScript({ type: MessageType.ClearStats });
|
||||||
|
sendMessageToWorkerScript(pageState.twitchWorker, {
|
||||||
|
type: MessageType.ClearStats,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
sendMessageToContentScript({ type: MessageType.GetStoreState });
|
||||||
|
|
||||||
document.currentScript!.remove();
|
document.currentScript!.remove();
|
||||||
|
130
src/page/sendMessage.ts
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import { MessageType } from "../types";
|
||||||
|
import type {
|
||||||
|
SendMessageAndWaitForResponseFn,
|
||||||
|
SendMessageAndWaitForResponseWorkerFn,
|
||||||
|
SendMessageFn,
|
||||||
|
SendMessageWorkerFn,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
// TODO: Secure communication between content, page, and worker scripts.
|
||||||
|
|
||||||
|
function sendMessage(
|
||||||
|
recipient: Window | Worker | undefined,
|
||||||
|
type: MessageType,
|
||||||
|
message: any
|
||||||
|
): void {
|
||||||
|
if (!recipient) return;
|
||||||
|
recipient.postMessage({
|
||||||
|
type,
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendMessageAndWaitForResponse(
|
||||||
|
recipient: Window | Worker | undefined,
|
||||||
|
type: MessageType,
|
||||||
|
message: any,
|
||||||
|
responseType: MessageType,
|
||||||
|
responseMessageType: MessageType,
|
||||||
|
responseTimeout: number
|
||||||
|
): Promise<any> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!recipient) {
|
||||||
|
console.warn("[TTV LOL PRO] Recipient is undefined.");
|
||||||
|
resolve(undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const listener = (event: MessageEvent) => {
|
||||||
|
if (event.data?.type !== responseType) return;
|
||||||
|
const message = event.data?.message;
|
||||||
|
if (!message) return;
|
||||||
|
if (message.type === responseMessageType) {
|
||||||
|
resolve(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.addEventListener("message", listener);
|
||||||
|
sendMessage(recipient, type, message);
|
||||||
|
setTimeout(() => {
|
||||||
|
self.removeEventListener("message", listener);
|
||||||
|
reject(new Error("Timed out waiting for message response."));
|
||||||
|
}, responseTimeout);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSendMessageToContentScript(): SendMessageFn {
|
||||||
|
return (message: any) =>
|
||||||
|
sendMessage(self, MessageType.ContentScriptMessage, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSendMessageToContentScriptAndWaitForResponse(): SendMessageAndWaitForResponseFn {
|
||||||
|
return async (
|
||||||
|
scope: "page" | "worker",
|
||||||
|
message: any,
|
||||||
|
responseMessageType: MessageType,
|
||||||
|
responseTimeout: number = 5000
|
||||||
|
) => {
|
||||||
|
return sendMessageAndWaitForResponse(
|
||||||
|
self,
|
||||||
|
MessageType.ContentScriptMessage,
|
||||||
|
message,
|
||||||
|
scope === "page"
|
||||||
|
? MessageType.PageScriptMessage
|
||||||
|
: MessageType.WorkerScriptMessage,
|
||||||
|
responseMessageType,
|
||||||
|
responseTimeout
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSendMessageToPageScript(): SendMessageFn {
|
||||||
|
return (message: any) =>
|
||||||
|
sendMessage(self, MessageType.PageScriptMessage, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSendMessageToPageScriptAndWaitForResponse(): SendMessageAndWaitForResponseFn {
|
||||||
|
return async (
|
||||||
|
scope: "page" | "worker",
|
||||||
|
message: any,
|
||||||
|
responseMessageType: MessageType,
|
||||||
|
responseTimeout: number = 5000
|
||||||
|
) => {
|
||||||
|
return sendMessageAndWaitForResponse(
|
||||||
|
self,
|
||||||
|
MessageType.PageScriptMessage,
|
||||||
|
message,
|
||||||
|
scope === "page"
|
||||||
|
? MessageType.PageScriptMessage
|
||||||
|
: MessageType.WorkerScriptMessage,
|
||||||
|
responseMessageType,
|
||||||
|
responseTimeout
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSendMessageToWorkerScript(): SendMessageWorkerFn {
|
||||||
|
return (worker: Worker | undefined, message: any) =>
|
||||||
|
sendMessage(worker, MessageType.WorkerScriptMessage, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSendMessageToWorkerScriptAndWaitForResponse(): SendMessageAndWaitForResponseWorkerFn {
|
||||||
|
return async (
|
||||||
|
worker: Worker | undefined,
|
||||||
|
message: any,
|
||||||
|
responseMessageType: MessageType,
|
||||||
|
scope: "page" | "worker",
|
||||||
|
responseTimeout: number = 5000
|
||||||
|
) => {
|
||||||
|
return sendMessageAndWaitForResponse(
|
||||||
|
worker,
|
||||||
|
MessageType.WorkerScriptMessage,
|
||||||
|
message,
|
||||||
|
scope === "page"
|
||||||
|
? MessageType.PageScriptMessage
|
||||||
|
: MessageType.WorkerScriptMessage,
|
||||||
|
responseMessageType,
|
||||||
|
responseTimeout
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
51
src/page/types.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import type { State } from "../store/types";
|
||||||
|
import { MessageType } from "../types";
|
||||||
|
|
||||||
|
export type SendMessageFn = (message: any) => void;
|
||||||
|
export type SendMessageWorkerFn = (
|
||||||
|
worker: Worker | undefined,
|
||||||
|
message: any
|
||||||
|
) => void;
|
||||||
|
export type SendMessageAndWaitForResponseFn = (
|
||||||
|
scope: "page" | "worker",
|
||||||
|
message: any,
|
||||||
|
responseMessageType: MessageType,
|
||||||
|
responseTimeout?: number
|
||||||
|
) => Promise<any>;
|
||||||
|
export type SendMessageAndWaitForResponseWorkerFn = (
|
||||||
|
worker: Worker | undefined,
|
||||||
|
message: any,
|
||||||
|
responseMessageType: MessageType,
|
||||||
|
scope: "page" | "worker",
|
||||||
|
responseTimeout?: number
|
||||||
|
) => Promise<any>;
|
||||||
|
|
||||||
|
export interface PageState {
|
||||||
|
isChromium: boolean;
|
||||||
|
scope: "page" | "worker";
|
||||||
|
state?: State;
|
||||||
|
twitchWorker?: Worker;
|
||||||
|
sendMessageToContentScript: SendMessageFn;
|
||||||
|
sendMessageToContentScriptAndWaitForResponse: SendMessageAndWaitForResponseFn;
|
||||||
|
sendMessageToPageScript: SendMessageFn;
|
||||||
|
sendMessageToPageScriptAndWaitForResponse: SendMessageAndWaitForResponseFn;
|
||||||
|
sendMessageToWorkerScript: SendMessageWorkerFn;
|
||||||
|
sendMessageToWorkerScriptAndWaitForResponse: SendMessageAndWaitForResponseWorkerFn;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UsherManifest {
|
||||||
|
channelName: string | null;
|
||||||
|
assignedMap: Map<string, string>; // E.g. "720p60" -> "https://video-weaver.fra02.hls.ttvnw.net/v1/playlist/..."
|
||||||
|
replacementMap: Map<string, string> | null; // Same as above, but with new URLs.
|
||||||
|
consecutiveMidrollResponses: number; // Used to avoid infinite loops.
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlaybackAccessToken {
|
||||||
|
value: string;
|
||||||
|
signature: string;
|
||||||
|
authorization: {
|
||||||
|
isForbidden: boolean;
|
||||||
|
forbiddenReasonCode: string;
|
||||||
|
};
|
||||||
|
__typename: string;
|
||||||
|
}
|
@ -1,10 +1,68 @@
|
|||||||
import { FetchOptions, getFetch } from "./getFetch";
|
import { MessageType } from "../types";
|
||||||
|
import { getFetch } from "./getFetch";
|
||||||
|
import {
|
||||||
|
getSendMessageToContentScript,
|
||||||
|
getSendMessageToContentScriptAndWaitForResponse,
|
||||||
|
getSendMessageToPageScript,
|
||||||
|
getSendMessageToPageScriptAndWaitForResponse,
|
||||||
|
getSendMessageToWorkerScript,
|
||||||
|
getSendMessageToWorkerScriptAndWaitForResponse,
|
||||||
|
} from "./sendMessage";
|
||||||
|
import type { PageState } from "./types";
|
||||||
|
|
||||||
console.info("[TTV LOL PRO] 🚀 Worker script running.");
|
console.info("[TTV LOL PRO] Worker script running.");
|
||||||
|
|
||||||
const options: FetchOptions = {
|
declare var getParams: () => string;
|
||||||
|
let params;
|
||||||
|
try {
|
||||||
|
params = JSON.parse(getParams()!);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[TTV LOL PRO] Failed to parse params:", error);
|
||||||
|
}
|
||||||
|
getParams = undefined as any;
|
||||||
|
|
||||||
|
const sendMessageToContentScript = getSendMessageToContentScript();
|
||||||
|
const sendMessageToContentScriptAndWaitForResponse =
|
||||||
|
getSendMessageToContentScriptAndWaitForResponse();
|
||||||
|
const sendMessageToPageScript = getSendMessageToPageScript();
|
||||||
|
const sendMessageToPageScriptAndWaitForResponse =
|
||||||
|
getSendMessageToPageScriptAndWaitForResponse();
|
||||||
|
const sendMessageToWorkerScript = getSendMessageToWorkerScript();
|
||||||
|
const sendMessageToWorkerScriptAndWaitForResponse =
|
||||||
|
getSendMessageToWorkerScriptAndWaitForResponse();
|
||||||
|
|
||||||
|
const pageState: PageState = {
|
||||||
|
isChromium: params.isChromium,
|
||||||
scope: "worker",
|
scope: "worker",
|
||||||
shouldWaitForStore: false,
|
state: undefined,
|
||||||
|
twitchWorker: undefined, // Can't get the worker instance from inside the worker.
|
||||||
|
sendMessageToContentScript,
|
||||||
|
sendMessageToContentScriptAndWaitForResponse,
|
||||||
|
sendMessageToPageScript,
|
||||||
|
sendMessageToPageScriptAndWaitForResponse,
|
||||||
|
sendMessageToWorkerScript,
|
||||||
|
sendMessageToWorkerScriptAndWaitForResponse,
|
||||||
};
|
};
|
||||||
|
|
||||||
self.fetch = getFetch(options);
|
self.fetch = getFetch(pageState);
|
||||||
|
|
||||||
|
self.addEventListener("message", event => {
|
||||||
|
if (event.data?.type !== MessageType.WorkerScriptMessage) return;
|
||||||
|
|
||||||
|
const message = event.data?.message;
|
||||||
|
if (!message) return;
|
||||||
|
|
||||||
|
switch (message.type) {
|
||||||
|
case MessageType.GetStoreStateResponse: // From Page
|
||||||
|
if (pageState.state == null) {
|
||||||
|
console.log("[TTV LOL PRO] Received store state from page script.");
|
||||||
|
} else {
|
||||||
|
console.debug("[TTV LOL PRO] Received store state from page script.");
|
||||||
|
}
|
||||||
|
const state = message.state;
|
||||||
|
pageState.state = state;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sendMessageToPageScript({ type: MessageType.GetStoreState });
|
||||||
|
@ -17,23 +17,11 @@
|
|||||||
>options page</a
|
>options page</a
|
||||||
>.
|
>.
|
||||||
</div>
|
</div>
|
||||||
<div id="warning-banner-limited-proxy" class="warning-banner">
|
|
||||||
<h3 class="warning-banner-title">You are using a limited proxy!</h3>
|
|
||||||
The proxy you are using has a limited number of simultaneous connections.
|
|
||||||
This means that you may experience buffering issues. Consider donating to
|
|
||||||
get access to unlimited proxies, or
|
|
||||||
<a
|
|
||||||
href="https://github.com/younesaassila/ttv-lol-pro/discussions/151"
|
|
||||||
target="_blank"
|
|
||||||
class="warning-banner-link"
|
|
||||||
>host your own proxy</a
|
|
||||||
>.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<!-- Logo -->
|
<!-- Logo -->
|
||||||
<div class="logo-wrapper">
|
<div class="logo-wrapper">
|
||||||
<img src="../images/brand/icon.png" alt="TTV LOL PRO" />
|
<img src="../common/images/brand/icon.png" alt="TTV LOL PRO" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Stream status -->
|
<!-- Stream status -->
|
||||||
@ -54,7 +42,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<h3 id="channel-name"></h3>
|
<h3 id="channel-name"></h3>
|
||||||
<p id="reason"></p>
|
<p id="reason"></p>
|
||||||
<small id="info"></small>
|
<div id="info-container"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="whitelist-status" data-whitelisted="false">
|
<div id="whitelist-status" data-whitelisted="false">
|
||||||
<input
|
<input
|
||||||
|
@ -5,26 +5,23 @@ import {
|
|||||||
anonymizeIpAddress,
|
anonymizeIpAddress,
|
||||||
anonymizeIpAddresses,
|
anonymizeIpAddresses,
|
||||||
} from "../common/ts/anonymizeIpAddress";
|
} from "../common/ts/anonymizeIpAddress";
|
||||||
|
import { alpha2 } from "../common/ts/countryCodes";
|
||||||
import findChannelFromTwitchTvUrl from "../common/ts/findChannelFromTwitchTvUrl";
|
import findChannelFromTwitchTvUrl from "../common/ts/findChannelFromTwitchTvUrl";
|
||||||
import getProxyInfoFromUrl from "../common/ts/getProxyInfoFromUrl";
|
import isChannelWhitelisted from "../common/ts/isChannelWhitelisted";
|
||||||
import isChromium from "../common/ts/isChromium";
|
|
||||||
import store from "../store";
|
import store from "../store";
|
||||||
import type { StreamStatus } from "../types";
|
import type { StreamStatus } from "../types";
|
||||||
|
|
||||||
type WarningBannerType = "noProxies" | "limitedProxy";
|
type WarningBannerType = "noProxies";
|
||||||
|
|
||||||
//#region HTML Elements
|
//#region HTML Elements
|
||||||
const warningBannerNoProxiesElement = $(
|
const warningBannerNoProxiesElement = $(
|
||||||
"#warning-banner-no-proxies"
|
"#warning-banner-no-proxies"
|
||||||
) as HTMLDivElement;
|
) as HTMLDivElement;
|
||||||
const warningBannerLimitedProxyElement = $(
|
|
||||||
"#warning-banner-limited-proxy"
|
|
||||||
) as HTMLDivElement;
|
|
||||||
const streamStatusElement = $("#stream-status") as HTMLDivElement;
|
const streamStatusElement = $("#stream-status") as HTMLDivElement;
|
||||||
const proxiedElement = $("#proxied") as HTMLDivElement;
|
const proxiedElement = $("#proxied") as HTMLDivElement;
|
||||||
const channelNameElement = $("#channel-name") as HTMLHeadingElement;
|
const channelNameElement = $("#channel-name") as HTMLHeadingElement;
|
||||||
const reasonElement = $("#reason") as HTMLParagraphElement;
|
const reasonElement = $("#reason") as HTMLParagraphElement;
|
||||||
const infoElement = $("#info") as HTMLElement;
|
const infoContainerElement = $("#info-container") as HTMLDivElement;
|
||||||
const whitelistStatusElement = $("#whitelist-status") as HTMLDivElement;
|
const whitelistStatusElement = $("#whitelist-status") as HTMLDivElement;
|
||||||
const whitelistToggleElement = $("#whitelist-toggle") as HTMLInputElement;
|
const whitelistToggleElement = $("#whitelist-toggle") as HTMLInputElement;
|
||||||
const copyDebugInfoButtonElement = $(
|
const copyDebugInfoButtonElement = $(
|
||||||
@ -39,21 +36,11 @@ if (store.readyState === "complete") main();
|
|||||||
else store.addEventListener("load", main);
|
else store.addEventListener("load", main);
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
let proxies: string[];
|
const proxies = store.state.optimizedProxiesEnabled
|
||||||
if (isChromium) {
|
? store.state.optimizedProxies
|
||||||
proxies = store.state.normalProxies;
|
: store.state.normalProxies;
|
||||||
} else {
|
|
||||||
proxies = store.state.optimizedProxiesEnabled
|
|
||||||
? store.state.optimizedProxies
|
|
||||||
: store.state.normalProxies;
|
|
||||||
}
|
|
||||||
const isLimitedProxy =
|
|
||||||
proxies.length > 0 &&
|
|
||||||
getProxyInfoFromUrl(proxies[0]).host === "chrome.api.cdn-perfprod.com";
|
|
||||||
if (proxies.length === 0) {
|
if (proxies.length === 0) {
|
||||||
setWarningBanner("noProxies");
|
setWarningBanner("noProxies");
|
||||||
} else if (isLimitedProxy) {
|
|
||||||
setWarningBanner("limitedProxy");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
|
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
|
||||||
@ -68,20 +55,22 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setWarningBanner(type: WarningBannerType) {
|
function setWarningBanner(type: WarningBannerType) {
|
||||||
if (type === "noProxies") {
|
// Hide all warning banners.
|
||||||
warningBannerNoProxiesElement.style.display = "block";
|
warningBannerNoProxiesElement.style.display = "none";
|
||||||
warningBannerLimitedProxyElement.style.display = "none";
|
|
||||||
} else if (type === "limitedProxy") {
|
switch (type) {
|
||||||
warningBannerNoProxiesElement.style.display = "none";
|
case "noProxies":
|
||||||
warningBannerLimitedProxyElement.style.display = "block";
|
warningBannerNoProxiesElement.style.display = "block";
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setStreamStatusElement(channelName: string) {
|
function setStreamStatusElement(channelName: string) {
|
||||||
const channelNameLower = channelName.toLowerCase();
|
const channelNameLower = channelName.toLowerCase();
|
||||||
|
const isWhitelisted = isChannelWhitelisted(channelNameLower);
|
||||||
const status = store.state.streamStatuses[channelNameLower];
|
const status = store.state.streamStatuses[channelNameLower];
|
||||||
if (status) {
|
if (status) {
|
||||||
setProxyStatus(channelNameLower, status);
|
setProxyStatus(channelNameLower, isWhitelisted, status);
|
||||||
setWhitelistStatus(channelNameLower);
|
setWhitelistStatus(channelNameLower);
|
||||||
streamStatusElement.style.display = "flex";
|
streamStatusElement.style.display = "flex";
|
||||||
} else {
|
} else {
|
||||||
@ -89,25 +78,35 @@ function setStreamStatusElement(channelName: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setProxyStatus(channelNameLower: string, status: StreamStatus) {
|
function setProxyStatus(
|
||||||
|
channelNameLower: string,
|
||||||
|
isWhitelisted: boolean,
|
||||||
|
status: StreamStatus
|
||||||
|
) {
|
||||||
// Proxied
|
// Proxied
|
||||||
if (status.proxied) {
|
if (status.proxied) {
|
||||||
proxiedElement.classList.remove("error");
|
proxiedElement.classList.remove("error");
|
||||||
proxiedElement.classList.remove("idle");
|
proxiedElement.classList.remove("idle");
|
||||||
proxiedElement.classList.add("success");
|
proxiedElement.classList.add("success");
|
||||||
|
proxiedElement.title = "Proxying";
|
||||||
} else if (
|
} else if (
|
||||||
!status.proxied &&
|
!status.proxied &&
|
||||||
status.proxyHost &&
|
status.proxyHost &&
|
||||||
|
status.stats &&
|
||||||
|
status.stats.proxied > 0 &&
|
||||||
store.state.optimizedProxiesEnabled &&
|
store.state.optimizedProxiesEnabled &&
|
||||||
store.state.optimizedProxies.length > 0
|
store.state.optimizedProxies.length > 0 &&
|
||||||
|
!isWhitelisted
|
||||||
) {
|
) {
|
||||||
proxiedElement.classList.remove("error");
|
proxiedElement.classList.remove("error");
|
||||||
proxiedElement.classList.remove("success");
|
proxiedElement.classList.remove("success");
|
||||||
proxiedElement.classList.add("idle");
|
proxiedElement.classList.add("idle");
|
||||||
|
proxiedElement.title = "Idling";
|
||||||
} else {
|
} else {
|
||||||
proxiedElement.classList.remove("success");
|
proxiedElement.classList.remove("success");
|
||||||
proxiedElement.classList.remove("idle");
|
proxiedElement.classList.remove("idle");
|
||||||
proxiedElement.classList.add("error");
|
proxiedElement.classList.add("error");
|
||||||
|
proxiedElement.title = "Not proxying";
|
||||||
}
|
}
|
||||||
// Channel name
|
// Channel name
|
||||||
channelNameElement.textContent = channelNameLower;
|
channelNameElement.textContent = channelNameLower;
|
||||||
@ -124,14 +123,24 @@ function setProxyStatus(channelNameLower: string, status: StreamStatus) {
|
|||||||
messages.push(`Proxy: ${anonymizeIpAddress(status.proxyHost)}`);
|
messages.push(`Proxy: ${anonymizeIpAddress(status.proxyHost)}`);
|
||||||
}
|
}
|
||||||
if (status.proxyCountry) {
|
if (status.proxyCountry) {
|
||||||
messages.push(`Country: ${status.proxyCountry}`);
|
messages.push(
|
||||||
|
`Country: ${
|
||||||
|
(alpha2 as Record<string, string>)[status.proxyCountry] ??
|
||||||
|
status.proxyCountry
|
||||||
|
}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (store.state.optimizedProxiesEnabled) {
|
if (store.state.optimizedProxiesEnabled) {
|
||||||
messages.push("Optimized proxies enabled");
|
messages.push("Using optimized proxies");
|
||||||
}
|
}
|
||||||
if (messages.length > 0) {
|
infoContainerElement.innerHTML = "";
|
||||||
infoElement.textContent = messages.join(", ");
|
infoContainerElement.style.display = "none";
|
||||||
infoElement.style.display = "block";
|
for (const message of messages) {
|
||||||
|
const smallElement = document.createElement("small");
|
||||||
|
smallElement.className = "info";
|
||||||
|
smallElement.textContent = message;
|
||||||
|
infoContainerElement.appendChild(smallElement);
|
||||||
|
infoContainerElement.style.display = "flex";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,48 +171,70 @@ function setWhitelistStatus(channelNameLower: string) {
|
|||||||
copyDebugInfoButtonElement.addEventListener("click", async e => {
|
copyDebugInfoButtonElement.addEventListener("click", async e => {
|
||||||
const extensionInfo = await browser.management.getSelf();
|
const extensionInfo = await browser.management.getSelf();
|
||||||
const userAgentParser = Bowser.getParser(window.navigator.userAgent);
|
const userAgentParser = Bowser.getParser(window.navigator.userAgent);
|
||||||
|
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
|
||||||
|
const activeTab = tabs[0];
|
||||||
|
const channelName =
|
||||||
|
activeTab?.url != null ? findChannelFromTwitchTvUrl(activeTab.url) : null;
|
||||||
|
const channelNameLower =
|
||||||
|
channelName != null ? channelName.toLowerCase() : null;
|
||||||
|
const isWhitelisted =
|
||||||
|
channelNameLower != null ? isChannelWhitelisted(channelNameLower) : null;
|
||||||
|
const status =
|
||||||
|
channelNameLower != null
|
||||||
|
? store.state.streamStatuses[channelNameLower]
|
||||||
|
: null;
|
||||||
|
|
||||||
const debugInfo = [
|
const debugInfo = [
|
||||||
`${extensionInfo.name} v${extensionInfo.version}`,
|
`**Debug Info**\n`,
|
||||||
`- Install type: ${extensionInfo.installType}`,
|
`Extension: ${extensionInfo.name} v${extensionInfo.version} (${extensionInfo.installType})\n`,
|
||||||
`- Browser: ${userAgentParser.getBrowserName()} ${userAgentParser.getBrowserVersion()}`,
|
`Browser: ${userAgentParser.getBrowserName()} ${userAgentParser.getBrowserVersion()} (${userAgentParser.getOSName()} ${userAgentParser.getOSVersion()})\n`,
|
||||||
`- OS: ${userAgentParser.getOSName()} ${userAgentParser.getOSVersion()}`,
|
`Options:\n`,
|
||||||
`- Passport enabled: ${store.state.proxyUsherRequests}`,
|
`- Passport level: ${store.state.passportLevel}\n`,
|
||||||
`- Is laissez-passer: ${store.state.proxyTwitchWebpage}`,
|
`- Anonymous mode: ${store.state.anonymousMode}\n`,
|
||||||
`- Is redacted: ${store.state.anonymousMode}`,
|
store.state.optimizedProxiesEnabled
|
||||||
`- Optimized proxies enabled: ${store.state.optimizedProxiesEnabled}`,
|
? `- Using optimized proxies: ${JSON.stringify(
|
||||||
`- Optimized proxies: ${JSON.stringify(
|
e.shiftKey
|
||||||
e.shiftKey
|
? store.state.optimizedProxies
|
||||||
? store.state.optimizedProxies
|
: anonymizeIpAddresses(store.state.optimizedProxies)
|
||||||
: anonymizeIpAddresses(store.state.optimizedProxies)
|
)}\n`
|
||||||
)}`,
|
: `- Using normal proxies: ${JSON.stringify(
|
||||||
`- Normal proxies: ${JSON.stringify(
|
e.shiftKey
|
||||||
e.shiftKey
|
? store.state.normalProxies
|
||||||
? store.state.normalProxies
|
: anonymizeIpAddresses(store.state.normalProxies)
|
||||||
: anonymizeIpAddresses(store.state.normalProxies)
|
)}\n`,
|
||||||
)}`,
|
channelName != null
|
||||||
isChromium
|
? [
|
||||||
? `- Should extension be active: ${store.state.chromiumProxyActive}`
|
`Channel name: ${channelName}${
|
||||||
|
isWhitelisted ? " (whitelisted)" : ""
|
||||||
|
}\n`,
|
||||||
|
`Stream status:\n`,
|
||||||
|
status != null
|
||||||
|
? [
|
||||||
|
`- Proxied: ${status.stats?.proxied ?? "N/A"}, Not proxied: ${
|
||||||
|
status.stats?.notProxied ?? "N/A"
|
||||||
|
}\n`,
|
||||||
|
`- Proxy: ${
|
||||||
|
status.proxyHost != null
|
||||||
|
? anonymizeIpAddress(status.proxyHost)
|
||||||
|
: "N/A"
|
||||||
|
}\n`,
|
||||||
|
`- Country: ${status.proxyCountry ?? "N/A"}\n`,
|
||||||
|
].join("")
|
||||||
|
: "",
|
||||||
|
].join("")
|
||||||
: "",
|
: "",
|
||||||
isChromium
|
store.state.adLog.length > 0
|
||||||
? `- Number of opened Twitch tabs: ${store.state.openedTwitchTabs.length}`
|
? `Latest ad log entry: ${JSON.stringify({
|
||||||
|
...store.state.adLog[store.state.adLog.length - 1],
|
||||||
|
videoWeaverUrl: undefined,
|
||||||
|
})}\n`
|
||||||
: "",
|
: "",
|
||||||
`- Last ad log entry: ${
|
].join("");
|
||||||
store.state.adLog.length
|
|
||||||
? JSON.stringify({
|
|
||||||
...store.state.adLog[store.state.adLog.length - 1],
|
|
||||||
videoWeaverUrl: undefined,
|
|
||||||
})
|
|
||||||
: "N/A"
|
|
||||||
}`,
|
|
||||||
].join("\n");
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(debugInfo);
|
await navigator.clipboard.writeText(debugInfo);
|
||||||
copyDebugInfoButtonDescriptionElement.textContent = "Copied to clipboard!";
|
copyDebugInfoButtonDescriptionElement.textContent = "Copied to clipboard!";
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
copyDebugInfoButtonDescriptionElement.textContent = `Failed to copy to clipboard: ${error}`;
|
||||||
copyDebugInfoButtonDescriptionElement.textContent =
|
|
||||||
"Failed to copy to clipboard.";
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
src: url("../fonts/Inter-VariableFont_slnt\,wght.ttf");
|
src: url("../common/fonts/Inter-VariableFont_slnt\,wght.ttf");
|
||||||
font-family: "Inter";
|
font-family: "Inter";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,6 +112,7 @@ main > * {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
|
cursor: help;
|
||||||
}
|
}
|
||||||
#stream-status #proxied.success {
|
#stream-status #proxied.success {
|
||||||
color: var(--success-color);
|
color: var(--success-color);
|
||||||
@ -131,16 +132,23 @@ main > * {
|
|||||||
/* Proxy status reason */
|
/* Proxy status reason */
|
||||||
#stream-status #reason {
|
#stream-status #reason {
|
||||||
grid-area: middle-right;
|
grid-area: middle-right;
|
||||||
margin: 2px 0 0 0;
|
margin: 4px 0 0 0;
|
||||||
font-size: 9pt;
|
font-size: 9pt;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
/* Proxy status info */
|
/* Proxy status info */
|
||||||
#stream-status #info {
|
#stream-status #info-container {
|
||||||
display: none;
|
display: none;
|
||||||
grid-area: bottom-right;
|
grid-area: bottom-right;
|
||||||
margin: 4px 0 0 0;
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 6px 0 0 0;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
#stream-status .info {
|
||||||
font-size: 7pt;
|
font-size: 7pt;
|
||||||
|
text-overflow: ellipsis;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
/* Whitelist status */
|
/* Whitelist status */
|
||||||
|
@ -20,15 +20,5 @@
|
|||||||
"urlFilter": "*.twitch.tv/r/c/*",
|
"urlFilter": "*.twitch.tv/r/c/*",
|
||||||
"resourceTypes": ["image"]
|
"resourceTypes": ["image"]
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 3,
|
|
||||||
"priority": 1,
|
|
||||||
"action": {
|
|
||||||
"type": "block"
|
|
||||||
},
|
|
||||||
"condition": {
|
|
||||||
"urlFilter": "*.ads.twitch.tv/*"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -6,15 +6,16 @@ export default function getDefaultState() {
|
|||||||
adLog: [],
|
adLog: [],
|
||||||
adLogEnabled: true,
|
adLogEnabled: true,
|
||||||
adLogLastSent: 0,
|
adLogLastSent: 0,
|
||||||
anonymousMode: false,
|
anonymousMode: true,
|
||||||
chromiumProxyActive: false,
|
chromiumProxyActive: false,
|
||||||
dnsResponses: [],
|
dnsResponses: [],
|
||||||
normalProxies: ["chrome.api.cdn-perfprod.com:4023"],
|
normalProxies: [],
|
||||||
openedTwitchTabs: [],
|
openedTwitchTabs: [],
|
||||||
optimizedProxies: isChromium ? [] : ["firefox.api.cdn-perfprod.com:2023"],
|
optimizedProxies: isChromium
|
||||||
optimizedProxiesEnabled: !isChromium,
|
? ["chromium.api.cdn-perfprod.com:2023"]
|
||||||
proxyTwitchWebpage: false,
|
: ["firefox.api.cdn-perfprod.com:2023"],
|
||||||
proxyUsherRequests: true,
|
optimizedProxiesEnabled: true,
|
||||||
|
passportLevel: 0,
|
||||||
streamStatuses: {},
|
streamStatuses: {},
|
||||||
videoWeaverUrlsByChannel: {},
|
videoWeaverUrlsByChannel: {},
|
||||||
whitelistedChannels: [],
|
whitelistedChannels: [],
|
||||||
|
@ -31,7 +31,7 @@ class Store<T extends Record<string | symbol, any>> {
|
|||||||
if (newValue === undefined) continue; // Ignore deletions.
|
if (newValue === undefined) continue; // Ignore deletions.
|
||||||
this._state[key as keyof T] = newValue;
|
this._state[key as keyof T] = newValue;
|
||||||
}
|
}
|
||||||
this.dispatchEvent("change");
|
this.dispatchEvent("change", changes);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,9 +68,9 @@ class Store<T extends Record<string | symbol, any>> {
|
|||||||
if (index !== -1) this._listenersByEvent[type].splice(index, 1);
|
if (index !== -1) this._listenersByEvent[type].splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatchEvent(type: EventType) {
|
dispatchEvent(type: EventType, ...args: any[]) {
|
||||||
const listeners = this._listenersByEvent[type] || [];
|
const listeners = this._listenersByEvent[type] || [];
|
||||||
listeners.forEach(listener => listener());
|
listeners.forEach(listener => listener(...args));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,8 +16,7 @@ export interface State {
|
|||||||
openedTwitchTabs: Tabs.Tab[];
|
openedTwitchTabs: Tabs.Tab[];
|
||||||
optimizedProxies: string[];
|
optimizedProxies: string[];
|
||||||
optimizedProxiesEnabled: boolean;
|
optimizedProxiesEnabled: boolean;
|
||||||
proxyTwitchWebpage: boolean;
|
passportLevel: number;
|
||||||
proxyUsherRequests: boolean;
|
|
||||||
streamStatuses: Record<string, StreamStatus>;
|
streamStatuses: Record<string, StreamStatus>;
|
||||||
videoWeaverUrlsByChannel: Record<string, string[]>;
|
videoWeaverUrlsByChannel: Record<string, string[]>;
|
||||||
whitelistedChannels: string[];
|
whitelistedChannels: string[];
|
||||||
|
44
src/types.ts
@ -23,11 +23,10 @@ export const enum AdType {
|
|||||||
|
|
||||||
export interface AdLogEntry {
|
export interface AdLogEntry {
|
||||||
adType: AdType;
|
adType: AdType;
|
||||||
channel: string | null;
|
|
||||||
isPurpleScreen: boolean;
|
isPurpleScreen: boolean;
|
||||||
proxy: string | null;
|
proxy: string | null;
|
||||||
proxyTwitchWebpage: boolean;
|
channel: string | null;
|
||||||
proxyUsherRequests: boolean;
|
passportLevel: number;
|
||||||
anonymousMode: boolean;
|
anonymousMode: boolean;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
videoWeaverHost: string;
|
videoWeaverHost: string;
|
||||||
@ -51,3 +50,42 @@ export interface DnsResponse {
|
|||||||
timestamp: number;
|
timestamp: number;
|
||||||
ttl: number;
|
ttl: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const enum MessageType {
|
||||||
|
ContentScriptMessage = "TLP_ContentScriptMessage",
|
||||||
|
PageScriptMessage = "TLP_PageScriptMessage",
|
||||||
|
WorkerScriptMessage = "TLP_WorkerScriptMessage",
|
||||||
|
GetStoreState = "TLP_GetStoreState",
|
||||||
|
GetStoreStateResponse = "TLP_GetStoreStateResponse",
|
||||||
|
EnableFullMode = "TLP_EnableFullMode",
|
||||||
|
EnableFullModeResponse = "TLP_EnableFullModeResponse",
|
||||||
|
DisableFullMode = "TLP_DisableFullMode",
|
||||||
|
UsherResponse = "TLP_UsherResponse",
|
||||||
|
NewPlaybackAccessToken = "TLP_NewPlaybackAccessToken",
|
||||||
|
NewPlaybackAccessTokenResponse = "TLP_NewPlaybackAccessTokenResponse",
|
||||||
|
ClearStats = "TLP_ClearStats",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const enum ProxyRequestType {
|
||||||
|
Passport = "passport",
|
||||||
|
Usher = "usher",
|
||||||
|
VideoWeaver = "videoWeaver",
|
||||||
|
GraphQL = "graphQL",
|
||||||
|
GraphQLToken = "graphQLToken",
|
||||||
|
GraphQLIntegrity = "graphQLIntegrity",
|
||||||
|
TwitchWebpage = "twitchWebpage",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ProxyRequestParams =
|
||||||
|
| {
|
||||||
|
isChromium: true;
|
||||||
|
optimizedProxiesEnabled: boolean;
|
||||||
|
passportLevel: number;
|
||||||
|
fullModeEnabled?: boolean;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
isChromium: false;
|
||||||
|
optimizedProxiesEnabled: boolean;
|
||||||
|
passportLevel: number;
|
||||||
|
isFlagged?: boolean;
|
||||||
|
};
|
||||||
|