import { EventEmitter } from 'events'; import React 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('.'); const second_last = parts.at(-2); if (!second_last) { throw new Error('url too short?'); } let lookback = !['co', 'com'].includes(second_last) ? -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 ): [ Record, React.Dispatch>> ] { const [eventCounts, setEventCounts] = React.useState>({ '*': 0, }); React.useEffect(() => { const callback = (eventSubtype: string) => { setEventCounts((eventCounts) => ({ ...eventCounts, ...{ [eventSubtype]: (eventCounts[eventSubtype] || 0) + 1 }, ...{ '*': (eventCounts['*'] === undefined ? 0 : eventCounts['*']) + 1 }, })); }; e.on('change', callback); return () => { e.removeListener('change', callback); }; }, []); return [eventCounts, setEventCounts]; } 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); } else { return result; } 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, reject) => { const FR = new FileReader(); FR.addEventListener('load', (e) => { const target = e.target; if (!target) { return reject('File missing?'); } 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: { toString: () => string }) => string | Record = (id) => id.toString(), key = '', ret = [] as [string, string][], 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 (obj === null) { ret.push([key, '']); } else if (typeof obj === 'object') { for (const [subkey, value] of Object.entries(obj)) { flattenObject(value, parser, prefix + subkey, ret); } } else if (!parsed) { try { flattenObject(parser(obj as { toString: () => string }), parser, key, ret, true); } catch (e) { //emergency case, mostly for just type safety ret.push([key, JSON.stringify(obj)]); } } else if (typeof obj === 'string') { ret.push([key, obj]); } else { throw new Error('Something went wrong when parsing ' + obj); } return ret; } export function flattenObjectEntries( entries: [string, unknown][], parser: (to_parse: { toString: () => string }) => 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; } } export function normalizeForClassname(string: string) { return string.replace(/[^a-z0-9]/gi, '-'); } export function wordlist(words: string[]) { return words.reduce( (acc, word, i) => `${acc}${i > 0 ? (i < words.length - 1 ? ',' : ' i') : ''} ${word}`, '' ); }