Merge pull request #361 from younesaassila/feat/auto-whitelist-subs

Feat/auto whitelist subs
This commit is contained in:
Younes Aassila 2025-02-01 15:14:50 +01:00 committed by GitHub
commit 66e0ccb309
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 230 additions and 79 deletions

View File

@ -4,8 +4,7 @@ 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( return store.state.whitelistedChannels.some(
channel => channel.toLowerCase() c => c.toLowerCase() === channelName.toLowerCase()
); );
return whitelistedChannelsLower.includes(channelName.toLowerCase());
} }

View File

@ -0,0 +1,10 @@
import store from "../../store";
export default function wasChannelSubscriber(
channelName: string | null
): boolean {
if (!channelName) return false;
return store.state.activeChannelSubscriptions.some(
c => c.toLowerCase() === channelName.toLowerCase()
);
}

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";
@ -97,66 +98,116 @@ 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, responseType, responseMessageType } = 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 { channelName, wasSubscribed, isSubscribed } = message;
const isWhitelisted = isChannelWhitelisted(channelName);
console.log("[TTV LOL PRO] ChannelSubStatusChange", {
channelName,
wasSubscribed,
isSubscribed,
isWhitelisted,
});
if (store.state.whitelistChannelSubscriptions && channelName != null) {
if (!wasSubscribed && isSubscribed) {
store.state.activeChannelSubscriptions.push(channelName);
// Add to whitelist.
if (!isWhitelisted) {
console.log(`[TTV LOL PRO] Adding '${channelName}' to whitelist.`);
store.state.whitelistedChannels.push(channelName);
}
} else if (wasSubscribed && !isSubscribed) {
store.state.activeChannelSubscriptions =
store.state.activeChannelSubscriptions.filter(
c => c.toLowerCase() !== channelName.toLowerCase()
);
// Remove from whitelist.
if (isWhitelisted) {
console.log(
`[TTV LOL PRO] Removing '${channelName}' from whitelist.`
);
store.state.whitelistedChannels =
store.state.whitelistedChannels.filter(
c => c.toLowerCase() !== channelName.toLowerCase()
);
}
}
}
window.postMessage({
type: responseType,
message: {
type: responseMessageType,
whitelistedChannels: JSON.parse(
JSON.stringify(store.state.whitelistedChannels)
),
},
});
}
// ---
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);
} }
} }

View File

@ -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 = $(
@ -163,6 +166,16 @@ function main() {
return [true]; return [true];
}, },
}); });
whitelistSubscriptionsCheckboxElement.checked =
store.state.whitelistChannelSubscriptions;
whitelistSubscriptionsCheckboxElement.addEventListener("change", () => {
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;
@ -548,6 +561,7 @@ exportButtonElement.addEventListener("click", () => {
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 -->

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) {
@ -58,13 +60,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) {
@ -265,6 +268,36 @@ export function getFetch(pageState: PageState): typeof fetch {
encodeURIComponent('"player_type":"frontpage"') encodeURIComponent('"player_type":"frontpage"')
); );
const channelName = findChannelFromUsherUrl(url); const channelName = findChannelFromUsherUrl(url);
if (
pageState.state?.whitelistChannelSubscriptions &&
channelName != null
) {
const wasSubscribed = wasChannelSubscriber(channelName, pageState);
const isSubscribed = url.includes(
encodeURIComponent('"subscriber":true')
);
const hasSubStatusChanged =
(wasSubscribed && !isSubscribed) || (!wasSubscribed && isSubscribed);
if (hasSubStatusChanged) {
try {
const response =
await pageState.sendMessageToContentScriptAndWaitForResponse(
pageState.scope,
{
type: MessageType.ChannelSubStatusChange,
channelName,
wasSubscribed,
isSubscribed,
},
MessageType.ChannelSubStatusChangeResponse
);
if (typeof response.whitelistedChannels === "object") {
pageState.state.whitelistedChannels =
response.whitelistedChannels;
}
} catch {}
}
}
const isWhitelisted = isChannelWhitelisted(channelName, pageState); const isWhitelisted = isChannelWhitelisted(channelName, pageState);
if (!isLivestream || isFrontpage || isWhitelisted) { if (!isLivestream || isFrontpage || isWhitelisted) {
console.log( console.log(
@ -616,11 +649,23 @@ function isChannelWhitelisted(
pageState: PageState pageState: PageState
): boolean { ): boolean {
if (!channelName) return false; if (!channelName) return false;
const whitelistedChannelsLower = return (
pageState.state?.whitelistedChannels.map(channel => pageState.state?.whitelistedChannels.some(
channel.toLowerCase() c => c.toLowerCase() === channelName.toLowerCase()
) ?? []; ) ?? false
return whitelistedChannelsLower.includes(channelName.toLowerCase()); );
}
function wasChannelSubscriber(
channelName: string | null | undefined,
pageState: PageState
): boolean {
if (!channelName) return false;
return (
pageState.state?.activeChannelSubscriptions.some(
c => c.toLowerCase() === channelName.toLowerCase()
) ?? false
);
} }
async function flagRequest( async function flagRequest(

View File

@ -130,9 +130,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,14 @@ 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."); console.error("[TTV LOL PRO] Message recipient is undefined.");
resolve(undefined); resolve(undefined);
return; 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 +48,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

@ -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,6 +3,7 @@ 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,
@ -18,6 +19,7 @@ export default function getDefaultState() {
passportLevel: 0, passportLevel: 0,
streamStatuses: {}, streamStatuses: {},
videoWeaverUrlsByChannel: {}, videoWeaverUrlsByChannel: {},
whitelistChannelSubscriptions: true,
whitelistedChannels: [], whitelistedChannels: [],
}; };
return state; return state;

View File

@ -6,6 +6,7 @@ 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;
@ -19,6 +20,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

@ -79,6 +79,8 @@ export const enum MessageType {
EnableFullMode = "TLP_EnableFullMode", EnableFullMode = "TLP_EnableFullMode",
EnableFullModeResponse = "TLP_EnableFullModeResponse", EnableFullModeResponse = "TLP_EnableFullModeResponse",
DisableFullMode = "TLP_DisableFullMode", DisableFullMode = "TLP_DisableFullMode",
ChannelSubStatusChange = "TLP_ChannelSubStatusChange",
ChannelSubStatusChangeResponse = "TLP_ChannelSubStatusChangeResponse",
UsherResponse = "TLP_UsherResponse", UsherResponse = "TLP_UsherResponse",
NewPlaybackAccessToken = "TLP_NewPlaybackAccessToken", NewPlaybackAccessToken = "TLP_NewPlaybackAccessToken",
NewPlaybackAccessTokenResponse = "TLP_NewPlaybackAccessTokenResponse", NewPlaybackAccessTokenResponse = "TLP_NewPlaybackAccessTokenResponse",