import { EventEmitter } from "events"; import { Dispatch, ReactChildren, SetStateAction, useEffect, useState, } from "react"; export type Unpromisify = T extends Promise ? R : T; export type Unarray = T extends Array ? R : T; export type Tab = Unarray>>; export type Request = { cookieStoreId?: string; documentUrl?: string; // RL of the document in which the resource will be loaded. For example, if the web page at "https://example.com" contains an image or an iframe, then the documentUrl for the image or iframe will be "https://example.com". For a top-level document, documentUrl is undefined. frameId: number; incognito?: boolean; method: string; originUrl: string; parentFrameId: number; proxyInfo?: { host: string; port: number; type: string; username: string; proxyDNS: boolean; failoverTimeout: number; }; requestHeaders?: { name: string; value?: string; binaryValue?: number[] }[]; requestId: string; tabId: number; thirdParty?: boolean; timeStamp: number; type: string; url: string; // the target of the request; urlClassification?: { firstParty: string[]; thirdParty: string[] }; }; export function getshorthost(host: string) { const parts = host .replace(/^.*:\/\//, "") .replace(/\/.*$/, "") .split("."); let lookback = parts.at(-2).length > 3 ? -2 : -3; if (parts.at(-2) == "doubleclick" || parts.at(-2) == "google") { lookback = -4; // to distinguish between google ads and stats } else if (parts.at(-2) == "google") { lookback = -3; // to distinguish various google services } return parts.slice(lookback).join("."); } export function useEmitter( e: EventEmitter ): [number, Dispatch>] { const [counter, setCounter] = useState(0); useEffect(() => { const callback = () => { setCounter((counter) => counter + 1); }; e.on("change", callback); return () => { e.removeListener("change", callback); }; }, []); return [counter, setCounter]; } export function parseCookie(cookie: string): Record { return cookie .split(";") .map((l) => l.split("=")) .reduce( (acc, [key, value]) => ({ ...acc, [key]: value, }), {} ); } export async function getTabByID(id: number) { const tabs = await browser.tabs.query({ currentWindow: true }); return tabs.find((tab) => tab.id == id); } export function parseToObject(str: unknown): Record { let result: Record; let original_string: string; if (typeof str === "string") { original_string = str; result = JSON.parse(str); } else if (typeof str == "object") { result = str as Record; original_string = (result[Symbol.for("originalString")] as string) || JSON.stringify(str); } result[Symbol.for("originalString")] = original_string; return result; } export function isJSONObject( str: unknown ): str is Record | string | number { try { const firstChar = JSON.stringify(parseToObject(str))[0]; return ["{", "["].includes(firstChar); } catch (e) { return false; } } export function isURL(str: unknown): str is string { try { return !!(typeof str === "string" && new URL(str)); } catch (e) { return false; } } export function hyphenate(str: string): string { return str.replace(/[_\[A-Z]/g, `${String.fromCharCode(173)}$&`); } export function unique(array: T[]): Array { return Array.from(new Set(array)); } export function allSubhosts(host: string) { const parts = host.split("."); const result = []; for (let i = 0; i < parts.length - 2; i++) { result.push(parts.slice(i).join(".")); } return result; } export function reduceConcat(a: T[], b: T[]): T[] { return a.concat(b); } export function getDate() { const d = new Date(); return `${d.getFullYear()}-${(d.getMonth() + 1) .toString() .padStart(2, "0")}-${d.getDate().toString().padStart(2, "0")}`; } export function toBase64(file: File): Promise { return new Promise((resolve) => { const FR = new FileReader(); FR.addEventListener("load", (e) => { resolve(e.target.result as string); }); FR.readAsDataURL(file); }); } export function makeThrottle(interval: number) { let last_emit = 0; function emit(callback: () => void) { if (Date.now() - last_emit > interval) { callback(); last_emit = Date.now(); return true; } else { return false; } } return function (callback: () => void) { if (!emit(callback)) { setTimeout(() => emit(callback), interval); } }; } export function isSameURL(url1: string, url2: string): boolean { if (url1 === url2) { return true; } url1 = url1.replace(/^https?:\/\//, "").replace(/\/$/, ""); url2 = url2.replace(/^https?:\/\//, "").replace(/\/$/, ""); return url1 === url2; } export function isBase64(s: string): boolean { try { atob(s); return true; } catch (e) {} return false; } export function isBase64JSON(s: unknown): s is string { return typeof s === "string" && isBase64(s) && isJSONObject(atob(s)); } export function flattenObject( obj: unknown, parser: (to_parse: unknown) => string | Record = (id) => id.toString(), key = "", ret = [], parsed = false ): [string, string][] { const prefix = key === "" ? "" : `${key}.`; if (Array.isArray(obj)) { if (obj.length == 1) { flattenObject(obj[0], parser, key, ret); } else { for (let i in obj) { flattenObject(obj[i], parser, prefix + i, ret); } } } else if (typeof obj === "object") { for (const [subkey, value] of Object.entries(obj)) { flattenObject(value, parser, prefix + subkey, ret); } } else if (!parsed) { flattenObject(parser(obj), parser, key, ret, true); } else { ret.push([key, obj]); } return ret; } export function flattenObjectEntries( entries: [string, unknown][], parser: (to_parse: unknown) => string | Record = (id) => id.toString() ): [string, string][] { return flattenObject(Object.fromEntries(entries), parser); } export function maskString( str: string, max_fraction_remaining: number, max_chars_total: number ): string { const amount_of_chars_to_cut = str.length - Math.min(str.length * max_fraction_remaining, max_chars_total); if (amount_of_chars_to_cut == 0) { return str; } return ( str.slice(0, str.length / 2 - amount_of_chars_to_cut / 2) + "(...)" + str.slice(str.length / 2 + amount_of_chars_to_cut / 2) ); } export function safeDecodeURIComponent(s: string) { try { return decodeURIComponent(s); } catch (e) { return s; } }