mirror of
https://github.com/younesaassila/ttv-lol-pro.git
synced 2025-05-29 21:10:22 +02:00
🔖 Release version 2.1.0
This commit is contained in:
commit
dcd03ce4d4
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ttv-lol-pro",
|
"name": "ttv-lol-pro",
|
||||||
"version": "2.0.2",
|
"version": "2.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ttv-lol-pro",
|
"name": "ttv-lol-pro",
|
||||||
"version": "2.0.2",
|
"version": "2.1.0",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bowser": "^2.11.0",
|
"bowser": "^2.11.0",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ttv-lol-pro",
|
"name": "ttv-lol-pro",
|
||||||
"version": "2.0.2",
|
"version": "2.1.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,
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
import browser from "webextension-polyfill";
|
import browser from "webextension-polyfill";
|
||||||
import isChromium from "../common/ts/isChromium";
|
import isChromium from "../common/ts/isChromium";
|
||||||
import updateProxySettings from "../common/ts/updateProxySettings";
|
import checkForOpenedTwitchTabs from "./handlers/checkForOpenedTwitchTabs";
|
||||||
import store from "../store";
|
|
||||||
import onAuthRequired from "./handlers/onAuthRequired";
|
import onAuthRequired from "./handlers/onAuthRequired";
|
||||||
import onBeforeSendHeaders from "./handlers/onBeforeSendHeaders";
|
import onBeforeSendHeaders from "./handlers/onBeforeSendHeaders";
|
||||||
import onBeforeUsherRequest from "./handlers/onBeforeUsherRequest";
|
|
||||||
import onBeforeVideoWeaverRequest from "./handlers/onBeforeVideoWeaverRequest";
|
import onBeforeVideoWeaverRequest from "./handlers/onBeforeVideoWeaverRequest";
|
||||||
import onHeadersReceived from "./handlers/onHeadersReceived";
|
|
||||||
import onProxyRequest from "./handlers/onProxyRequest";
|
import onProxyRequest from "./handlers/onProxyRequest";
|
||||||
|
import onResponseStarted from "./handlers/onResponseStarted";
|
||||||
import onStartupStoreCleanup from "./handlers/onStartupStoreCleanup";
|
import onStartupStoreCleanup from "./handlers/onStartupStoreCleanup";
|
||||||
|
import onTabCreated from "./handlers/onTabCreated";
|
||||||
|
import onTabRemoved from "./handlers/onTabRemoved";
|
||||||
|
import onTabUpdated from "./handlers/onTabUpdated";
|
||||||
|
|
||||||
console.info("🚀 Background script loaded.");
|
console.info("🚀 Background script loaded.");
|
||||||
|
|
||||||
@ -22,13 +23,19 @@ browser.webRequest.onAuthRequired.addListener(
|
|||||||
["blocking"]
|
["blocking"]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Monitor proxied status of requests.
|
||||||
|
browser.webRequest.onResponseStarted.addListener(onResponseStarted, {
|
||||||
|
urls: ["https://*.ttvnw.net/*", "https://*.twitch.tv/*"],
|
||||||
|
});
|
||||||
|
|
||||||
if (isChromium) {
|
if (isChromium) {
|
||||||
const setProxySettings = () => {
|
// Check if there are any opened Twitch tabs on startup.
|
||||||
if (store.readyState !== "complete")
|
checkForOpenedTwitchTabs();
|
||||||
return store.addEventListener("load", setProxySettings);
|
|
||||||
updateProxySettings();
|
// Keep track of opened Twitch tabs to enable/disable the PAC script.
|
||||||
};
|
browser.tabs.onCreated.addListener(onTabCreated);
|
||||||
setProxySettings();
|
browser.tabs.onUpdated.addListener(onTabUpdated);
|
||||||
|
browser.tabs.onRemoved.addListener(onTabRemoved);
|
||||||
} else {
|
} else {
|
||||||
// Block tracking pixels.
|
// Block tracking pixels.
|
||||||
browser.webRequest.onBeforeRequest.addListener(
|
browser.webRequest.onBeforeRequest.addListener(
|
||||||
@ -37,15 +44,6 @@ if (isChromium) {
|
|||||||
["blocking"]
|
["blocking"]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Map channel names to Video Weaver URLs.
|
|
||||||
browser.webRequest.onBeforeRequest.addListener(
|
|
||||||
onBeforeUsherRequest,
|
|
||||||
{
|
|
||||||
urls: ["https://usher.ttvnw.net/api/channel/hls/*"],
|
|
||||||
},
|
|
||||||
["blocking"]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Proxy requests.
|
// Proxy requests.
|
||||||
browser.proxy.onRequest.addListener(
|
browser.proxy.onRequest.addListener(
|
||||||
onProxyRequest,
|
onProxyRequest,
|
||||||
@ -72,9 +70,4 @@ if (isChromium) {
|
|||||||
},
|
},
|
||||||
["blocking"]
|
["blocking"]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Monitor responses of proxied requests.
|
|
||||||
browser.webRequest.onHeadersReceived.addListener(onHeadersReceived, {
|
|
||||||
urls: ["https://*.ttvnw.net/*", "https://*.twitch.tv/*"],
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
30
src/background/handlers/checkForOpenedTwitchTabs.ts
Normal file
30
src/background/handlers/checkForOpenedTwitchTabs.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import browser from "webextension-polyfill";
|
||||||
|
import isChromium from "../../common/ts/isChromium";
|
||||||
|
import {
|
||||||
|
clearProxySettings,
|
||||||
|
updateProxySettings,
|
||||||
|
} from "../../common/ts/proxySettings";
|
||||||
|
import store from "../../store";
|
||||||
|
|
||||||
|
export default function checkForOpenedTwitchTabs() {
|
||||||
|
if (store.readyState !== "complete")
|
||||||
|
return store.addEventListener("load", checkForOpenedTwitchTabs);
|
||||||
|
|
||||||
|
browser.tabs
|
||||||
|
.query({ url: ["https://www.twitch.tv/*", "https://m.twitch.tv/*"] })
|
||||||
|
.then(tabs => {
|
||||||
|
if (tabs.length === 0) {
|
||||||
|
if (isChromium) clearProxySettings();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
`🔍 Found ${tabs.length} opened Twitch tabs: ${tabs
|
||||||
|
.map(tab => tab.id)
|
||||||
|
.join(", ")}`
|
||||||
|
);
|
||||||
|
if (isChromium) {
|
||||||
|
updateProxySettings();
|
||||||
|
}
|
||||||
|
store.state.openedTwitchTabs = tabs.map(tab => tab.id);
|
||||||
|
});
|
||||||
|
}
|
@ -1,61 +0,0 @@
|
|||||||
import { WebRequest } from "webextension-polyfill";
|
|
||||||
import filterResponseDataWrapper from "../../common/ts/filterResponseDataWrapper";
|
|
||||||
import {
|
|
||||||
twitchApiChannelNameRegex,
|
|
||||||
videoWeaverUrlRegex,
|
|
||||||
} from "../../common/ts/regexes";
|
|
||||||
import store from "../../store";
|
|
||||||
import type { StreamStatus } from "../../types";
|
|
||||||
|
|
||||||
export default function onBeforeUsherRequest(
|
|
||||||
details: WebRequest.OnBeforeRequestDetailsType
|
|
||||||
): void | WebRequest.BlockingResponseOrPromise {
|
|
||||||
const match = twitchApiChannelNameRegex.exec(details.url);
|
|
||||||
if (!match) return;
|
|
||||||
const channelName = match[1]?.toLowerCase();
|
|
||||||
if (!channelName) return;
|
|
||||||
|
|
||||||
filterResponseDataWrapper(details, text => {
|
|
||||||
const videoWeaverUrls = text.match(videoWeaverUrlRegex);
|
|
||||||
if (!videoWeaverUrls) return text;
|
|
||||||
console.log(
|
|
||||||
`📺 Found ${videoWeaverUrls.length} video-weaver URLs for ${channelName}.`
|
|
||||||
);
|
|
||||||
const existingVideoWeaverUrls =
|
|
||||||
store.state.videoWeaverUrlsByChannel[channelName] ?? [];
|
|
||||||
const newVideoWeaverUrls = videoWeaverUrls.filter(
|
|
||||||
url => !existingVideoWeaverUrls.includes(url)
|
|
||||||
);
|
|
||||||
store.state.videoWeaverUrlsByChannel[channelName] = [
|
|
||||||
...existingVideoWeaverUrls,
|
|
||||||
...newVideoWeaverUrls,
|
|
||||||
];
|
|
||||||
const streamStatus = getStreamStatus(channelName);
|
|
||||||
setStreamStatus(channelName, {
|
|
||||||
...(streamStatus ?? { proxied: false, reason: "" }),
|
|
||||||
proxyCountry: extractProxyCountryFromManifest(text),
|
|
||||||
});
|
|
||||||
return text;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStreamStatus(channelName: string | null): StreamStatus | null {
|
|
||||||
if (!channelName) return null;
|
|
||||||
return store.state.streamStatuses[channelName] ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setStreamStatus(
|
|
||||||
channelName: string | null,
|
|
||||||
streamStatus: StreamStatus
|
|
||||||
): boolean {
|
|
||||||
if (!channelName) return false;
|
|
||||||
store.state.streamStatuses[channelName] = streamStatus;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractProxyCountryFromManifest(text: string): string | undefined {
|
|
||||||
const match = /USER-COUNTRY="([A-Z]+)"/i.exec(text);
|
|
||||||
if (!match) return;
|
|
||||||
const [, proxyCountry] = match;
|
|
||||||
return proxyCountry;
|
|
||||||
}
|
|
@ -108,7 +108,7 @@ export default async function onProxyRequest(
|
|||||||
|
|
||||||
function getProxyInfoArrayFromUrls(urls: string[]): ProxyInfo[] {
|
function getProxyInfoArrayFromUrls(urls: string[]): ProxyInfo[] {
|
||||||
return [
|
return [
|
||||||
...urls.map(url => getProxyInfoFromUrl(url)),
|
...urls.map(getProxyInfoFromUrl),
|
||||||
{ type: "direct" } as ProxyInfo, // Fallback to direct connection if all proxies fail.
|
{ type: "direct" } as ProxyInfo, // Fallback to direct connection if all proxies fail.
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { WebRequest } from "webextension-polyfill";
|
import { WebRequest } from "webextension-polyfill";
|
||||||
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 {
|
import {
|
||||||
passportHostRegex,
|
passportHostRegex,
|
||||||
twitchGqlHostRegex,
|
twitchGqlHostRegex,
|
||||||
@ -8,14 +10,15 @@ import {
|
|||||||
usherHostRegex,
|
usherHostRegex,
|
||||||
videoWeaverHostRegex,
|
videoWeaverHostRegex,
|
||||||
} from "../../common/ts/regexes";
|
} from "../../common/ts/regexes";
|
||||||
|
import { getStreamStatus, setStreamStatus } from "../../common/ts/streamStatus";
|
||||||
import store from "../../store";
|
import store from "../../store";
|
||||||
import type { ProxyInfo, StreamStatus } from "../../types";
|
import type { ProxyInfo } from "../../types";
|
||||||
|
|
||||||
export default function onHeadersReceived(
|
export default function onResponseStarted(
|
||||||
details: WebRequest.OnHeadersReceivedDetailsType & {
|
details: WebRequest.OnResponseStartedDetailsType & {
|
||||||
proxyInfo?: ProxyInfo;
|
proxyInfo?: ProxyInfo;
|
||||||
}
|
}
|
||||||
): void | WebRequest.BlockingResponseOrPromise {
|
): void {
|
||||||
const host = getHostFromUrl(details.url);
|
const host = getHostFromUrl(details.url);
|
||||||
if (!host) return;
|
if (!host) return;
|
||||||
|
|
||||||
@ -77,25 +80,31 @@ export default function onHeadersReceived(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getProxyFromDetails(
|
function getProxyFromDetails(
|
||||||
details: WebRequest.OnHeadersReceivedDetailsType & {
|
details: WebRequest.OnResponseStartedDetailsType & {
|
||||||
proxyInfo?: ProxyInfo;
|
proxyInfo?: ProxyInfo;
|
||||||
}
|
}
|
||||||
): string | null {
|
): string | null {
|
||||||
const proxyInfo = details.proxyInfo; // Firefox only.
|
if (isChromium) {
|
||||||
if (!proxyInfo || proxyInfo.type === "direct") return null;
|
const ip = details.ip;
|
||||||
return `${proxyInfo.host}:${proxyInfo.port}`;
|
if (!ip) return null;
|
||||||
}
|
const dnsResponse = store.state.dnsResponses.find(
|
||||||
|
dnsResponse => dnsResponse.ips.indexOf(ip) !== -1
|
||||||
function getStreamStatus(channelName: string | null): StreamStatus | null {
|
);
|
||||||
if (!channelName) return null;
|
if (!dnsResponse) return null;
|
||||||
return store.state.streamStatuses[channelName] ?? null;
|
const proxies = [
|
||||||
}
|
...store.state.optimizedProxies,
|
||||||
|
...store.state.normalProxies,
|
||||||
function setStreamStatus(
|
];
|
||||||
channelName: string | null,
|
const proxyInfoArray = proxies.map(getProxyInfoFromUrl);
|
||||||
streamStatus: StreamStatus
|
const possibleProxies = proxyInfoArray.filter(
|
||||||
): boolean {
|
proxy => proxy.host === dnsResponse.host
|
||||||
if (!channelName) return false;
|
);
|
||||||
store.state.streamStatuses[channelName] = streamStatus;
|
if (possibleProxies.length === 1)
|
||||||
return true;
|
return `${possibleProxies[0].host}:${possibleProxies[0].port}`;
|
||||||
|
return dnsResponse.host;
|
||||||
|
} else {
|
||||||
|
const proxyInfo = details.proxyInfo; // Firefox only.
|
||||||
|
if (!proxyInfo || proxyInfo.type === "direct") return null;
|
||||||
|
return `${proxyInfo.host}:${proxyInfo.port}`;
|
||||||
|
}
|
||||||
}
|
}
|
@ -12,6 +12,8 @@ export default function onStartupStoreCleanup(): void {
|
|||||||
if (store.readyState !== "complete")
|
if (store.readyState !== "complete")
|
||||||
return store.addEventListener("load", onStartupStoreCleanup);
|
return store.addEventListener("load", onStartupStoreCleanup);
|
||||||
|
|
||||||
|
store.state.dnsResponses = [];
|
||||||
|
store.state.openedTwitchTabs = [];
|
||||||
store.state.streamStatuses = {};
|
store.state.streamStatuses = {};
|
||||||
store.state.videoWeaverUrlsByChannel = {};
|
store.state.videoWeaverUrlsByChannel = {};
|
||||||
}
|
}
|
||||||
|
18
src/background/handlers/onTabCreated.ts
Normal file
18
src/background/handlers/onTabCreated.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Tabs } from "webextension-polyfill";
|
||||||
|
import getHostFromUrl from "../../common/ts/getHostFromUrl";
|
||||||
|
import isChromium from "../../common/ts/isChromium";
|
||||||
|
import { updateProxySettings } from "../../common/ts/proxySettings";
|
||||||
|
import { twitchTvHostRegex } from "../../common/ts/regexes";
|
||||||
|
import store from "../../store";
|
||||||
|
|
||||||
|
export default function onTabCreated(tab: Tabs.Tab): void {
|
||||||
|
if (!tab.url) return;
|
||||||
|
const host = getHostFromUrl(tab.url);
|
||||||
|
if (twitchTvHostRegex.test(host)) {
|
||||||
|
console.log(`➕ Opened Twitch tab: ${tab.id}`);
|
||||||
|
if (isChromium && store.state.openedTwitchTabs.length === 0) {
|
||||||
|
updateProxySettings();
|
||||||
|
}
|
||||||
|
store.state.openedTwitchTabs.push(tab.id);
|
||||||
|
}
|
||||||
|
}
|
14
src/background/handlers/onTabRemoved.ts
Normal file
14
src/background/handlers/onTabRemoved.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import isChromium from "../../common/ts/isChromium";
|
||||||
|
import { clearProxySettings } from "../../common/ts/proxySettings";
|
||||||
|
import store from "../../store";
|
||||||
|
|
||||||
|
export default function onTabRemoved(tabId: number): void {
|
||||||
|
const index = store.state.openedTwitchTabs.indexOf(tabId);
|
||||||
|
if (index !== -1) {
|
||||||
|
console.log(`➖ Closed Twitch tab: ${tabId}`);
|
||||||
|
store.state.openedTwitchTabs.splice(index, 1);
|
||||||
|
if (isChromium && store.state.openedTwitchTabs.length === 0) {
|
||||||
|
clearProxySettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
42
src/background/handlers/onTabUpdated.ts
Normal file
42
src/background/handlers/onTabUpdated.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { Tabs } from "webextension-polyfill";
|
||||||
|
import getHostFromUrl from "../../common/ts/getHostFromUrl";
|
||||||
|
import isChromium from "../../common/ts/isChromium";
|
||||||
|
import {
|
||||||
|
clearProxySettings,
|
||||||
|
updateProxySettings,
|
||||||
|
} from "../../common/ts/proxySettings";
|
||||||
|
import { twitchTvHostRegex } from "../../common/ts/regexes";
|
||||||
|
import store from "../../store";
|
||||||
|
|
||||||
|
export default function onTabUpdated(
|
||||||
|
tabId: number,
|
||||||
|
changeInfo: Tabs.OnUpdatedChangeInfoType,
|
||||||
|
tab: Tabs.Tab
|
||||||
|
): void {
|
||||||
|
// Also check for `changeInfo.status === "complete"` because the `url` property
|
||||||
|
// is not always accurate when navigating to a new page.
|
||||||
|
if (!(changeInfo.url || changeInfo.status === "complete")) return;
|
||||||
|
|
||||||
|
const url = changeInfo.url || tab.url;
|
||||||
|
const host = getHostFromUrl(url);
|
||||||
|
const isTwitchTab = twitchTvHostRegex.test(host);
|
||||||
|
const wasTwitchTab = store.state.openedTwitchTabs.includes(tabId);
|
||||||
|
|
||||||
|
if (isTwitchTab && !wasTwitchTab) {
|
||||||
|
console.log(`➕ Opened Twitch tab: ${tabId}`);
|
||||||
|
if (isChromium && store.state.openedTwitchTabs.length === 0) {
|
||||||
|
updateProxySettings();
|
||||||
|
}
|
||||||
|
store.state.openedTwitchTabs.push(tabId);
|
||||||
|
}
|
||||||
|
if (!isTwitchTab && wasTwitchTab) {
|
||||||
|
const index = store.state.openedTwitchTabs.indexOf(tabId);
|
||||||
|
if (index !== -1) {
|
||||||
|
console.log(`➖ Closed Twitch tab: ${tabId}`);
|
||||||
|
store.state.openedTwitchTabs.splice(index, 1);
|
||||||
|
if (isChromium && store.state.openedTwitchTabs.length === 0) {
|
||||||
|
clearProxySettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
37
src/common/ts/file.ts
Normal file
37
src/common/ts/file.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* Read a file from the user's computer.
|
||||||
|
* @param accept
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export async function readFile(accept = "text/plain;charset=utf-8") {
|
||||||
|
return new Promise<string>((resolve, reject) => {
|
||||||
|
const input = document.createElement("input");
|
||||||
|
input.type = "file";
|
||||||
|
input.accept = accept;
|
||||||
|
input.addEventListener("change", async e => {
|
||||||
|
const input = e.target as HTMLInputElement;
|
||||||
|
const file = input.files?.[0];
|
||||||
|
if (!file) return reject("No file selected");
|
||||||
|
const data = await file.text();
|
||||||
|
return resolve(data);
|
||||||
|
});
|
||||||
|
input.click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save a file to the user's computer.
|
||||||
|
* @param filename
|
||||||
|
* @param content
|
||||||
|
* @param type
|
||||||
|
*/
|
||||||
|
export function saveFile(
|
||||||
|
filename: string,
|
||||||
|
content: string,
|
||||||
|
type = "text/plain;charset=utf-8"
|
||||||
|
) {
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.setAttribute("href", `data:${type},` + encodeURIComponent(content));
|
||||||
|
a.setAttribute("download", filename);
|
||||||
|
a.click();
|
||||||
|
}
|
@ -1,5 +1,11 @@
|
|||||||
import { twitchApiChannelNameRegex } from "./regexes";
|
import { twitchApiChannelNameRegex } from "./regexes";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the channel name from a Twitch Usher URL.
|
||||||
|
* Returns `null` if the URL is not a valid Usher URL.
|
||||||
|
* @param usherUrl
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
export default function findChannelFromUsherUrl(
|
export default function findChannelFromUsherUrl(
|
||||||
usherUrl: string
|
usherUrl: string
|
||||||
): string | null {
|
): string | null {
|
||||||
|
@ -1,5 +1,11 @@
|
|||||||
import store from "../../store";
|
import store from "../../store";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the channel name from a Video Weaver URL.
|
||||||
|
* Returns `null` if the URL is not a valid Video Weaver URL.
|
||||||
|
* @param videoWeaverUrl
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
export default function findChannelFromVideoWeaverUrl(videoWeaverUrl: string) {
|
export default function findChannelFromVideoWeaverUrl(videoWeaverUrl: string) {
|
||||||
const channelName = Object.keys(store.state.videoWeaverUrlsByChannel).find(
|
const channelName = Object.keys(store.state.videoWeaverUrlsByChannel).find(
|
||||||
channelName =>
|
channelName =>
|
||||||
|
@ -7,8 +7,9 @@ import {
|
|||||||
usherHostRegex,
|
usherHostRegex,
|
||||||
videoWeaverHostRegex,
|
videoWeaverHostRegex,
|
||||||
} from "./regexes";
|
} from "./regexes";
|
||||||
|
import updateDnsResponses from "./updateDnsResponses";
|
||||||
|
|
||||||
export default function updateProxySettings() {
|
export function updateProxySettings() {
|
||||||
const { proxyTwitchWebpage, proxyUsherRequests } = store.state;
|
const { proxyTwitchWebpage, proxyUsherRequests } = store.state;
|
||||||
|
|
||||||
const proxies = store.state.optimizedProxiesEnabled
|
const proxies = store.state.optimizedProxiesEnabled
|
||||||
@ -43,6 +44,7 @@ export default function updateProxySettings() {
|
|||||||
console.log(
|
console.log(
|
||||||
`⚙️ Proxying requests through one of: ${proxies.toString() || "<empty>"}`
|
`⚙️ Proxying requests through one of: ${proxies.toString() || "<empty>"}`
|
||||||
);
|
);
|
||||||
|
updateDnsResponses();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,3 +57,9 @@ function getProxyInfoStringFromUrls(urls: string[]): string {
|
|||||||
"DIRECT",
|
"DIRECT",
|
||||||
].join("; ");
|
].join("; ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function clearProxySettings() {
|
||||||
|
chrome.proxy.settings.clear({ scope: "regular" }, function () {
|
||||||
|
console.log("⚙️ Proxy settings cleared");
|
||||||
|
});
|
||||||
|
}
|
@ -1,15 +0,0 @@
|
|||||||
export default async function readFile(accept = "text/plain;charset=utf-8") {
|
|
||||||
return new Promise<string>((resolve, reject) => {
|
|
||||||
const input = document.createElement("input");
|
|
||||||
input.type = "file";
|
|
||||||
input.accept = accept;
|
|
||||||
input.addEventListener("change", async e => {
|
|
||||||
const input = e.target as HTMLInputElement;
|
|
||||||
const file = input.files?.[0];
|
|
||||||
if (!file) return reject("No file selected");
|
|
||||||
const data = await file.text();
|
|
||||||
return resolve(data);
|
|
||||||
});
|
|
||||||
input.click();
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
export default function saveFile(
|
|
||||||
filename: string,
|
|
||||||
content: string,
|
|
||||||
type = "text/plain;charset=utf-8"
|
|
||||||
) {
|
|
||||||
const a = document.createElement("a");
|
|
||||||
a.setAttribute("href", `data:${type},` + encodeURIComponent(content));
|
|
||||||
a.setAttribute("download", filename);
|
|
||||||
a.click();
|
|
||||||
}
|
|
29
src/common/ts/streamStatus.ts
Normal file
29
src/common/ts/streamStatus.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import store from "../../store";
|
||||||
|
import type { StreamStatus } from "../../types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely get the stream status for a channel.
|
||||||
|
* @param channelName
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function getStreamStatus(
|
||||||
|
channelName: string | null
|
||||||
|
): StreamStatus | null {
|
||||||
|
if (!channelName) return null;
|
||||||
|
return store.state.streamStatuses[channelName] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely set the stream status for a channel.
|
||||||
|
* @param channelName
|
||||||
|
* @param streamStatus
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function setStreamStatus(
|
||||||
|
channelName: string | null,
|
||||||
|
streamStatus: StreamStatus
|
||||||
|
): boolean {
|
||||||
|
if (!channelName) return false;
|
||||||
|
store.state.streamStatuses[channelName] = streamStatus;
|
||||||
|
return true;
|
||||||
|
}
|
70
src/common/ts/updateDnsResponses.ts
Normal file
70
src/common/ts/updateDnsResponses.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import ip from "ip";
|
||||||
|
import store from "../../store";
|
||||||
|
import type { DnsResponse } from "../../types";
|
||||||
|
import getProxyInfoFromUrl from "./getProxyInfoFromUrl";
|
||||||
|
|
||||||
|
export default async function updateDnsResponses() {
|
||||||
|
const proxies = [
|
||||||
|
...store.state.optimizedProxies,
|
||||||
|
...store.state.normalProxies,
|
||||||
|
];
|
||||||
|
const proxyInfoArray = proxies.map(getProxyInfoFromUrl);
|
||||||
|
|
||||||
|
for (const proxyInfo of proxyInfoArray) {
|
||||||
|
const { host } = proxyInfo;
|
||||||
|
|
||||||
|
const dnsResponseIndex = store.state.dnsResponses.findIndex(
|
||||||
|
dnsResponse => dnsResponse.host === host
|
||||||
|
);
|
||||||
|
const dnsResponse =
|
||||||
|
dnsResponseIndex !== -1
|
||||||
|
? store.state.dnsResponses[dnsResponseIndex]
|
||||||
|
: null;
|
||||||
|
if (
|
||||||
|
dnsResponse != null &&
|
||||||
|
Date.now() - dnsResponse.timestamp < dnsResponse.ttl * 1000
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ip.isV4Format(host) || ip.isV6Format(host)) {
|
||||||
|
if (dnsResponseIndex !== -1) {
|
||||||
|
store.state.dnsResponses.splice(dnsResponseIndex, 1);
|
||||||
|
}
|
||||||
|
store.state.dnsResponses.push({
|
||||||
|
host,
|
||||||
|
ips: [host],
|
||||||
|
timestamp: Date.now(),
|
||||||
|
ttl: Infinity,
|
||||||
|
} as DnsResponse);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`https://dns.google/resolve?name=${host}`);
|
||||||
|
const json = await response.json();
|
||||||
|
const { Answer } = json;
|
||||||
|
if (!Array.isArray(Answer)) {
|
||||||
|
console.error("Answer is not an array:", Answer);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const ips = Answer.map((answer: any) => answer.data);
|
||||||
|
const ttl =
|
||||||
|
Number(response.headers.get("Cache-Control").split("=")[1]) || 0;
|
||||||
|
if (dnsResponseIndex !== -1) {
|
||||||
|
store.state.dnsResponses.splice(dnsResponseIndex, 1);
|
||||||
|
}
|
||||||
|
store.state.dnsResponses.push({
|
||||||
|
host,
|
||||||
|
ips,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
ttl,
|
||||||
|
} as DnsResponse);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("🔍 DNS responses updated:");
|
||||||
|
console.log(store.state.dnsResponses);
|
||||||
|
}
|
@ -1,37 +1,39 @@
|
|||||||
import pageScript from "url:../page/page.ts";
|
import pageScriptURL from "url:../page/page.ts";
|
||||||
import workerScript from "url:../page/worker.ts";
|
import workerScriptURL from "url:../page/worker.ts";
|
||||||
import { twitchChannelNameRegex } from "../common/ts/regexes";
|
import { twitchChannelNameRegex } from "../common/ts/regexes";
|
||||||
|
import { getStreamStatus, setStreamStatus } from "../common/ts/streamStatus";
|
||||||
import store from "../store";
|
import store from "../store";
|
||||||
|
|
||||||
console.info("[TTV LOL PRO] 🚀 Content script running.");
|
console.info("[TTV LOL PRO] 🚀 Content script running.");
|
||||||
|
|
||||||
injectScript(pageScript);
|
injectPageScript();
|
||||||
|
|
||||||
function injectScript(src: string) {
|
if (store.readyState === "complete") clearStats();
|
||||||
|
else store.addEventListener("load", clearStats);
|
||||||
|
|
||||||
|
window.addEventListener("message", onMessage);
|
||||||
|
|
||||||
|
function injectPageScript() {
|
||||||
// From https://stackoverflow.com/a/9517879
|
// From https://stackoverflow.com/a/9517879
|
||||||
const script = document.createElement("script");
|
const script = document.createElement("script");
|
||||||
script.src = src;
|
script.src = pageScriptURL; // src/page/page.ts
|
||||||
script.dataset.params = JSON.stringify({
|
script.dataset.params = JSON.stringify({
|
||||||
workerScriptURL: workerScript,
|
workerScriptURL: workerScriptURL, // src/page/worker.ts
|
||||||
});
|
});
|
||||||
script.onload = () => script.remove();
|
script.onload = () => script.remove();
|
||||||
// ------------------------------------------
|
// ---------------------------------------
|
||||||
// 🦊🦊🦊 DEAR FIREFOX ADDON REVIEWER 🦊🦊🦊
|
// 🦊 Attention Firefox Addon Reviewer 🦊
|
||||||
// ------------------------------------------
|
// ---------------------------------------
|
||||||
// This is NOT remote code execution. The script being injected is
|
// Please note that this does NOT involve remote code execution. The injected scripts are bundled
|
||||||
// bundled with the extension (look at the `url:` imports above provided by
|
// with the extension. The `url:` imports above are used to get the runtime URLs of the respective scripts.
|
||||||
// the Parcel bundler). By the way, no custom CSP is used.
|
// 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).append(script); // Note: Despite what the TS types say, `document.head` can be `null`.
|
||||||
}
|
}
|
||||||
|
|
||||||
if (store.readyState === "complete") onStoreReady();
|
/**
|
||||||
else store.addEventListener("load", onStoreReady);
|
* Clear stats for stream on page load/reload.
|
||||||
|
* @returns
|
||||||
function onStoreReady() {
|
*/
|
||||||
// Clear stats for stream on page load/reload.
|
|
||||||
clearStats();
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearStats() {
|
function clearStats() {
|
||||||
const match = twitchChannelNameRegex.exec(location.href);
|
const match = twitchChannelNameRegex.exec(location.href);
|
||||||
if (!match) return;
|
if (!match) return;
|
||||||
@ -45,3 +47,18 @@ function clearStats() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onMessage(event: MessageEvent) {
|
||||||
|
if (event.source !== window) return;
|
||||||
|
if (event.data?.type === "UsherResponse") {
|
||||||
|
const { channel, videoWeaverUrls, proxyCountry } = event.data;
|
||||||
|
// Update Video Weaver URLs.
|
||||||
|
store.state.videoWeaverUrlsByChannel[channel] = videoWeaverUrls;
|
||||||
|
// Update proxy country.
|
||||||
|
const streamStatus = getStreamStatus(channel);
|
||||||
|
setStreamStatus(channel, {
|
||||||
|
...(streamStatus ?? { proxied: false, reason: "" }),
|
||||||
|
proxyCountry,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"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.0.2",
|
"version": "2.1.0",
|
||||||
"background": {
|
"background": {
|
||||||
"service_worker": "background/background.ts",
|
"service_worker": "background/background.ts",
|
||||||
"type": "module"
|
"type": "module"
|
||||||
@ -10,9 +10,9 @@
|
|||||||
"declarative_net_request": {
|
"declarative_net_request": {
|
||||||
"rule_resources": [
|
"rule_resources": [
|
||||||
{
|
{
|
||||||
"id": "rules",
|
"id": "ruleset",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"path": "rules/rules.json"
|
"path": "rulesets/ruleset.json"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -23,6 +23,13 @@
|
|||||||
"default_title": "TTV LOL PRO",
|
"default_title": "TTV LOL PRO",
|
||||||
"default_popup": "popup/menu.html"
|
"default_popup": "popup/menu.html"
|
||||||
},
|
},
|
||||||
|
"content_scripts": [
|
||||||
|
{
|
||||||
|
"matches": ["https://www.twitch.tv/*", "https://m.twitch.tv/*"],
|
||||||
|
"js": ["content/content.ts"],
|
||||||
|
"run_at": "document_start"
|
||||||
|
}
|
||||||
|
],
|
||||||
"icons": {
|
"icons": {
|
||||||
"128": "images/brand/icon.png"
|
"128": "images/brand/icon.png"
|
||||||
},
|
},
|
||||||
@ -35,6 +42,7 @@
|
|||||||
"declarativeNetRequest",
|
"declarativeNetRequest",
|
||||||
"proxy",
|
"proxy",
|
||||||
"storage",
|
"storage",
|
||||||
|
"tabs",
|
||||||
"webRequest",
|
"webRequest",
|
||||||
"webRequestAuthProvider"
|
"webRequestAuthProvider"
|
||||||
],
|
],
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"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.0.2",
|
"version": "2.1.0",
|
||||||
"background": {
|
"background": {
|
||||||
"scripts": ["background/background.ts"],
|
"scripts": ["background/background.ts"],
|
||||||
"persistent": false
|
"persistent": false
|
||||||
@ -22,7 +22,7 @@
|
|||||||
},
|
},
|
||||||
"content_scripts": [
|
"content_scripts": [
|
||||||
{
|
{
|
||||||
"matches": ["https://*.twitch.tv/*"],
|
"matches": ["https://www.twitch.tv/*", "https://m.twitch.tv/*"],
|
||||||
"js": ["content/content.ts"],
|
"js": ["content/content.ts"],
|
||||||
"run_at": "document_start"
|
"run_at": "document_start"
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import $ from "../common/ts/$";
|
import $ from "../common/ts/$";
|
||||||
|
import { readFile, saveFile } from "../common/ts/file";
|
||||||
import getProxyInfoFromUrl from "../common/ts/getProxyInfoFromUrl";
|
import getProxyInfoFromUrl from "../common/ts/getProxyInfoFromUrl";
|
||||||
import isChromium from "../common/ts/isChromium";
|
import isChromium from "../common/ts/isChromium";
|
||||||
import readFile from "../common/ts/readFile";
|
import { updateProxySettings } from "../common/ts/proxySettings";
|
||||||
import saveFile from "../common/ts/saveFile";
|
|
||||||
import sendAdLog from "../common/ts/sendAdLog";
|
import sendAdLog from "../common/ts/sendAdLog";
|
||||||
import updateProxySettings from "../common/ts/updateProxySettings";
|
|
||||||
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";
|
||||||
@ -92,12 +91,16 @@ function main() {
|
|||||||
proxyUsherRequestsCheckboxElement.addEventListener("change", () => {
|
proxyUsherRequestsCheckboxElement.addEventListener("change", () => {
|
||||||
const checked = proxyUsherRequestsCheckboxElement.checked;
|
const checked = proxyUsherRequestsCheckboxElement.checked;
|
||||||
store.state.proxyUsherRequests = checked;
|
store.state.proxyUsherRequests = checked;
|
||||||
if (isChromium) updateProxySettings();
|
if (isChromium && store.state.openedTwitchTabs.length > 0) {
|
||||||
|
updateProxySettings();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
proxyTwitchWebpageCheckboxElement.checked = store.state.proxyTwitchWebpage;
|
proxyTwitchWebpageCheckboxElement.checked = store.state.proxyTwitchWebpage;
|
||||||
proxyTwitchWebpageCheckboxElement.addEventListener("change", () => {
|
proxyTwitchWebpageCheckboxElement.addEventListener("change", () => {
|
||||||
store.state.proxyTwitchWebpage = proxyTwitchWebpageCheckboxElement.checked;
|
store.state.proxyTwitchWebpage = proxyTwitchWebpageCheckboxElement.checked;
|
||||||
if (isChromium) updateProxySettings();
|
if (isChromium && store.state.openedTwitchTabs.length > 0) {
|
||||||
|
updateProxySettings();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
// Whitelisted channels
|
// Whitelisted channels
|
||||||
if (isChromium) {
|
if (isChromium) {
|
||||||
@ -142,7 +145,9 @@ function main() {
|
|||||||
isAddAllowed: isNormalProxyUrlAllowed,
|
isAddAllowed: isNormalProxyUrlAllowed,
|
||||||
isEditAllowed: isNormalProxyUrlAllowed,
|
isEditAllowed: isNormalProxyUrlAllowed,
|
||||||
onEdit() {
|
onEdit() {
|
||||||
if (isChromium) updateProxySettings();
|
if (isChromium && store.state.openedTwitchTabs.length > 0) {
|
||||||
|
updateProxySettings();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
hidePromptMarker: true,
|
hidePromptMarker: true,
|
||||||
insertMode: "both",
|
insertMode: "both",
|
||||||
|
@ -1,16 +1,21 @@
|
|||||||
import acceptFlag from "../common/ts/acceptFlag";
|
import acceptFlag from "../common/ts/acceptFlag";
|
||||||
|
import findChannelFromUsherUrl from "../common/ts/findChannelFromUsherUrl";
|
||||||
import getHostFromUrl from "../common/ts/getHostFromUrl";
|
import getHostFromUrl from "../common/ts/getHostFromUrl";
|
||||||
import {
|
import {
|
||||||
twitchGqlHostRegex,
|
twitchGqlHostRegex,
|
||||||
usherHostRegex,
|
usherHostRegex,
|
||||||
videoWeaverHostRegex,
|
videoWeaverHostRegex,
|
||||||
|
videoWeaverUrlRegex,
|
||||||
} from "../common/ts/regexes";
|
} from "../common/ts/regexes";
|
||||||
|
|
||||||
const NATIVE_FETCH = self.fetch;
|
const NATIVE_FETCH = self.fetch;
|
||||||
|
const IS_CHROMIUM = !!self.chrome;
|
||||||
|
|
||||||
export interface FetchOptions {}
|
export interface FetchOptions {
|
||||||
|
scope: "page" | "worker";
|
||||||
|
}
|
||||||
|
|
||||||
export function getFetch(options: FetchOptions = {}): typeof fetch {
|
export function getFetch(options: FetchOptions): typeof fetch {
|
||||||
const knownVideoWeaverUrls = new Set<string>();
|
const knownVideoWeaverUrls = new Set<string>();
|
||||||
const videoWeaverUrlsToFlag = new Map<string, number>(); // Video Weaver URLs to flag -> number of times flagged.
|
const videoWeaverUrlsToFlag = new Map<string, number>(); // Video Weaver URLs to flag -> number of times flagged.
|
||||||
const videoWeaverUrlsToIgnore = new Set<string>(); // No response check.
|
const videoWeaverUrlsToIgnore = new Set<string>(); // No response check.
|
||||||
@ -22,7 +27,13 @@ export function getFetch(options: FetchOptions = {}): typeof fetch {
|
|||||||
const url = input instanceof Request ? input.url : input.toString();
|
const url = input instanceof Request ? input.url : input.toString();
|
||||||
// Firefox doesn't support relative URLs in content scripts (workers too!).
|
// Firefox doesn't support relative URLs in content scripts (workers too!).
|
||||||
// See https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Chrome_incompatibilities#content_script_https_requests
|
// See https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Chrome_incompatibilities#content_script_https_requests
|
||||||
if (url.startsWith("/")) {
|
if (url.startsWith("//")) {
|
||||||
|
// Missing protocol.
|
||||||
|
const newUrl = `${location.protocol}${url}`;
|
||||||
|
if (input instanceof Request) input = new Request(newUrl, input);
|
||||||
|
else input = newUrl;
|
||||||
|
} else if (url.startsWith("/")) {
|
||||||
|
// Missing origin.
|
||||||
const newUrl = `${location.origin}${url}`;
|
const newUrl = `${location.origin}${url}`;
|
||||||
if (input instanceof Request) input = new Request(newUrl, input);
|
if (input instanceof Request) input = new Request(newUrl, input);
|
||||||
else input = newUrl;
|
else input = newUrl;
|
||||||
@ -115,12 +126,19 @@ export function getFetch(options: FetchOptions = {}): typeof fetch {
|
|||||||
if (host != null && usherHostRegex.test(host)) {
|
if (host != null && usherHostRegex.test(host)) {
|
||||||
await readResponseBody();
|
await readResponseBody();
|
||||||
console.debug("[TTV LOL PRO] 🥅 Caught Usher response.");
|
console.debug("[TTV LOL PRO] 🥅 Caught Usher response.");
|
||||||
// Remove all Video Weaver URLs from known URLs.
|
const videoWeaverUrls = responseBody
|
||||||
responseBody.split("\n").forEach(line => {
|
.split("\n")
|
||||||
if (line.includes("video-weaver.")) {
|
.filter(line => videoWeaverUrlRegex.test(line));
|
||||||
knownVideoWeaverUrls.delete(line.trim());
|
// Send Video Weaver URLs to content script.
|
||||||
}
|
sendMessageToContentScript(options.scope, {
|
||||||
|
type: "UsherResponse",
|
||||||
|
channel: findChannelFromUsherUrl(url),
|
||||||
|
videoWeaverUrls,
|
||||||
|
proxyCountry:
|
||||||
|
/USER-COUNTRY="([A-Z]+)"/i.exec(responseBody)?.[1] || null,
|
||||||
});
|
});
|
||||||
|
// Remove all Video Weaver URLs from known URLs.
|
||||||
|
videoWeaverUrls.forEach(url => knownVideoWeaverUrls.delete(url));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Video Weaver responses.
|
// Video Weaver responses.
|
||||||
@ -246,7 +264,24 @@ function removeHeaderFromMap(headersMap: Map<string, string>, name: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sendMessageToContentScript(scope: "page" | "worker", message: any) {
|
||||||
|
if (scope === "page") {
|
||||||
|
self.postMessage(message);
|
||||||
|
} else {
|
||||||
|
self.postMessage({
|
||||||
|
type: "ContentScriptMessage",
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function flagRequest(headersMap: Map<string, string>) {
|
function flagRequest(headersMap: Map<string, string>) {
|
||||||
|
if (IS_CHROMIUM) {
|
||||||
|
console.debug(
|
||||||
|
"[TTV LOL PRO] 🚩 Request flagging is not supported on Chromium. Ignoring…"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const accept = getHeaderFromMap(headersMap, "Accept");
|
const accept = getHeaderFromMap(headersMap, "Accept");
|
||||||
setHeaderToMap(headersMap, "Accept", `${accept || ""}${acceptFlag}`);
|
setHeaderToMap(headersMap, "Accept", `${accept || ""}${acceptFlag}`);
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,8 @@ console.info("[TTV LOL PRO] 🚀 Page script running.");
|
|||||||
|
|
||||||
const params = JSON.parse(document.currentScript.dataset.params);
|
const params = JSON.parse(document.currentScript.dataset.params);
|
||||||
|
|
||||||
window.fetch = getFetch();
|
window.fetch = getFetch({ scope: "page" });
|
||||||
|
|
||||||
// Inject custom worker script to intercept fetch requests made from workers and
|
|
||||||
// decide whether to proxy them or not.
|
|
||||||
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 url = scriptURL.toString();
|
||||||
@ -25,6 +23,11 @@ window.Worker = class Worker extends window.Worker {
|
|||||||
);
|
);
|
||||||
script = `importScripts("${url}");`; // Will fail on Firefox Nightly.
|
script = `importScripts("${url}");`; // Will fail on Firefox Nightly.
|
||||||
}
|
}
|
||||||
|
// ---------------------------------------
|
||||||
|
// 🦊 Attention Firefox Addon Reviewer 🦊
|
||||||
|
// ---------------------------------------
|
||||||
|
// 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.
|
||||||
const newScript = `
|
const newScript = `
|
||||||
try {
|
try {
|
||||||
importScripts("${params.workerScriptURL}");
|
importScripts("${params.workerScriptURL}");
|
||||||
@ -37,6 +40,11 @@ window.Worker = class Worker extends window.Worker {
|
|||||||
new Blob([newScript], { type: "text/javascript" })
|
new Blob([newScript], { type: "text/javascript" })
|
||||||
);
|
);
|
||||||
super(newScriptURL, options);
|
super(newScriptURL, options);
|
||||||
|
this.addEventListener("message", event => {
|
||||||
|
if (event.data?.type === "ContentScriptMessage") {
|
||||||
|
window.postMessage(event.data.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2,4 +2,4 @@ import { getFetch } from "./getFetch";
|
|||||||
|
|
||||||
console.info("[TTV LOL PRO] 🚀 Worker script running.");
|
console.info("[TTV LOL PRO] 🚀 Worker script running.");
|
||||||
|
|
||||||
self.fetch = getFetch();
|
self.fetch = getFetch({ scope: "worker" });
|
||||||
|
@ -67,6 +67,9 @@ function setStreamStatusElement(channelName: string) {
|
|||||||
setProxyStatus(channelNameLower, status);
|
setProxyStatus(channelNameLower, status);
|
||||||
setWhitelistStatus(channelNameLower);
|
setWhitelistStatus(channelNameLower);
|
||||||
streamStatusElement.style.display = "flex";
|
streamStatusElement.style.display = "flex";
|
||||||
|
if (isChromium) {
|
||||||
|
whitelistStatusElement.style.display = "none";
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
streamStatusElement.style.display = "none";
|
streamStatusElement.style.display = "none";
|
||||||
}
|
}
|
||||||
@ -80,6 +83,7 @@ function setProxyStatus(channelNameLower: string, status: StreamStatus) {
|
|||||||
proxiedElement.classList.add("success");
|
proxiedElement.classList.add("success");
|
||||||
} else if (
|
} else if (
|
||||||
!status.proxied &&
|
!status.proxied &&
|
||||||
|
status.proxyHost &&
|
||||||
store.state.optimizedProxiesEnabled &&
|
store.state.optimizedProxiesEnabled &&
|
||||||
store.state.optimizedProxies.length > 0
|
store.state.optimizedProxies.length > 0
|
||||||
) {
|
) {
|
||||||
|
@ -2,11 +2,13 @@ import isChromium from "../common/ts/isChromium";
|
|||||||
import type { State } from "./types";
|
import type { State } from "./types";
|
||||||
|
|
||||||
export default function getDefaultState() {
|
export default function getDefaultState() {
|
||||||
return {
|
const state: State = {
|
||||||
adLog: [],
|
adLog: [],
|
||||||
adLogEnabled: true,
|
adLogEnabled: true,
|
||||||
adLogLastSent: 0,
|
adLogLastSent: 0,
|
||||||
|
dnsResponses: [],
|
||||||
normalProxies: isChromium ? ["chrome.api.cdn-perfprod.com:4023"] : [],
|
normalProxies: isChromium ? ["chrome.api.cdn-perfprod.com:4023"] : [],
|
||||||
|
openedTwitchTabs: [],
|
||||||
optimizedProxies: isChromium ? [] : ["firefox.api.cdn-perfprod.com:2023"],
|
optimizedProxies: isChromium ? [] : ["firefox.api.cdn-perfprod.com:2023"],
|
||||||
optimizedProxiesEnabled: !isChromium,
|
optimizedProxiesEnabled: !isChromium,
|
||||||
proxyTwitchWebpage: false,
|
proxyTwitchWebpage: false,
|
||||||
@ -14,5 +16,6 @@ export default function getDefaultState() {
|
|||||||
streamStatuses: {},
|
streamStatuses: {},
|
||||||
videoWeaverUrlsByChannel: {},
|
videoWeaverUrlsByChannel: {},
|
||||||
whitelistedChannels: [],
|
whitelistedChannels: [],
|
||||||
} as State;
|
};
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { AdLogEntry, StreamStatus } from "../types";
|
import type { AdLogEntry, DnsResponse, StreamStatus } from "../types";
|
||||||
|
|
||||||
export type EventType = "load" | "change";
|
export type EventType = "load" | "change";
|
||||||
export type ReadyState = "loading" | "complete";
|
export type ReadyState = "loading" | "complete";
|
||||||
@ -8,7 +8,9 @@ export interface State {
|
|||||||
adLog: AdLogEntry[];
|
adLog: AdLogEntry[];
|
||||||
adLogEnabled: boolean;
|
adLogEnabled: boolean;
|
||||||
adLogLastSent: number;
|
adLogLastSent: number;
|
||||||
|
dnsResponses: DnsResponse[];
|
||||||
normalProxies: string[];
|
normalProxies: string[];
|
||||||
|
openedTwitchTabs: number[];
|
||||||
optimizedProxies: string[];
|
optimizedProxies: string[];
|
||||||
optimizedProxiesEnabled: boolean;
|
optimizedProxiesEnabled: boolean;
|
||||||
proxyTwitchWebpage: boolean;
|
proxyTwitchWebpage: boolean;
|
||||||
|
@ -43,3 +43,10 @@ export interface StreamStatus {
|
|||||||
notProxied: number;
|
notProxied: number;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DnsResponse {
|
||||||
|
host: string;
|
||||||
|
ips: string[];
|
||||||
|
timestamp: number;
|
||||||
|
ttl: number;
|
||||||
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2020",
|
"target": "ES2020",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"allowSyntheticDefaultImports": true
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"noEmit": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user