mirror of
https://github.com/younesaassila/ttv-lol-pro.git
synced 2025-05-28 12:30:14 +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",
|
||||
"version": "2.0.2",
|
||||
"version": "2.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ttv-lol-pro",
|
||||
"version": "2.0.2",
|
||||
"version": "2.1.0",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"bowser": "^2.11.0",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ttv-lol-pro",
|
||||
"version": "2.0.2",
|
||||
"version": "2.1.0",
|
||||
"description": "TTV LOL PRO removes most livestream ads from Twitch.",
|
||||
"@parcel/bundler-default": {
|
||||
"minBundles": 10000000,
|
||||
|
@ -1,14 +1,15 @@
|
||||
import browser from "webextension-polyfill";
|
||||
import isChromium from "../common/ts/isChromium";
|
||||
import updateProxySettings from "../common/ts/updateProxySettings";
|
||||
import store from "../store";
|
||||
import checkForOpenedTwitchTabs from "./handlers/checkForOpenedTwitchTabs";
|
||||
import onAuthRequired from "./handlers/onAuthRequired";
|
||||
import onBeforeSendHeaders from "./handlers/onBeforeSendHeaders";
|
||||
import onBeforeUsherRequest from "./handlers/onBeforeUsherRequest";
|
||||
import onBeforeVideoWeaverRequest from "./handlers/onBeforeVideoWeaverRequest";
|
||||
import onHeadersReceived from "./handlers/onHeadersReceived";
|
||||
import onProxyRequest from "./handlers/onProxyRequest";
|
||||
import onResponseStarted from "./handlers/onResponseStarted";
|
||||
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.");
|
||||
|
||||
@ -22,13 +23,19 @@ browser.webRequest.onAuthRequired.addListener(
|
||||
["blocking"]
|
||||
);
|
||||
|
||||
// Monitor proxied status of requests.
|
||||
browser.webRequest.onResponseStarted.addListener(onResponseStarted, {
|
||||
urls: ["https://*.ttvnw.net/*", "https://*.twitch.tv/*"],
|
||||
});
|
||||
|
||||
if (isChromium) {
|
||||
const setProxySettings = () => {
|
||||
if (store.readyState !== "complete")
|
||||
return store.addEventListener("load", setProxySettings);
|
||||
updateProxySettings();
|
||||
};
|
||||
setProxySettings();
|
||||
// Check if there are any opened Twitch tabs on startup.
|
||||
checkForOpenedTwitchTabs();
|
||||
|
||||
// Keep track of opened Twitch tabs to enable/disable the PAC script.
|
||||
browser.tabs.onCreated.addListener(onTabCreated);
|
||||
browser.tabs.onUpdated.addListener(onTabUpdated);
|
||||
browser.tabs.onRemoved.addListener(onTabRemoved);
|
||||
} else {
|
||||
// Block tracking pixels.
|
||||
browser.webRequest.onBeforeRequest.addListener(
|
||||
@ -37,15 +44,6 @@ if (isChromium) {
|
||||
["blocking"]
|
||||
);
|
||||
|
||||
// Map channel names to Video Weaver URLs.
|
||||
browser.webRequest.onBeforeRequest.addListener(
|
||||
onBeforeUsherRequest,
|
||||
{
|
||||
urls: ["https://usher.ttvnw.net/api/channel/hls/*"],
|
||||
},
|
||||
["blocking"]
|
||||
);
|
||||
|
||||
// Proxy requests.
|
||||
browser.proxy.onRequest.addListener(
|
||||
onProxyRequest,
|
||||
@ -72,9 +70,4 @@ if (isChromium) {
|
||||
},
|
||||
["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[] {
|
||||
return [
|
||||
...urls.map(url => getProxyInfoFromUrl(url)),
|
||||
...urls.map(getProxyInfoFromUrl),
|
||||
{ type: "direct" } as ProxyInfo, // Fallback to direct connection if all proxies fail.
|
||||
];
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { WebRequest } from "webextension-polyfill";
|
||||
import findChannelFromVideoWeaverUrl from "../../common/ts/findChannelFromVideoWeaverUrl";
|
||||
import getHostFromUrl from "../../common/ts/getHostFromUrl";
|
||||
import getProxyInfoFromUrl from "../../common/ts/getProxyInfoFromUrl";
|
||||
import isChromium from "../../common/ts/isChromium";
|
||||
import {
|
||||
passportHostRegex,
|
||||
twitchGqlHostRegex,
|
||||
@ -8,14 +10,15 @@ import {
|
||||
usherHostRegex,
|
||||
videoWeaverHostRegex,
|
||||
} from "../../common/ts/regexes";
|
||||
import { getStreamStatus, setStreamStatus } from "../../common/ts/streamStatus";
|
||||
import store from "../../store";
|
||||
import type { ProxyInfo, StreamStatus } from "../../types";
|
||||
import type { ProxyInfo } from "../../types";
|
||||
|
||||
export default function onHeadersReceived(
|
||||
details: WebRequest.OnHeadersReceivedDetailsType & {
|
||||
export default function onResponseStarted(
|
||||
details: WebRequest.OnResponseStartedDetailsType & {
|
||||
proxyInfo?: ProxyInfo;
|
||||
}
|
||||
): void | WebRequest.BlockingResponseOrPromise {
|
||||
): void {
|
||||
const host = getHostFromUrl(details.url);
|
||||
if (!host) return;
|
||||
|
||||
@ -77,25 +80,31 @@ export default function onHeadersReceived(
|
||||
}
|
||||
|
||||
function getProxyFromDetails(
|
||||
details: WebRequest.OnHeadersReceivedDetailsType & {
|
||||
details: WebRequest.OnResponseStartedDetailsType & {
|
||||
proxyInfo?: ProxyInfo;
|
||||
}
|
||||
): string | null {
|
||||
const proxyInfo = details.proxyInfo; // Firefox only.
|
||||
if (!proxyInfo || proxyInfo.type === "direct") return null;
|
||||
return `${proxyInfo.host}:${proxyInfo.port}`;
|
||||
}
|
||||
|
||||
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;
|
||||
if (isChromium) {
|
||||
const ip = details.ip;
|
||||
if (!ip) return null;
|
||||
const dnsResponse = store.state.dnsResponses.find(
|
||||
dnsResponse => dnsResponse.ips.indexOf(ip) !== -1
|
||||
);
|
||||
if (!dnsResponse) return null;
|
||||
const proxies = [
|
||||
...store.state.optimizedProxies,
|
||||
...store.state.normalProxies,
|
||||
];
|
||||
const proxyInfoArray = proxies.map(getProxyInfoFromUrl);
|
||||
const possibleProxies = proxyInfoArray.filter(
|
||||
proxy => proxy.host === dnsResponse.host
|
||||
);
|
||||
if (possibleProxies.length === 1)
|
||||
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")
|
||||
return store.addEventListener("load", onStartupStoreCleanup);
|
||||
|
||||
store.state.dnsResponses = [];
|
||||
store.state.openedTwitchTabs = [];
|
||||
store.state.streamStatuses = {};
|
||||
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";
|
||||
|
||||
/**
|
||||
* 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(
|
||||
usherUrl: string
|
||||
): string | null {
|
||||
|
@ -1,5 +1,11 @@
|
||||
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) {
|
||||
const channelName = Object.keys(store.state.videoWeaverUrlsByChannel).find(
|
||||
channelName =>
|
||||
|
@ -7,8 +7,9 @@ import {
|
||||
usherHostRegex,
|
||||
videoWeaverHostRegex,
|
||||
} from "./regexes";
|
||||
import updateDnsResponses from "./updateDnsResponses";
|
||||
|
||||
export default function updateProxySettings() {
|
||||
export function updateProxySettings() {
|
||||
const { proxyTwitchWebpage, proxyUsherRequests } = store.state;
|
||||
|
||||
const proxies = store.state.optimizedProxiesEnabled
|
||||
@ -43,6 +44,7 @@ export default function updateProxySettings() {
|
||||
console.log(
|
||||
`⚙️ Proxying requests through one of: ${proxies.toString() || "<empty>"}`
|
||||
);
|
||||
updateDnsResponses();
|
||||
});
|
||||
}
|
||||
|
||||
@ -55,3 +57,9 @@ function getProxyInfoStringFromUrls(urls: string[]): string {
|
||||
"DIRECT",
|
||||
].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 workerScript from "url:../page/worker.ts";
|
||||
import pageScriptURL from "url:../page/page.ts";
|
||||
import workerScriptURL from "url:../page/worker.ts";
|
||||
import { twitchChannelNameRegex } from "../common/ts/regexes";
|
||||
import { getStreamStatus, setStreamStatus } from "../common/ts/streamStatus";
|
||||
import store from "../store";
|
||||
|
||||
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
|
||||
const script = document.createElement("script");
|
||||
script.src = src;
|
||||
script.src = pageScriptURL; // src/page/page.ts
|
||||
script.dataset.params = JSON.stringify({
|
||||
workerScriptURL: workerScript,
|
||||
workerScriptURL: workerScriptURL, // src/page/worker.ts
|
||||
});
|
||||
script.onload = () => script.remove();
|
||||
// ------------------------------------------
|
||||
// 🦊🦊🦊 DEAR FIREFOX ADDON REVIEWER 🦊🦊🦊
|
||||
// ------------------------------------------
|
||||
// This is NOT remote code execution. The script being injected is
|
||||
// bundled with the extension (look at the `url:` imports above provided by
|
||||
// the Parcel bundler). By the way, no custom CSP is used.
|
||||
// ---------------------------------------
|
||||
// 🦊 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).append(script); // Note: Despite what the TS types say, `document.head` can be `null`.
|
||||
}
|
||||
|
||||
if (store.readyState === "complete") onStoreReady();
|
||||
else store.addEventListener("load", onStoreReady);
|
||||
|
||||
function onStoreReady() {
|
||||
// Clear stats for stream on page load/reload.
|
||||
clearStats();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear stats for stream on page load/reload.
|
||||
* @returns
|
||||
*/
|
||||
function clearStats() {
|
||||
const match = twitchChannelNameRegex.exec(location.href);
|
||||
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,
|
||||
"name": "TTV LOL PRO",
|
||||
"description": "TTV LOL PRO removes most livestream ads from Twitch.",
|
||||
"version": "2.0.2",
|
||||
"version": "2.1.0",
|
||||
"background": {
|
||||
"service_worker": "background/background.ts",
|
||||
"type": "module"
|
||||
@ -10,9 +10,9 @@
|
||||
"declarative_net_request": {
|
||||
"rule_resources": [
|
||||
{
|
||||
"id": "rules",
|
||||
"id": "ruleset",
|
||||
"enabled": true,
|
||||
"path": "rules/rules.json"
|
||||
"path": "rulesets/ruleset.json"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -23,6 +23,13 @@
|
||||
"default_title": "TTV LOL PRO",
|
||||
"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": {
|
||||
"128": "images/brand/icon.png"
|
||||
},
|
||||
@ -35,6 +42,7 @@
|
||||
"declarativeNetRequest",
|
||||
"proxy",
|
||||
"storage",
|
||||
"tabs",
|
||||
"webRequest",
|
||||
"webRequestAuthProvider"
|
||||
],
|
||||
|
@ -2,7 +2,7 @@
|
||||
"manifest_version": 2,
|
||||
"name": "TTV LOL PRO",
|
||||
"description": "TTV LOL PRO removes most livestream ads from Twitch.",
|
||||
"version": "2.0.2",
|
||||
"version": "2.1.0",
|
||||
"background": {
|
||||
"scripts": ["background/background.ts"],
|
||||
"persistent": false
|
||||
@ -22,7 +22,7 @@
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["https://*.twitch.tv/*"],
|
||||
"matches": ["https://www.twitch.tv/*", "https://m.twitch.tv/*"],
|
||||
"js": ["content/content.ts"],
|
||||
"run_at": "document_start"
|
||||
}
|
||||
|
@ -1,10 +1,9 @@
|
||||
import $ from "../common/ts/$";
|
||||
import { readFile, saveFile } from "../common/ts/file";
|
||||
import getProxyInfoFromUrl from "../common/ts/getProxyInfoFromUrl";
|
||||
import isChromium from "../common/ts/isChromium";
|
||||
import readFile from "../common/ts/readFile";
|
||||
import saveFile from "../common/ts/saveFile";
|
||||
import { updateProxySettings } from "../common/ts/proxySettings";
|
||||
import sendAdLog from "../common/ts/sendAdLog";
|
||||
import updateProxySettings from "../common/ts/updateProxySettings";
|
||||
import store from "../store";
|
||||
import getDefaultState from "../store/getDefaultState";
|
||||
import type { State } from "../store/types";
|
||||
@ -92,12 +91,16 @@ function main() {
|
||||
proxyUsherRequestsCheckboxElement.addEventListener("change", () => {
|
||||
const checked = proxyUsherRequestsCheckboxElement.checked;
|
||||
store.state.proxyUsherRequests = checked;
|
||||
if (isChromium) updateProxySettings();
|
||||
if (isChromium && store.state.openedTwitchTabs.length > 0) {
|
||||
updateProxySettings();
|
||||
}
|
||||
});
|
||||
proxyTwitchWebpageCheckboxElement.checked = store.state.proxyTwitchWebpage;
|
||||
proxyTwitchWebpageCheckboxElement.addEventListener("change", () => {
|
||||
store.state.proxyTwitchWebpage = proxyTwitchWebpageCheckboxElement.checked;
|
||||
if (isChromium) updateProxySettings();
|
||||
if (isChromium && store.state.openedTwitchTabs.length > 0) {
|
||||
updateProxySettings();
|
||||
}
|
||||
});
|
||||
// Whitelisted channels
|
||||
if (isChromium) {
|
||||
@ -142,7 +145,9 @@ function main() {
|
||||
isAddAllowed: isNormalProxyUrlAllowed,
|
||||
isEditAllowed: isNormalProxyUrlAllowed,
|
||||
onEdit() {
|
||||
if (isChromium) updateProxySettings();
|
||||
if (isChromium && store.state.openedTwitchTabs.length > 0) {
|
||||
updateProxySettings();
|
||||
}
|
||||
},
|
||||
hidePromptMarker: true,
|
||||
insertMode: "both",
|
||||
|
@ -1,16 +1,21 @@
|
||||
import acceptFlag from "../common/ts/acceptFlag";
|
||||
import findChannelFromUsherUrl from "../common/ts/findChannelFromUsherUrl";
|
||||
import getHostFromUrl from "../common/ts/getHostFromUrl";
|
||||
import {
|
||||
twitchGqlHostRegex,
|
||||
usherHostRegex,
|
||||
videoWeaverHostRegex,
|
||||
videoWeaverUrlRegex,
|
||||
} from "../common/ts/regexes";
|
||||
|
||||
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 videoWeaverUrlsToFlag = new Map<string, number>(); // Video Weaver URLs to flag -> number of times flagged.
|
||||
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();
|
||||
// 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
|
||||
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}`;
|
||||
if (input instanceof Request) input = new Request(newUrl, input);
|
||||
else input = newUrl;
|
||||
@ -115,12 +126,19 @@ export function getFetch(options: FetchOptions = {}): typeof fetch {
|
||||
if (host != null && usherHostRegex.test(host)) {
|
||||
await readResponseBody();
|
||||
console.debug("[TTV LOL PRO] 🥅 Caught Usher response.");
|
||||
// Remove all Video Weaver URLs from known URLs.
|
||||
responseBody.split("\n").forEach(line => {
|
||||
if (line.includes("video-weaver.")) {
|
||||
knownVideoWeaverUrls.delete(line.trim());
|
||||
}
|
||||
const videoWeaverUrls = responseBody
|
||||
.split("\n")
|
||||
.filter(line => videoWeaverUrlRegex.test(line));
|
||||
// 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.
|
||||
@ -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>) {
|
||||
if (IS_CHROMIUM) {
|
||||
console.debug(
|
||||
"[TTV LOL PRO] 🚩 Request flagging is not supported on Chromium. Ignoring…"
|
||||
);
|
||||
return;
|
||||
}
|
||||
const accept = getHeaderFromMap(headersMap, "Accept");
|
||||
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);
|
||||
|
||||
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 {
|
||||
constructor(scriptURL: string | URL, options?: WorkerOptions) {
|
||||
const url = scriptURL.toString();
|
||||
@ -25,6 +23,11 @@ window.Worker = class Worker extends window.Worker {
|
||||
);
|
||||
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 = `
|
||||
try {
|
||||
importScripts("${params.workerScriptURL}");
|
||||
@ -37,6 +40,11 @@ window.Worker = class Worker extends window.Worker {
|
||||
new Blob([newScript], { type: "text/javascript" })
|
||||
);
|
||||
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.");
|
||||
|
||||
self.fetch = getFetch();
|
||||
self.fetch = getFetch({ scope: "worker" });
|
||||
|
@ -67,6 +67,9 @@ function setStreamStatusElement(channelName: string) {
|
||||
setProxyStatus(channelNameLower, status);
|
||||
setWhitelistStatus(channelNameLower);
|
||||
streamStatusElement.style.display = "flex";
|
||||
if (isChromium) {
|
||||
whitelistStatusElement.style.display = "none";
|
||||
}
|
||||
} else {
|
||||
streamStatusElement.style.display = "none";
|
||||
}
|
||||
@ -80,6 +83,7 @@ function setProxyStatus(channelNameLower: string, status: StreamStatus) {
|
||||
proxiedElement.classList.add("success");
|
||||
} else if (
|
||||
!status.proxied &&
|
||||
status.proxyHost &&
|
||||
store.state.optimizedProxiesEnabled &&
|
||||
store.state.optimizedProxies.length > 0
|
||||
) {
|
||||
|
@ -2,11 +2,13 @@ import isChromium from "../common/ts/isChromium";
|
||||
import type { State } from "./types";
|
||||
|
||||
export default function getDefaultState() {
|
||||
return {
|
||||
const state: State = {
|
||||
adLog: [],
|
||||
adLogEnabled: true,
|
||||
adLogLastSent: 0,
|
||||
dnsResponses: [],
|
||||
normalProxies: isChromium ? ["chrome.api.cdn-perfprod.com:4023"] : [],
|
||||
openedTwitchTabs: [],
|
||||
optimizedProxies: isChromium ? [] : ["firefox.api.cdn-perfprod.com:2023"],
|
||||
optimizedProxiesEnabled: !isChromium,
|
||||
proxyTwitchWebpage: false,
|
||||
@ -14,5 +16,6 @@ export default function getDefaultState() {
|
||||
streamStatuses: {},
|
||||
videoWeaverUrlsByChannel: {},
|
||||
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 ReadyState = "loading" | "complete";
|
||||
@ -8,7 +8,9 @@ export interface State {
|
||||
adLog: AdLogEntry[];
|
||||
adLogEnabled: boolean;
|
||||
adLogLastSent: number;
|
||||
dnsResponses: DnsResponse[];
|
||||
normalProxies: string[];
|
||||
openedTwitchTabs: number[];
|
||||
optimizedProxies: string[];
|
||||
optimizedProxiesEnabled: boolean;
|
||||
proxyTwitchWebpage: boolean;
|
||||
|
@ -43,3 +43,10 @@ export interface StreamStatus {
|
||||
notProxied: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface DnsResponse {
|
||||
host: string;
|
||||
ips: string[];
|
||||
timestamp: number;
|
||||
ttl: number;
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user