Merge pull request #369 from younesaassila/v2.4.0

Release version 2.4.0
This commit is contained in:
Younes Aassila 2025-02-13 15:11:32 +01:00 committed by GitHub
commit fa1c74e23e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 1375 additions and 1346 deletions

View File

@ -6,15 +6,17 @@ on:
tags-ignore: ["**"] tags-ignore: ["**"]
pull_request: pull_request:
branches: ["**"] branches: ["**"]
types: [opened, synchronize, reopened, ready_for_review]
jobs: jobs:
build: build:
name: Build name: Build
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ !github.event.pull_request.draft }}
concurrency: build-${{ github.ref }} concurrency: build-${{ github.ref }}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Install dependencies - name: Install dependencies
run: npm install run: npm install
- name: Run linter - name: Run linter

View File

@ -18,6 +18,7 @@ on:
pull_request: pull_request:
# The branches below must be a subset of the branches above # The branches below must be a subset of the branches above
branches: ["v2"] branches: ["v2"]
types: [opened, synchronize, reopened, ready_for_review]
schedule: schedule:
- cron: "32 9 * * 6" - cron: "32 9 * * 6"
@ -25,6 +26,7 @@ jobs:
analyze: analyze:
name: Analyze name: Analyze
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ !github.event.pull_request.draft }}
concurrency: codeql-${{ github.ref }} concurrency: codeql-${{ github.ref }}
permissions: permissions:
actions: read actions: read
@ -42,11 +44,11 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v2 uses: github/codeql-action/init@v3
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
@ -59,7 +61,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v2 uses: github/codeql-action/autobuild@v3
# Command-line programs to run using the OS shell. # Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@ -72,6 +74,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh # ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2 uses: github/codeql-action/analyze@v3
with: with:
category: "/language:${{matrix.language}}" category: "/language:${{matrix.language}}"

View File

@ -5,14 +5,20 @@
# Source repository: https://github.com/actions/dependency-review-action # Source repository: https://github.com/actions/dependency-review-action
# Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement # Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement
name: "Dependency Review" name: "Dependency Review"
on: [pull_request] on:
pull_request:
branches: ["**"]
types: [opened, synchronize, reopened, ready_for_review]
permissions: permissions:
contents: read contents: read
jobs: jobs:
dependency-review: dependency-review:
name: Dependency Review
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ !github.event.pull_request.draft }}
concurrency: dependencies-${{ github.ref }}
steps: steps:
- name: "Checkout Repository" - name: "Checkout Repository"
uses: actions/checkout@v4 uses: actions/checkout@v4

2109
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "ttv-lol-pro", "name": "ttv-lol-pro",
"version": "2.3.10", "version": "2.4.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,
@ -43,24 +43,22 @@
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"bowser": "^2.11.0", "bowser": "^2.11.0",
"ip-address": "^9.0.5", "ip-address": "^10.0.1",
"m3u8-parser": "^7.2.0" "m3u8-parser": "^7.2.0"
}, },
"devDependencies": { "devDependencies": {
"@parcel/config-webextension": "^2.12.0", "@parcel/config-webextension": "^2.13.3",
"@types/chrome": "^0.0.271", "@types/chrome": "^0.0.304",
"@types/jsbn": "^1.2.33", "@types/node": "^22.13.2",
"@types/node": "^20.16.5",
"@types/webextension-polyfill": "^0.12.1", "@types/webextension-polyfill": "^0.12.1",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"os-browserify": "^0.3.0", "parcel": "^2.13.3",
"parcel": "^2.12.0",
"postcss": "^8.4.47",
"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",
"shx": "^0.3.4", "shx": "^0.3.4",
"typescript": "^5.6.2", "svgo": "^3.3.2",
"typescript": "^5.7.3",
"webextension-polyfill": "^0.12.0" "webextension-polyfill": "^0.12.0"
}, },
"private": true "private": true

View File

@ -4,8 +4,8 @@ export default function isChannelWhitelisted(
channelName: string | null channelName: string | null
): boolean { ): boolean {
if (!channelName) return false; if (!channelName) return false;
const whitelistedChannelsLower = store.state.whitelistedChannels.map( const channelNameLower = channelName.toLowerCase();
channel => channel.toLowerCase() return store.state.whitelistedChannels.some(
channel => channel.toLowerCase() === channelNameLower
); );
return whitelistedChannelsLower.includes(channelName.toLowerCase());
} }

View File

@ -1,9 +1,24 @@
import { Address6 } from "ip-address"; import { Address6 } from "ip-address";
import type { ProxyInfo } from "../../types"; import type { ProxyInfo, ProxyType } from "../../types";
const DEFAULT_PORTS: Readonly<Record<ProxyType, number>> = Object.freeze({
direct: 0,
http: 80,
https: 443,
socks: 1080,
socks4: 1080,
});
export function getProxyInfoFromUrl( export function getProxyInfoFromUrl(
url: string url: string
): ProxyInfo & { type: "http"; host: string; port: number } { ): ProxyInfo & { host: string; port: number } {
let type: ProxyType | undefined = undefined;
if (url.includes("://")) {
const [protocol] = url.split("://", 1);
type = protocol as ProxyType;
url = url.substring(protocol.length + 3, url.length);
}
const lastIndexOfAt = url.lastIndexOf("@"); const lastIndexOfAt = url.lastIndexOf("@");
const hostname = url.substring(lastIndexOfAt + 1, url.length); const hostname = url.substring(lastIndexOfAt + 1, url.length);
const lastIndexOfColon = getLastIndexOfColon(hostname); const lastIndexOfColon = getLastIndexOfColon(hostname);
@ -12,7 +27,7 @@ export function getProxyInfoFromUrl(
let port: number | undefined = undefined; let port: number | undefined = undefined;
if (lastIndexOfColon === -1) { if (lastIndexOfColon === -1) {
host = hostname; host = hostname;
port = 3128; // Default port port = type ? DEFAULT_PORTS[type] : 3128; // Default port
} else { } else {
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));
@ -31,7 +46,7 @@ export function getProxyInfoFromUrl(
} }
return { return {
type: "http", type: type ?? "http",
host, host,
port, port,
username, username,

View File

@ -1,5 +1,5 @@
import store from "../../store"; import store from "../../store";
import { ProxyRequestType } from "../../types"; import { ProxyRequestType, ProxyType } from "../../types";
import isRequestTypeProxied from "./isRequestTypeProxied"; import isRequestTypeProxied from "./isRequestTypeProxied";
import { getProxyInfoFromUrl, getUrlFromProxyInfo } from "./proxyInfo"; import { getProxyInfoFromUrl, getUrlFromProxyInfo } from "./proxyInfo";
import { import {
@ -11,6 +11,14 @@ import {
} from "./regexes"; } from "./regexes";
import updateDnsResponses from "./updateDnsResponses"; import updateDnsResponses from "./updateDnsResponses";
const PROXY_TYPE_MAP: Readonly<Record<ProxyType, string>> = Object.freeze({
direct: "DIRECT",
http: "PROXY",
https: "HTTPS",
socks: "SOCKS5",
socks4: "SOCKS4",
});
export function updateProxySettings(requestFilter?: ProxyRequestType[]) { export function updateProxySettings(requestFilter?: ProxyRequestType[]) {
const { optimizedProxiesEnabled, passportLevel } = store.state; const { optimizedProxiesEnabled, passportLevel } = store.state;
@ -92,7 +100,7 @@ function getProxyInfoStringFromUrls(urls: string[]): string {
return [ return [
...urls.map(url => { ...urls.map(url => {
const proxyInfo = getProxyInfoFromUrl(url); const proxyInfo = getProxyInfoFromUrl(url);
return `PROXY ${getUrlFromProxyInfo({ return `${PROXY_TYPE_MAP[proxyInfo.type]} ${getUrlFromProxyInfo({
...proxyInfo, ...proxyInfo,
// Don't include username/password in PAC script. // Don't include username/password in PAC script.
username: undefined, username: undefined,

View File

@ -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\/|moderator\/)?((?!(?:directory|jobs|p|privacy|store|turbo)\b)\w+)/i; /^https?:\/\/(?:www|m)\.twitch\.tv\/(?:videos\/|popout\/|moderator\/)?((?!(?:directory|downloads|jobs|p|privacy|search|settings|store|turbo)\b)\w+)/i;
export const twitchGqlHostRegex = /^gql\.twitch\.tv$/i; export const 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;

View File

@ -2,6 +2,7 @@ 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 browser, { Storage } from "webextension-polyfill";
import findChannelFromTwitchTvUrl from "../common/ts/findChannelFromTwitchTvUrl"; 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 { getStreamStatus, setStreamStatus } from "../common/ts/streamStatus"; import { getStreamStatus, setStreamStatus } from "../common/ts/streamStatus";
import store from "../store"; import store from "../store";
@ -46,10 +47,13 @@ function onStoreLoad() {
/** /**
* Clear stats for stream on page load/reload. * Clear stats for stream on page load/reload.
* @param channelName
* @param delayMs
* @returns * @returns
*/ */
function clearStats(channelName: string | null) { async function clearStats(channelName: string | null, delayMs?: number) {
if (!channelName) return; if (!channelName) return;
if (delayMs) await new Promise(resolve => setTimeout(resolve, delayMs));
const channelNameLower = channelName.toLowerCase(); const channelNameLower = channelName.toLowerCase();
if (store.state.streamStatuses.hasOwnProperty(channelNameLower)) { if (store.state.streamStatuses.hasOwnProperty(channelNameLower)) {
delete store.state.streamStatuses[channelNameLower]; delete store.state.streamStatuses[channelNameLower];
@ -97,66 +101,118 @@ function onBackgroundMessage(message: any): undefined {
} }
function onPageMessage(event: MessageEvent) { function onPageMessage(event: MessageEvent) {
if (event.data?.type !== MessageType.ContentScriptMessage) return; if (!event.data || event.data.type !== MessageType.ContentScriptMessage) {
return;
}
const message = event.data?.message; const { message } = event.data;
if (!message) return; if (!message) return;
switch (message.type) { if (message.type === MessageType.GetStoreState) {
case MessageType.GetStoreState: const sendStoreState = () => {
const sendStoreState = () => { window.postMessage({
window.postMessage({ type: MessageType.PageScriptMessage,
type: MessageType.PageScriptMessage, message: {
message: { type: MessageType.GetStoreStateResponse,
type: MessageType.GetStoreStateResponse, state: JSON.parse(JSON.stringify(store.state)),
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:
try {
browser.runtime.sendMessage(message);
} catch (error) {
console.error(
"[TTV LOL PRO] Failed to send UsherResponse message",
error
);
}
break;
case MessageType.MultipleAdBlockersInUse:
const channelName = findChannelFromTwitchTvUrl(location.href);
if (!channelName) break;
const streamStatus = getStreamStatus(channelName);
setStreamStatus(channelName, {
...(streamStatus ?? { proxied: false }),
reason: "Another Twitch ad blocker is in use",
}); });
break; };
case MessageType.ClearStats: if (store.readyState === "complete") sendStoreState();
clearStats(message.channelName); else store.addEventListener("load", sendStoreState);
break; }
// ---
else if (message.type === MessageType.EnableFullMode) {
try {
browser.runtime.sendMessage(message);
} catch (error) {
console.error(
"[TTV LOL PRO] Failed to send EnableFullMode message",
error
);
}
}
// ---
else if (message.type === MessageType.DisableFullMode) {
try {
browser.runtime.sendMessage(message);
} catch (error) {
console.error(
"[TTV LOL PRO] Failed to send DisableFullMode message",
error
);
}
}
// ---
else if (message.type === MessageType.ChannelSubStatusChange) {
const { channelNameLower, wasSubscribed, isSubscribed } = message;
const isWhitelisted = isChannelWhitelisted(channelNameLower);
console.log("[TTV LOL PRO] ChannelSubStatusChange", {
channelNameLower,
wasSubscribed,
isSubscribed,
isWhitelisted,
});
const currentChannelNameLower = findChannelFromTwitchTvUrl(
location.href
)?.toLowerCase();
if (store.state.whitelistChannelSubscriptions && channelNameLower != null) {
if (!wasSubscribed && isSubscribed) {
store.state.activeChannelSubscriptions.push(channelNameLower);
// Add to whitelist.
if (!isWhitelisted) {
console.log(
`[TTV LOL PRO] Adding '${channelNameLower}' to whitelist.`
);
store.state.whitelistedChannels.push(channelNameLower);
if (channelNameLower === currentChannelNameLower) {
location.reload();
}
}
} else if (wasSubscribed && !isSubscribed) {
store.state.activeChannelSubscriptions =
store.state.activeChannelSubscriptions.filter(
channel => channel.toLowerCase() !== channelNameLower
);
// Remove from whitelist.
if (isWhitelisted) {
console.log(
`[TTV LOL PRO] Removing '${channelNameLower}' from whitelist.`
);
store.state.whitelistedChannels =
store.state.whitelistedChannels.filter(
channel => channel.toLowerCase() !== channelNameLower
);
if (channelNameLower === currentChannelNameLower) {
location.reload();
}
}
}
}
}
// ---
else if (message.type === MessageType.UsherResponse) {
try {
browser.runtime.sendMessage(message);
} catch (error) {
console.error(
"[TTV LOL PRO] Failed to send UsherResponse message",
error
);
}
}
// ---
else if (message.type === MessageType.MultipleAdBlockersInUse) {
const channelName = findChannelFromTwitchTvUrl(location.href);
if (!channelName) return;
const streamStatus = getStreamStatus(channelName);
setStreamStatus(channelName, {
...(streamStatus ?? { proxied: false }),
reason: "Another Twitch ad blocker is in use",
});
}
// ---
else if (message.type === MessageType.ClearStats) {
clearStats(message.channelName, 2000);
} }
} }

View File

@ -3,7 +3,7 @@
"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.",
"homepage_url": "https://github.com/younesaassila/ttv-lol-pro", "homepage_url": "https://github.com/younesaassila/ttv-lol-pro",
"version": "2.3.10", "version": "2.4.0",
"background": { "background": {
"service_worker": "background/background.ts", "service_worker": "background/background.ts",
"type": "module" "type": "module"

View File

@ -3,7 +3,7 @@
"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.",
"homepage_url": "https://github.com/younesaassila/ttv-lol-pro", "homepage_url": "https://github.com/younesaassila/ttv-lol-pro",
"version": "2.3.10", "version": "2.4.0",
"background": { "background": {
"scripts": ["background/background.ts"], "scripts": ["background/background.ts"],
"persistent": false "persistent": false

View File

@ -28,7 +28,7 @@ type ListOptions = {
getPromptPlaceholder(insertMode: InsertMode): string; getPromptPlaceholder(insertMode: InsertMode): string;
isAddAllowed(text: string): AllowedResult; isAddAllowed(text: string): AllowedResult;
isEditAllowed(text: string): AllowedResult; isEditAllowed(text: string): AllowedResult;
onEdit?(text: string): void; onChange?(oldText: string | undefined, newText: string): void;
focusPrompt: boolean; focusPrompt: boolean;
hidePromptMarker: boolean; hidePromptMarker: boolean;
insertMode: InsertMode; insertMode: InsertMode;
@ -75,6 +75,9 @@ const passportLevelProxyUsageWwwElement = $(
const whitelistedChannelsListElement = $( const whitelistedChannelsListElement = $(
"#whitelisted-channels-list" "#whitelisted-channels-list"
) as HTMLUListElement; ) as HTMLUListElement;
const whitelistSubscriptionsCheckboxElement = $(
"#whitelist-subscriptions-checkbox"
) as HTMLInputElement;
// Proxies // Proxies
const optimizedProxiesInputElement = $("#optimized") as HTMLInputElement; const optimizedProxiesInputElement = $("#optimized") as HTMLInputElement;
const optimizedProxiesListElement = $( const optimizedProxiesListElement = $(
@ -82,6 +85,9 @@ const optimizedProxiesListElement = $(
) as HTMLOListElement; ) as HTMLOListElement;
const normalProxiesInputElement = $("#normal") as HTMLInputElement; const normalProxiesInputElement = $("#normal") as HTMLInputElement;
const normalProxiesListElement = $("#normal-proxies-list") as HTMLOListElement; const normalProxiesListElement = $("#normal-proxies-list") as HTMLOListElement;
const otherProtocolsCheckboxElement = $(
"#other-protocols-checkbox"
) as HTMLInputElement;
// Ad log // Ad log
const adLogEnabledCheckboxElement = $( const adLogEnabledCheckboxElement = $(
"#ad-log-enabled-checkbox" "#ad-log-enabled-checkbox"
@ -156,55 +162,36 @@ function main() {
} }
return [true]; return [true];
}, },
isEditAllowed(text) { });
if (!/^[a-z0-9_]+$/i.test(text)) { whitelistSubscriptionsCheckboxElement.checked =
return [false, `'${text}' is not a valid channel name`]; store.state.whitelistChannelSubscriptions;
} whitelistSubscriptionsCheckboxElement.addEventListener("change", () => {
return [true]; const { checked } = whitelistSubscriptionsCheckboxElement;
}, store.state.whitelistChannelSubscriptions = checked;
if (!checked) {
// Clear active channel subscriptions to free up storage space.
store.state.activeChannelSubscriptions = [];
}
}); });
// Proxies // Proxies
if (store.state.optimizedProxiesEnabled) if (store.state.optimizedProxiesEnabled)
optimizedProxiesInputElement.checked = true; optimizedProxiesInputElement.checked = true;
else normalProxiesInputElement.checked = true; else normalProxiesInputElement.checked = true;
const onProxyTypeChange = () => { const onProxyModeChange = () => {
store.state.optimizedProxiesEnabled = optimizedProxiesInputElement.checked; store.state.optimizedProxiesEnabled = optimizedProxiesInputElement.checked;
if (isChromium && store.state.chromiumProxyActive) { if (isChromium && store.state.chromiumProxyActive) {
updateProxySettings(); updateProxySettings();
} }
updateProxyUsage(); updateProxyUsage();
}; };
optimizedProxiesInputElement.addEventListener("change", onProxyTypeChange); optimizedProxiesInputElement.addEventListener("change", onProxyModeChange);
normalProxiesInputElement.addEventListener("change", onProxyTypeChange); normalProxiesInputElement.addEventListener("change", onProxyModeChange);
listInit(optimizedProxiesListElement, "optimizedProxies", { loadProxiesLists();
getPromptPlaceholder: insertMode => { otherProtocolsCheckboxElement.checked = store.state.allowOtherProxyProtocols;
if (insertMode == "prepend") return "Enter a proxy URL… (Primary)"; otherProtocolsCheckboxElement.addEventListener("change", () => {
return "Enter a proxy URL… (Fallback)"; store.state.allowOtherProxyProtocols =
}, otherProtocolsCheckboxElement.checked;
isAddAllowed: isOptimizedProxyUrlAllowed, loadProxiesLists();
isEditAllowed: isOptimizedProxyUrlAllowed,
onEdit() {
if (isChromium && store.state.chromiumProxyActive) {
updateProxySettings();
}
},
hidePromptMarker: true,
insertMode: "both",
});
listInit(normalProxiesListElement, "normalProxies", {
getPromptPlaceholder: insertMode => {
if (insertMode == "prepend") return "Enter a proxy URL… (Primary)";
return "Enter a proxy URL… (Fallback)";
},
isAddAllowed: isNormalProxyUrlAllowed,
isEditAllowed: isNormalProxyUrlAllowed,
onEdit() {
if (isChromium && store.state.chromiumProxyActive) {
updateProxySettings();
}
},
hidePromptMarker: true,
insertMode: "both",
}); });
// Ad log // Ad log
adLogEnabledCheckboxElement.checked = store.state.adLogEnabled; adLogEnabledCheckboxElement.checked = store.state.adLogEnabled;
@ -291,6 +278,37 @@ function updateProxyUsage() {
} }
} }
function loadProxiesLists() {
listInit(optimizedProxiesListElement, "optimizedProxies", {
getPromptPlaceholder: insertMode => {
if (insertMode == "prepend") return "Enter a proxy URL… (Primary)";
return "Enter a proxy URL… (Fallback)";
},
isAddAllowed: isOptimizedProxyUrlAllowed,
onChange() {
if (isChromium && store.state.chromiumProxyActive) {
updateProxySettings();
}
},
hidePromptMarker: true,
insertMode: "both",
});
listInit(normalProxiesListElement, "normalProxies", {
getPromptPlaceholder: insertMode => {
if (insertMode == "prepend") return "Enter a proxy URL… (Primary)";
return "Enter a proxy URL… (Fallback)";
},
isAddAllowed: isNormalProxyUrlAllowed,
onChange() {
if (isChromium && store.state.chromiumProxyActive) {
updateProxySettings();
}
},
hidePromptMarker: true,
insertMode: "both",
});
}
function isOptimizedProxyUrlAllowed(url: string): AllowedResult { function isOptimizedProxyUrlAllowed(url: string): AllowedResult {
const urlLower = url.toLowerCase(); const urlLower = url.toLowerCase();
@ -323,8 +341,17 @@ function isOptimizedProxyUrlAllowed(url: string): AllowedResult {
return [false, "TTV LOL PRO v1 proxies are not compatible"]; return [false, "TTV LOL PRO v1 proxies are not compatible"];
} }
if (/^https?:\/\//i.test(url)) { if (url.includes("://")) {
return [false, "Proxy URLs must not contain a protocol (e.g. 'http://')"]; const [protocol] = url.split("://", 1);
if (!store.state.allowOtherProxyProtocols) {
return [
false,
"Proxy URLs are not allowed to contain a protocol (e.g. 'http://')",
];
} else if (!["http", "https", "socks", "socks4"].includes(protocol)) {
return [false, `'${protocol}' is not a supported protocol`];
}
url = url.substring(protocol.length + 3, url.length);
} }
if (url.includes("/")) { if (url.includes("/")) {
@ -388,6 +415,7 @@ function listInit(
storeKey: StoreStringArrayKey, storeKey: StoreStringArrayKey,
options: Partial<ListOptions> = {} options: Partial<ListOptions> = {}
) { ) {
listElement.innerHTML = ""; // Reset list element.
const listOptions: ListOptions = { ...DEFAULT_LIST_OPTIONS, ...options }; const listOptions: ListOptions = { ...DEFAULT_LIST_OPTIONS, ...options };
for (const text of store.state[storeKey]) { for (const text of store.state[storeKey]) {
_listAppend(listElement, storeKey, text, { _listAppend(listElement, storeKey, text, {
@ -447,18 +475,19 @@ function _listAppend(
return console.error(`Item '${text}' not found in '${storeKey}' array`); return console.error(`Item '${text}' not found in '${storeKey}' array`);
const textInput = e.target as HTMLInputElement; const textInput = e.target as HTMLInputElement;
const oldText = text;
const newText = textInput.value.trim(); const newText = textInput.value.trim();
// Remove item if text is empty. // Remove item if text is empty.
if (newText === "") { if (newText === "") {
store.state[storeKey].splice(itemIndex, 1); store.state[storeKey].splice(itemIndex, 1);
listItem.remove(); listItem.remove();
if (options.onEdit) options.onEdit(newText); if (options.onChange) options.onChange(oldText, newText);
return; return;
} }
// Check if text is valid. // Check if text is valid.
const [allowed, error] = options.isEditAllowed(newText); const [allowed, error] = options.isAddAllowed(newText);
if (!allowed) { if (!allowed) {
alert(error || "You cannot edit this item"); alert(error || "You cannot add this item");
textInput.value = text; textInput.value = text;
return; return;
} }
@ -467,7 +496,7 @@ function _listAppend(
textInput.placeholder = options.getItemPlaceholder(newText); textInput.placeholder = options.getItemPlaceholder(newText);
textInput.value = newText; // Update text in case it was trimmed. textInput.value = newText; // Update text in case it was trimmed.
text = newText; // Update current text variable. text = newText; // Update current text variable.
if (options.onEdit) options.onEdit(newText); if (options.onChange) options.onChange(oldText, newText);
}); });
listItem.append(textInput); listItem.append(textInput);
@ -522,7 +551,7 @@ function _listPrompt(
if (options.insertMode === "prepend") newArray.unshift(text); if (options.insertMode === "prepend") newArray.unshift(text);
else newArray.push(text); else newArray.push(text);
store.state[storeKey] = newArray; store.state[storeKey] = newArray;
if (options.onEdit) options.onEdit(text); if (options.onChange) options.onChange(undefined, text);
listItem.remove(); listItem.remove();
_listAppend(listElement, storeKey, text, options); _listAppend(listElement, storeKey, text, options);
@ -543,11 +572,13 @@ function _listPrompt(
exportButtonElement.addEventListener("click", () => { exportButtonElement.addEventListener("click", () => {
const state: Partial<State> = { const state: Partial<State> = {
adLogEnabled: store.state.adLogEnabled, adLogEnabled: store.state.adLogEnabled,
allowOtherProxyProtocols: store.state.allowOtherProxyProtocols,
anonymousMode: store.state.anonymousMode, anonymousMode: store.state.anonymousMode,
normalProxies: store.state.normalProxies, normalProxies: store.state.normalProxies,
optimizedProxies: store.state.optimizedProxies, optimizedProxies: store.state.optimizedProxies,
optimizedProxiesEnabled: store.state.optimizedProxiesEnabled, optimizedProxiesEnabled: store.state.optimizedProxiesEnabled,
passportLevel: store.state.passportLevel, passportLevel: store.state.passportLevel,
whitelistChannelSubscriptions: store.state.whitelistChannelSubscriptions,
whitelistedChannels: store.state.whitelistedChannels, whitelistedChannels: store.state.whitelistedChannels,
}; };
saveFile( saveFile(

View File

@ -138,6 +138,23 @@
Twitch tabs are whitelisted channels. Twitch tabs are whitelisted channels.
</small> </small>
<ul id="whitelisted-channels-list" class="store-list"></ul> <ul id="whitelisted-channels-list" class="store-list"></ul>
<ul class="options-list">
<li>
<input
type="checkbox"
name="whitelist-subscriptions-checkbox"
id="whitelist-subscriptions-checkbox"
/>
<label for="whitelist-subscriptions-checkbox">
Automatically whitelist channels you're subscribed to
</label>
<br />
<small>
This option will automatically add or remove channels from the
whitelist based on your subscriptions.
</small>
</li>
</ul>
</section> </section>
<!-- Proxies --> <!-- Proxies -->
@ -183,6 +200,34 @@
>List of other proxies</a >List of other proxies</a
>" discussion on TTV LOL PRO's GitHub repository. >" discussion on TTV LOL PRO's GitHub repository.
</small> </small>
<br /><br />
<ul class="options-list">
<li>
<input
type="checkbox"
name="other-protocols-checkbox"
id="other-protocols-checkbox"
/>
<label for="other-protocols-checkbox">
Allow the use of other protocols
</label>
<span class="tag">For advanced users only</span>
<br />
<small>
This option allows you to use protocols other than HTTP.
</small>
<br />
<small>
Supported protocols are HTTPS (<code>https://</code>), SOCKS4
(<code>socks4://</code>), and SOCKS5 (<code>socks://</code>).
</small>
<br />
<small>
To provide a protocol, use the format
<code>protocol://hostname:port</code>
</small>
</li>
</ul>
</section> </section>
<!-- Ad log --> <!-- Ad log -->

View File

@ -28,9 +28,11 @@ export function getFetch(pageState: PageState): typeof fetch {
// Listen for NewPlaybackAccessToken messages from the worker script. // Listen for NewPlaybackAccessToken messages from the worker script.
if (pageState.scope === "page") { if (pageState.scope === "page") {
self.addEventListener("message", async event => { self.addEventListener("message", async event => {
if (event.data?.type !== MessageType.PageScriptMessage) return; if (!event.data || event.data.type !== MessageType.PageScriptMessage) {
return;
}
const message = event.data?.message; const { message } = event.data;
if (!message) return; if (!message) return;
switch (message.type) { switch (message.type) {
@ -42,14 +44,10 @@ export function getFetch(pageState: PageState): typeof fetch {
cachedPlaybackTokenRequestHeaders, cachedPlaybackTokenRequestHeaders,
cachedPlaybackTokenRequestBody cachedPlaybackTokenRequestBody
); );
const message = { pageState.sendMessageToWorkerScripts(pageState.twitchWorkers, {
type: MessageType.NewPlaybackAccessTokenResponse, type: MessageType.NewPlaybackAccessTokenResponse,
newPlaybackAccessToken, newPlaybackAccessToken,
}; });
pageState.sendMessageToWorkerScripts(
pageState.twitchWorkers,
message
);
break; break;
} }
}); });
@ -58,13 +56,14 @@ export function getFetch(pageState: PageState): typeof fetch {
// Listen for ClearStats messages from the page script. // Listen for ClearStats messages from the page script.
self.addEventListener("message", event => { self.addEventListener("message", event => {
if ( if (
event.data?.type !== MessageType.PageScriptMessage && !event.data ||
event.data?.type !== MessageType.WorkerScriptMessage (event.data.type !== MessageType.PageScriptMessage &&
event.data.type !== MessageType.WorkerScriptMessage)
) { ) {
return; return;
} }
const message = event.data?.message; const { message } = event.data;
if (!message) return; if (!message) return;
switch (message.type) { switch (message.type) {
@ -72,9 +71,13 @@ export function getFetch(pageState: PageState): typeof fetch {
console.log("[TTV LOL PRO] Cleared stats (getFetch)."); console.log("[TTV LOL PRO] Cleared stats (getFetch).");
if (!message.channelName) break; if (!message.channelName) break;
const channelNameLower = message.channelName.toLowerCase(); const channelNameLower = message.channelName.toLowerCase();
usherManifests = usherManifests.filter( for (let i = 0; i < usherManifests.length; i++) {
manifest => manifest.channelName !== channelNameLower if (
); usherManifests[i].channelName?.toLowerCase() === channelNameLower
) {
usherManifests[i].deleted = true;
}
}
if (cachedPlaybackTokenRequestBody?.includes(channelNameLower)) { if (cachedPlaybackTokenRequestBody?.includes(channelNameLower)) {
cachedPlaybackTokenRequestHeaders = null; cachedPlaybackTokenRequestHeaders = null;
cachedPlaybackTokenRequestBody = null; cachedPlaybackTokenRequestBody = null;
@ -196,7 +199,7 @@ export function getFetch(pageState: PageState): typeof fetch {
pageState.state?.anonymousMode === true || pageState.state?.anonymousMode === true ||
(shouldFlagRequest && willFailIntegrityCheckIfProxied); (shouldFlagRequest && willFailIntegrityCheckIfProxied);
if (shouldOverrideRequest) { if (shouldOverrideRequest) {
const newRequest = await getDefaultPlaybackAccessTokenRequest( const newRequest = getDefaultPlaybackAccessTokenRequest(
channelName, channelName,
pageState.state?.anonymousMode === true pageState.state?.anonymousMode === true
); );
@ -400,6 +403,68 @@ export function getFetch(pageState: PageState): typeof fetch {
//#region Responses //#region Responses
graphqlRes: if (
host != null &&
twitchGqlHostRegex.test(host) &&
response.status < 400
) {
await waitForStore(pageState);
if (!pageState.state?.whitelistChannelSubscriptions) break graphqlRes;
responseBody ??= await readResponseBody();
// Preliminary check to avoid parsing the response body if possible.
if (
!responseBody.includes('"UserSelfConnection"') ||
!responseBody.includes('"subscriptionBenefit"') ||
!responseBody.includes('"login"')
) {
break graphqlRes;
}
try {
let channelName: string;
let isSubscribed: boolean;
const body = JSON.parse(responseBody);
if (Array.isArray(body)) {
const match = body.find(
(obj: any) =>
obj.data &&
obj.data.user &&
obj.data.user.login != null &&
obj.data.user.self &&
"subscriptionBenefit" in obj.data.user.self
);
if (match == null) break graphqlRes;
channelName = match.data.user.login;
isSubscribed = match.data.user.self.subscriptionBenefit != null;
} else {
const isMatch =
body.data &&
body.data.user &&
body.data.user.login != null &&
body.data.user.self &&
"subscriptionBenefit" in body.data.user.self;
if (!isMatch) break graphqlRes;
channelName = body.data.user.login;
isSubscribed = body.data.user.self.subscriptionBenefit != null;
}
if (!channelName) break graphqlRes;
const isLivestream = !/^\d+$/.test(channelName); // VODs have numeric IDs.
if (!isLivestream) break graphqlRes;
const wasSubscribed = wasChannelSubscriber(channelName, pageState);
const hasSubStatusChanged =
(wasSubscribed && !isSubscribed) || (!wasSubscribed && isSubscribed);
if (hasSubStatusChanged) {
pageState.sendMessageToContentScript({
type: MessageType.ChannelSubStatusChange,
channelNameLower: channelName.toLowerCase(),
wasSubscribed,
isSubscribed,
});
}
} catch (error) {
console.error("[TTV LOL PRO] Failed to parse GraphQL response:", error);
}
}
// Twitch Usher responses. // Twitch Usher responses.
usherRes: if ( usherRes: if (
host != null && host != null &&
@ -407,6 +472,7 @@ export function getFetch(pageState: PageState): typeof fetch {
response.status < 400 response.status < 400
) { ) {
//#region Usher responses. //#region Usher responses.
// No need to wait for store here because all Usher requests have already waited for it.
const isLivestream = !url.includes("/vod/"); const isLivestream = !url.includes("/vod/");
const isFrontpage = url.includes( const isFrontpage = url.includes(
encodeURIComponent('"player_type":"frontpage"') encodeURIComponent('"player_type":"frontpage"')
@ -416,6 +482,7 @@ export function getFetch(pageState: PageState): typeof fetch {
if (!isLivestream) break usherRes; if (!isLivestream) break usherRes;
responseBody ??= await readResponseBody(); responseBody ??= await readResponseBody();
usherManifests = usherManifests.filter(manifest => !manifest.deleted); // Clean up deleted manifests.
const assignedMap = parseUsherManifest(responseBody); const assignedMap = parseUsherManifest(responseBody);
if (assignedMap != null) { if (assignedMap != null) {
console.debug( console.debug(
@ -428,9 +495,12 @@ export function getFetch(pageState: PageState): typeof fetch {
replacementMap: null, replacementMap: null,
consecutiveMidrollResponses: 0, consecutiveMidrollResponses: 0,
consecutiveMidrollCooldown: 0, consecutiveMidrollCooldown: 0,
deleted: false,
}); });
} else { } else {
console.debug("[TTV LOL PRO] Received Usher response."); console.error(
"[TTV LOL PRO] Received Usher response but failed to parse it."
);
} }
// Send Video Weaver URLs to content script. // Send Video Weaver URLs to content script.
const videoWeaverUrls = [...(assignedMap?.values() ?? [])]; const videoWeaverUrls = [...(assignedMap?.values() ?? [])];
@ -476,14 +546,11 @@ export function getFetch(pageState: PageState): typeof fetch {
console.log("[TTV LOL PRO] Midroll ad detected."); console.log("[TTV LOL PRO] Midroll ad detected.");
manifest.consecutiveMidrollResponses += 1; manifest.consecutiveMidrollResponses += 1;
manifest.consecutiveMidrollCooldown = 15; manifest.consecutiveMidrollCooldown = 15;
const isWhitelisted = isChannelWhitelisted( await waitForStore(pageState);
manifest.channelName,
pageState
);
const shouldUpdateReplacementMap = const shouldUpdateReplacementMap =
pageState.state?.optimizedProxiesEnabled === true && pageState.state?.optimizedProxiesEnabled === true &&
manifest.consecutiveMidrollResponses <= 2 && // Avoid infinite loop. manifest.consecutiveMidrollResponses <= 2 && // Avoid infinite loop.
!isWhitelisted; !videoWeaverUrlsToNotProxy.has(url);
if (shouldUpdateReplacementMap) { if (shouldUpdateReplacementMap) {
const success = await updateVideoWeaverReplacementMap( const success = await updateVideoWeaverReplacementMap(
pageState, pageState,
@ -616,11 +683,25 @@ function isChannelWhitelisted(
pageState: PageState pageState: PageState
): boolean { ): boolean {
if (!channelName) return false; if (!channelName) return false;
const whitelistedChannelsLower = const channelNameLower = channelName.toLowerCase();
pageState.state?.whitelistedChannels.map(channel => return (
channel.toLowerCase() pageState.state?.whitelistedChannels.some(
) ?? []; channel => channel.toLowerCase() === channelNameLower
return whitelistedChannelsLower.includes(channelName.toLowerCase()); ) ?? false
);
}
function wasChannelSubscriber(
channelName: string | null | undefined,
pageState: PageState
): boolean {
if (!channelName) return false;
const channelNameLower = channelName.toLowerCase();
return (
pageState.state?.activeChannelSubscriptions.some(
channel => channel.toLowerCase() === channelNameLower
) ?? false
);
} }
async function flagRequest( async function flagRequest(
@ -674,10 +755,6 @@ function cancelRequest(): never {
throw new Error(); throw new Error();
} }
async function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
//#region Video Weaver URL replacement //#region Video Weaver URL replacement
/** /**
@ -686,10 +763,10 @@ async function sleep(ms: number): Promise<void> {
* @param anonymousMode * @param anonymousMode
* @returns * @returns
*/ */
async function getDefaultPlaybackAccessTokenRequest( function getDefaultPlaybackAccessTokenRequest(
channel: string | null = null, channel: string | null = null,
anonymousMode: boolean = false anonymousMode: boolean = false
): Promise<Request | null> { ): Request | null {
// We can use `location.href` because we're in the page script. // We can use `location.href` because we're in the page script.
const channelName = channel ?? findChannelFromTwitchTvUrl(location.href); const channelName = channel ?? findChannelFromTwitchTvUrl(location.href);
if (!channelName) return null; if (!channelName) return null;
@ -745,7 +822,7 @@ async function fetchReplacementPlaybackAccessToken(
): Promise<PlaybackAccessToken | null> { ): Promise<PlaybackAccessToken | null> {
// Not using the cached request because we'd need to check if integrity requests are proxied. // Not using the cached request because we'd need to check if integrity requests are proxied.
try { try {
let request = await getDefaultPlaybackAccessTokenRequest( let request = getDefaultPlaybackAccessTokenRequest(
null, null,
pageState.state?.anonymousMode === true pageState.state?.anonymousMode === true
); );

View File

@ -39,6 +39,7 @@ const pageState: PageState = {
sendMessageToWorkerScriptsAndWaitForResponse, sendMessageToWorkerScriptsAndWaitForResponse,
}; };
const NATIVE_FETCH = window.fetch;
window.fetch = getFetch(pageState); window.fetch = getFetch(pageState);
const NATIVE_WORKER = window.Worker; const NATIVE_WORKER = window.Worker;
@ -130,9 +131,9 @@ window.addEventListener("message", event => {
return; return;
} }
if (event.data?.type !== MessageType.PageScriptMessage) return; if (!event.data || event.data.type !== MessageType.PageScriptMessage) return;
const message = event.data?.message; const { message } = event.data;
if (!message) return; if (!message) return;
switch (message.type) { switch (message.type) {

View File

@ -13,7 +13,9 @@ function sendMessage(
type: MessageType, type: MessageType,
message: any message: any
): void { ): void {
if (!recipient) return; if (!recipient) {
return console.error("[TTV LOL PRO] Message recipient is undefined.");
}
recipient.postMessage({ recipient.postMessage({
type, type,
message, message,
@ -30,14 +32,12 @@ async function sendMessageAndWaitForResponse(
): Promise<any> { ): Promise<any> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (!recipient) { if (!recipient) {
console.warn("[TTV LOL PRO] Recipient is undefined."); return reject(new Error("Message recipient is undefined."));
resolve(undefined);
return;
} }
const listener = (event: MessageEvent) => { const listener = (event: MessageEvent) => {
if (event.data?.type !== responseType) return; if (!event.data || event.data.type !== responseType) return;
const message = event.data?.message; const { message } = event.data;
if (!message) return; if (!message) return;
if (message.type === responseMessageType) { if (message.type === responseMessageType) {
self.removeEventListener("message", listener); self.removeEventListener("message", listener);
@ -46,7 +46,12 @@ async function sendMessageAndWaitForResponse(
}; };
self.addEventListener("message", listener); self.addEventListener("message", listener);
sendMessage(recipient, type, message); recipient.postMessage({
type,
message,
responseType,
responseMessageType,
});
setTimeout(() => { setTimeout(() => {
self.removeEventListener("message", listener); self.removeEventListener("message", listener);
reject(new Error("Timed out waiting for message response.")); reject(new Error("Timed out waiting for message response."));

View File

@ -36,6 +36,7 @@ export interface UsherManifest {
replacementMap: Map<string, string> | null; // Same as above, but with new URLs. replacementMap: Map<string, string> | null; // Same as above, but with new URLs.
consecutiveMidrollResponses: number; // Used to avoid infinite loops. consecutiveMidrollResponses: number; // Used to avoid infinite loops.
consecutiveMidrollCooldown: number; // Used to avoid infinite loops. consecutiveMidrollCooldown: number; // Used to avoid infinite loops.
deleted: boolean; // Deletion flag for cleanup.
} }
export interface PlaybackAccessToken { export interface PlaybackAccessToken {

View File

@ -47,9 +47,11 @@ const pageState: PageState = {
self.fetch = getFetch(pageState); self.fetch = getFetch(pageState);
self.addEventListener("message", event => { self.addEventListener("message", event => {
if (event.data?.type !== MessageType.WorkerScriptMessage) return; if (!event.data || event.data.type !== MessageType.WorkerScriptMessage) {
return;
}
const message = event.data?.message; const { message } = event.data;
if (!message) return; if (!message) return;
switch (message.type) { switch (message.type) {

View File

@ -3,9 +3,11 @@ import type { State } from "./types";
export default function getDefaultState() { export default function getDefaultState() {
const state: State = { const state: State = {
activeChannelSubscriptions: [],
adLog: [], adLog: [],
adLogEnabled: true, adLogEnabled: true,
adLogLastSent: 0, adLogLastSent: 0,
allowOtherProxyProtocols: false,
anonymousMode: true, anonymousMode: true,
chromiumProxyActive: false, chromiumProxyActive: false,
dnsResponses: [], dnsResponses: [],
@ -18,6 +20,7 @@ export default function getDefaultState() {
passportLevel: 0, passportLevel: 0,
streamStatuses: {}, streamStatuses: {},
videoWeaverUrlsByChannel: {}, videoWeaverUrlsByChannel: {},
whitelistChannelSubscriptions: true,
whitelistedChannels: [], whitelistedChannels: [],
}; };
return state; return state;

View File

@ -6,9 +6,11 @@ export type ReadyState = "loading" | "complete";
export type StorageAreaName = "local" | "managed" | "sync"; export type StorageAreaName = "local" | "managed" | "sync";
export interface State { export interface State {
activeChannelSubscriptions: string[];
adLog: AdLogEntry[]; adLog: AdLogEntry[];
adLogEnabled: boolean; adLogEnabled: boolean;
adLogLastSent: number; adLogLastSent: number;
allowOtherProxyProtocols: boolean;
anonymousMode: boolean; anonymousMode: boolean;
chromiumProxyActive: boolean; chromiumProxyActive: boolean;
dnsResponses: DnsResponse[]; dnsResponses: DnsResponse[];
@ -19,6 +21,7 @@ export interface State {
passportLevel: number; passportLevel: number;
streamStatuses: Record<string, StreamStatus>; streamStatuses: Record<string, StreamStatus>;
videoWeaverUrlsByChannel: Record<string, string[]>; videoWeaverUrlsByChannel: Record<string, string[]>;
whitelistChannelSubscriptions: boolean;
whitelistedChannels: string[]; whitelistedChannels: string[];
} }

View File

@ -3,9 +3,11 @@ export type KeyOfType<T, V> = keyof {
[P in keyof T as T[P] extends V ? P : never]: any; [P in keyof T as T[P] extends V ? P : never]: any;
}; };
export type ProxyType = "direct" | "http" | "https" | "socks" | "socks4";
// From https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/proxy/ProxyInfo // From https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/proxy/ProxyInfo
export interface ProxyInfo { export interface ProxyInfo {
type: "direct" | "http" | "https" | "socks" | "socks4"; type: ProxyType;
host?: string; host?: string;
port?: number; port?: number;
username?: string; username?: string;
@ -82,6 +84,7 @@ export const enum MessageType {
UsherResponse = "TLP_UsherResponse", UsherResponse = "TLP_UsherResponse",
NewPlaybackAccessToken = "TLP_NewPlaybackAccessToken", NewPlaybackAccessToken = "TLP_NewPlaybackAccessToken",
NewPlaybackAccessTokenResponse = "TLP_NewPlaybackAccessTokenResponse", NewPlaybackAccessTokenResponse = "TLP_NewPlaybackAccessTokenResponse",
ChannelSubStatusChange = "TLP_ChannelSubStatusChange",
MultipleAdBlockersInUse = "TLP_MultipleAdBlockersInUse", MultipleAdBlockersInUse = "TLP_MultipleAdBlockersInUse",
ClearStats = "TLP_ClearStats", ClearStats = "TLP_ClearStats",
} }