From 1b20e41c8f1d6ef3b65c697ac8909ae323f0b821 Mon Sep 17 00:00:00 2001 From: emma Date: Mon, 25 Aug 2025 16:34:47 -0400 Subject: [PATCH 1/9] fix most behaviour types --- package.json | 1 + src/autoclick.ts | 11 ++-- src/autofetcher.ts | 94 ++++++++++++++++------------- src/autoplay.ts | 26 +++++---- src/index.ts | 2 +- src/lib/behavior.ts | 65 +++++++++++++-------- src/lib/utils.ts | 127 +++++++++++++++++++++++++--------------- src/site/facebook.ts | 133 +++++++++++++++++++++++++++++------------- src/site/instagram.ts | 80 ++++++++++++++++--------- src/site/telegram.ts | 17 ++++-- src/site/tiktok.ts | 49 ++++++++++++---- src/site/twitter.ts | 101 ++++++++++++++++++++++---------- src/site/youtube.ts | 5 +- tsconfig.json | 1 + yarn.lock | 5 ++ 15 files changed, 470 insertions(+), 247 deletions(-) diff --git a/package.json b/package.json index 7be88d4..2513ca6 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "author": "Webrecorder Software", "license": "AGPL-3.0-or-later", "devDependencies": { + "@types/query-selector-shadow-dom": "^1.0.4", "@typescript-eslint/eslint-plugin": "^8.28.0", "@typescript-eslint/parser": "^8.28.0", "@webpack-cli/init": "^1.1.3", diff --git a/src/autoclick.ts b/src/autoclick.ts index b2bc23c..30829d9 100644 --- a/src/autoclick.ts +++ b/src/autoclick.ts @@ -3,7 +3,7 @@ import { addToExternalSet, sleep } from "./lib/utils"; export class AutoClick extends BackgroundBehavior { _donePromise: Promise; - _markDone: () => void; + _markDone!: () => void; selector: string; seenElem = new WeakSet(); @@ -40,7 +40,7 @@ export class AutoClick extends BackgroundBehavior { return elem; } } catch (e) { - this.debug(e.toString()); + this.debug((e as Error).toString()); } return null; @@ -49,7 +49,7 @@ export class AutoClick extends BackgroundBehavior { async start() { const origHref = self.location.href; - const beforeUnload = (event) => { + const beforeUnload = (event: BeforeUnloadEvent) => { event.preventDefault(); return false; }; @@ -59,7 +59,7 @@ export class AutoClick extends BackgroundBehavior { // process external links on current origin - // eslint-disable-next-line no-constant-condition + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, no-constant-condition while (true) { const elem = this.nextSameOriginLink(); @@ -109,11 +109,12 @@ export class AutoClick extends BackgroundBehavior { }); } } + // @ts-expect-error TODO: this looks like a typo, there's no associated `try` block for this `catch` block, so it ends up being a method catch(e) { this.debug(e.toString()); } - done() { + async done() { return this._donePromise; } } diff --git a/src/autofetcher.ts b/src/autofetcher.ts index 080ca91..8a6e3e1 100644 --- a/src/autofetcher.ts +++ b/src/autofetcher.ts @@ -25,21 +25,25 @@ const MAX_CONCURRENT = 6; // =========================================================================== export class AutoFetcher extends BackgroundBehavior { - urlSet: Set = new Set(); + urlSet = new Set(); pendingQueue: string[] = []; waitQueue: string[] = []; - mutationObserver: MutationObserver; - numPending: number = 0; - numDone: number = 0; - headers: object; + mutationObserver?: MutationObserver; + numPending = 0; + numDone = 0; + headers: Record; _donePromise: Promise; - _markDone: (value: any) => void; + _markDone!: (value: PromiseLike | null) => void; active: boolean; running = false; static id = "Autofetcher"; - constructor(active = false, headers = null, startEarly = false) { + constructor( + active = false, + headers: Record | null = null, + startEarly = false, + ) { super(); this.headers = headers || {}; @@ -63,16 +67,16 @@ export class AutoFetcher extends BackgroundBehavior { this.initObserver(); - this.run(); + void this.run(); - sleep(500).then(() => { + void sleep(500).then(() => { if (!this.pendingQueue.length && !this.numPending) { this._markDone(null); } }); } - done() { + async done() { return this._donePromise; } @@ -80,7 +84,7 @@ export class AutoFetcher extends BackgroundBehavior { this.running = true; for (const url of this.waitQueue) { - this.doFetch(url); + void this.doFetch(url); } this.waitQueue = []; @@ -93,10 +97,10 @@ export class AutoFetcher extends BackgroundBehavior { return url && (url.startsWith("http:") || url.startsWith("https:")); } - queueUrl(url: string, immediate: boolean = false) { + queueUrl(url: string, immediate = false) { try { url = new URL(url, document.baseURI).href; - } catch (e) { + } catch (_) { return false; } @@ -111,7 +115,7 @@ export class AutoFetcher extends BackgroundBehavior { this.urlSet.add(url); if (this.running || immediate) { - this.doFetch(url); + void this.doFetch(url); } else { this.waitQueue.push(url); } @@ -128,10 +132,13 @@ export class AutoFetcher extends BackgroundBehavior { }); this.debug(`Autofetch: started ${url}`); - const reader = resp.body.getReader(); + const reader = resp.body!.getReader(); let res = null; - while ((res = await reader.read()) && !res.done); + + do { + res = await reader.read(); + } while (!res.done); this.debug(`Autofetch: finished ${url}`); @@ -156,7 +163,7 @@ export class AutoFetcher extends BackgroundBehavior { } as {}); abort.abort(); this.debug(`Autofetch: started non-cors stream for ${url}`); - } catch (e) { + } catch (_) { this.debug(`Autofetch: failed non-cors for ${url}`); } } @@ -165,11 +172,11 @@ export class AutoFetcher extends BackgroundBehavior { this.pendingQueue.push(url); if (this.numPending <= MAX_CONCURRENT) { while (this.pendingQueue.length > 0) { - const url = this.pendingQueue.shift(); + const url = this.pendingQueue.shift()!; this.numPending++; - let success = await doExternalFetch(url); + const success = await doExternalFetch(url); if (!success) { await this.doFetchNonCors(url); @@ -203,38 +210,41 @@ export class AutoFetcher extends BackgroundBehavior { }); } - processChangedNode(target) { + processChangedNode(target: Node) { switch (target.nodeType) { case Node.ATTRIBUTE_NODE: if (target.nodeName === "srcset") { - this.extractSrcSetAttr(target.nodeValue); + this.extractSrcSetAttr(target.nodeValue!); } if (target.nodeName === "loading" && target.nodeValue === "lazy") { - const elem = target.parentNode; - if (elem.tagName === "IMG") { + const elem = target.parentNode as Element | null; + if (elem?.tagName === "IMG") { elem.setAttribute("loading", "eager"); } } break; case Node.TEXT_NODE: - if (target.parentNode && target.parentNode.tagName === "STYLE") { - this.extractStyleText(target.nodeValue); + if ( + target.parentNode && + (target.parentNode as Element).tagName === "STYLE" + ) { + this.extractStyleText(target.nodeValue!); } break; case Node.ELEMENT_NODE: - if (target.sheet) { - this.extractStyleSheet(target.sheet); + if ("sheet" in target) { + this.extractStyleSheet((target as HTMLStyleElement).sheet!); } - this.extractSrcSrcSet(target); - setTimeout(() => this.extractSrcSrcSetAll(target), 1000); + this.extractSrcSrcSet(target as HTMLElement); + setTimeout(() => this.extractSrcSrcSetAll(target as HTMLElement), 1000); setTimeout(() => this.extractDataAttributes(target), 1000); break; } } - observeChange(changes) { + observeChange(changes: MutationRecord[]) { for (const change of changes) { this.processChangedNode(change.target); @@ -246,7 +256,7 @@ export class AutoFetcher extends BackgroundBehavior { } } - extractSrcSrcSetAll(root) { + extractSrcSrcSetAll(root: Document | HTMLElement) { const elems = querySelectorAllDeep(SRC_SET_SELECTOR, root); for (const elem of elems) { @@ -254,7 +264,7 @@ export class AutoFetcher extends BackgroundBehavior { } } - extractSrcSrcSet(elem) { + extractSrcSrcSet(elem: HTMLElement | null) { if (!elem || elem.nodeType !== Node.ELEMENT_NODE) { console.warn("No elem to extract from"); return; @@ -288,13 +298,13 @@ export class AutoFetcher extends BackgroundBehavior { if ( src && - (srcset || data_srcset || elem.parentElement.tagName === "NOSCRIPT") + (srcset || data_srcset || elem.parentElement?.tagName === "NOSCRIPT") ) { this.queueUrl(src); } } - extractSrcSetAttr(srcset) { + extractSrcSetAttr(srcset: string) { for (const v of srcset.split(SRCSET_REGEX)) { if (v) { const parts = v.trim().split(" "); @@ -303,7 +313,7 @@ export class AutoFetcher extends BackgroundBehavior { } } - extractStyleSheets(root?) { + extractStyleSheets(root?: Document | null) { root = root || document; for (const sheet of root.styleSheets) { @@ -311,12 +321,13 @@ export class AutoFetcher extends BackgroundBehavior { } } - extractStyleSheet(sheet) { + extractStyleSheet(sheet: CSSStyleSheet) { let rules; try { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition rules = sheet.cssRules || sheet.rules; - } catch (e) { + } catch (_) { this.debug("Can't access stylesheet"); return; } @@ -328,8 +339,8 @@ export class AutoFetcher extends BackgroundBehavior { } } - extractStyleText(text) { - const urlExtractor = (m, n1, n2, n3) => { + extractStyleText(text: string) { + const urlExtractor = (_m: unknown, n1: string, n2: string, n3: string) => { this.queueUrl(n2); return n1 + n2 + n3; }; @@ -337,13 +348,14 @@ export class AutoFetcher extends BackgroundBehavior { text.replace(STYLE_REGEX, urlExtractor).replace(IMPORT_REGEX, urlExtractor); } - extractDataAttributes(root) { + extractDataAttributes(root: Node | null) { const QUERY = "//@*[starts-with(name(), 'data-') and " + "(starts-with(., 'http') or starts-with(., '/') or starts-with(., './') or starts-with(., '../'))]"; for (const attr of xpathNodes(QUERY, root)) { - this.queueUrl(attr.value); + // @ts-expect-error TODO not sure what type `attr` should have here + this.queueUrl(attr.value as string); } } } diff --git a/src/autoplay.ts b/src/autoplay.ts index fd63351..58acb2f 100644 --- a/src/autoplay.ts +++ b/src/autoplay.ts @@ -25,7 +25,7 @@ export class Autoplay extends BackgroundBehavior { this._initDone = () => null; this.promises.push(new Promise((resolve) => (this._initDone = resolve))); if (startEarly) { - document.addEventListener("DOMContentLoaded", () => + document.addEventListener("DOMContentLoaded", async () => this.pollAudioVideo(), ); } @@ -35,7 +35,7 @@ export class Autoplay extends BackgroundBehavior { this.running = true; //this.initObserver(); - this.pollAudioVideo(); + void this.pollAudioVideo(); this._initDone(); } @@ -49,19 +49,20 @@ export class Autoplay extends BackgroundBehavior { this.polling = true; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition while (run) { for (const [, elem] of querySelectorAllDeep( "video, audio, picture", ).entries()) { if (!elem["__bx_autoplay_found"]) { if (!this.running) { - if (this.processFetchableUrl(elem)) { + if (this.processFetchableUrl(elem as HTMLMediaElement)) { elem["__bx_autoplay_found"] = true; } continue; } - await this.loadMedia(elem); + await this.loadMedia(elem as HTMLMediaElement); elem["__bx_autoplay_found"] = true; } } @@ -72,8 +73,8 @@ export class Autoplay extends BackgroundBehavior { this.polling = false; } - fetchSrcUrl(source) { - const url: string = source.src || source.currentSrc; + fetchSrcUrl(source: HTMLMediaElement | HTMLSourceElement) { + const url: string = source.src || (source as HTMLMediaElement).currentSrc; if (!url) { return false; @@ -94,7 +95,7 @@ export class Autoplay extends BackgroundBehavior { return true; } - processFetchableUrl(media) { + processFetchableUrl(media: HTMLMediaElement) { let found = this.fetchSrcUrl(media); const sources = media.querySelectorAll("source"); @@ -107,11 +108,12 @@ export class Autoplay extends BackgroundBehavior { return found; } - async loadMedia(media) { + async loadMedia(media: HTMLMediaElement) { this.debug("processing media element: " + media.outerHTML); const found = this.processFetchableUrl(media); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!media.play) { this.debug("media not playable, skipping"); return; @@ -138,12 +140,12 @@ export class Autoplay extends BackgroundBehavior { ); } - this.attemptMediaPlay(media).then( - async (finished: Promise | null) => { + void this.attemptMediaPlay(media).then( + async (finished: Promise | undefined) => { let check = true; if (finished) { - finished.then(() => (check = false)); + void finished.then(() => (check = false)); } while (check) { @@ -249,7 +251,7 @@ export class Autoplay extends BackgroundBehavior { return finished; } - done() { + async done() { return Promise.allSettled(this.promises); } } diff --git a/src/index.ts b/src/index.ts index 8fa2f3a..379922e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -58,7 +58,7 @@ export class BehaviorManager { autofetch: AutoFetcher; behaviors: any[]; loadedBehaviors: any; - mainBehavior: Behavior | BehaviorRunner | null; + mainBehavior: Behavior | BehaviorRunner | null; mainBehaviorClass: any; inited: boolean; started: boolean; diff --git a/src/lib/behavior.ts b/src/lib/behavior.ts index 41b1dba..92cca0c 100644 --- a/src/lib/behavior.ts +++ b/src/lib/behavior.ts @@ -3,22 +3,22 @@ import * as Lib from "./utils"; // =========================================================================== export class BackgroundBehavior { - debug(msg) { - behaviorLog(msg, "debug"); + debug(msg: unknown) { + void behaviorLog(msg, "debug"); } - error(msg) { - behaviorLog(msg, "error"); + error(msg: unknown) { + void behaviorLog(msg, "error"); } - log(msg, type = "info") { - behaviorLog(msg, type); + log(msg: unknown, type = "info") { + void behaviorLog(msg, type); } } // =========================================================================== export class Behavior extends BackgroundBehavior { - _running: any; + _running: Promise | null; paused: any; _unpause: any; state: any; @@ -42,7 +42,7 @@ export class Behavior extends BackgroundBehavior { this._running = this.run(); } - done() { + async done() { return this._running ? this._running : Promise.resolve(); } @@ -56,7 +56,7 @@ export class Behavior extends BackgroundBehavior { } this.debug(this.getState("done!")); } catch (e) { - this.error(e.toString()); + this.error((e as Error).toString()); } } @@ -77,7 +77,7 @@ export class Behavior extends BackgroundBehavior { } } - getState(msg: string, incrValue?) { + getState(msg: string, incrValue?: string) { if (incrValue) { if (this.state[incrValue] === undefined) { this.state[incrValue] = 1; @@ -113,34 +113,48 @@ export class Behavior extends BackgroundBehavior { // WIP: BehaviorRunner class allows for arbitrary behaviors outside of the // library to be run through the BehaviorManager -abstract class AbstractBehaviorInst { - abstract run: (ctx: any) => AsyncIterable; +export type Context = { + Lib: typeof Lib; + state: State; + opts: Opts; + log: (data: any, type?: string) => Promise; +}; - abstract awaitPageLoad?: (ctx: any) => Promise; +abstract class AbstractBehaviorInst { + abstract run: (ctx: Context) => AsyncIterable; + + abstract awaitPageLoad?: (ctx: Context) => Promise; } interface StaticAbstractBehavior { - id: String; + id: string; isMatch: () => boolean; init: () => any; } -type AbstractBehavior = (new () => AbstractBehaviorInst) & +type AbstractBehavior = (new () => AbstractBehaviorInst< + State, + Opts, + RunResult +>) & StaticAbstractBehavior; -export class BehaviorRunner extends BackgroundBehavior { - inst: AbstractBehaviorInst; +export class BehaviorRunner extends BackgroundBehavior { + inst: AbstractBehaviorInst; behaviorProps: StaticAbstractBehavior; - ctx: any; - _running: any; + ctx: Context; + _running: Promise | null; paused: any; - _unpause: any; + _unpause: ((value?: unknown) => void) | null; get id() { - return (this.inst?.constructor as any).id; + return (this.inst.constructor as any).id; } - constructor(behavior: AbstractBehavior, mainOpts = {}) { + constructor( + behavior: AbstractBehavior, + mainOpts = {}, + ) { super(); this.behaviorProps = behavior; this.inst = new behavior(); @@ -183,7 +197,7 @@ export class BehaviorRunner extends BackgroundBehavior { this._running = this.run(); } - done() { + async done() { return this._running ? this._running : Promise.resolve(); } @@ -199,7 +213,10 @@ export class BehaviorRunner extends BackgroundBehavior { } this.debug({ msg: "done!", behavior: this.behaviorProps.id }); } catch (e) { - this.error({ msg: e.toString(), behavior: this.behaviorProps.id }); + this.error({ + msg: (e as Error).toString(), + behavior: this.behaviorProps.id, + }); } } diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 977d1a1..8afa35f 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,9 +1,21 @@ -let _logFunc = console.log; -let _behaviorMgrClass = null; - -const scrollOpts = { behavior: "smooth", block: "center", inline: "center" }; - -export async function scrollAndClick(node, interval = 500, opts = scrollOpts) { +import { type BehaviorManager } from ".."; +import { type Context } from "./behavior"; + +let _logFunc: ((...data: any[]) => void) | null = console.log; +// @ts-expect-error TODO: what is this, and why is it declared twice, once here and once as a function? +let _behaviorMgrClass: (cls: typeof BehaviorManager) => void | null = null; + +const scrollOpts: ScrollIntoViewOptions = { + behavior: "smooth", + block: "center", + inline: "center", +}; + +export async function scrollAndClick( + node: HTMLElement, + interval = 500, + opts: ScrollIntoViewOptions = scrollOpts, +) { node.scrollIntoView(opts); await sleep(interval); node.click(); @@ -11,23 +23,23 @@ export async function scrollAndClick(node, interval = 500, opts = scrollOpts) { export const waitUnit = 200; -export async function sleep(timeout) { +export async function sleep(timeout: number) { return new Promise((resolve) => setTimeout(resolve, timeout)); } -export async function waitUntil(pred, interval = waitUnit) { +export async function waitUntil(pred: () => boolean, interval = waitUnit) { while (!pred()) { await sleep(interval); } } export async function waitUntilNode( - path, - root = document, - old = null, + path: string, + root: Node = document, + old: Node | null = null, timeout = 1000, interval = waitUnit, -) { +): Promise { let node = null; let stop = false; const waitP = waitUntil(() => { @@ -48,10 +60,10 @@ export async function awaitLoad(iframe?: HTMLIFrameElement) { const doc = iframe ? iframe.contentDocument : document; const win = iframe ? iframe.contentWindow : window; return new Promise((resolve) => { - if (doc.readyState === "complete") { + if (doc?.readyState === "complete") { resolve(null); } else { - win.addEventListener("load", resolve); + win?.addEventListener("load", resolve); } }); } @@ -102,7 +114,10 @@ export function checkToJsonOverride() { !!(Array.prototype as any).toJSON; } -export async function callBinding(binding, obj): Promise { +export async function callBinding( + binding: (obj: any) => any, + obj: any, +): Promise { try { if (needUnsetToJson) { unsetAllJson(); @@ -117,19 +132,19 @@ export async function callBinding(binding, obj): Promise { } } -export async function behaviorLog(data, type = "debug") { +export async function behaviorLog(data: unknown, type = "debug") { if (_logFunc) { await callBinding(_logFunc, { data, type }); } } -export async function addLink(url): Promise { +export async function addLink(url: string): Promise { if (typeof self["__bx_addLink"] === "function") { return await callBinding(self["__bx_addLink"], url); } } -export async function doExternalFetch(url): Promise { +export async function doExternalFetch(url: string): Promise { if (typeof self["__bx_fetch"] === "function") { return await callBinding(self["__bx_fetch"], url); } @@ -137,7 +152,7 @@ export async function doExternalFetch(url): Promise { return false; } -export async function addToExternalSet(url): Promise { +export async function addToExternalSet(url: string): Promise { if (typeof self["__bx_addSet"] === "function") { return await callBinding(self["__bx_addSet"], url); } @@ -152,7 +167,7 @@ export async function waitForNetworkIdle(idleTime = 500, concurrency = 0) { } } -export async function initFlow(params): Promise { +export async function initFlow(params: any): Promise { if (typeof self["__bx_initFlow"] === "function") { return await callBinding(self["__bx_initFlow"], params); } @@ -180,7 +195,9 @@ export function assertContentValid( } } -export async function openWindow(url) { +export async function openWindow( + url: string | URL, +): Promise { if (self["__bx_open"]) { const p = new Promise((resolve) => (self["__bx_openResolve"] = resolve)); await callBinding(self["__bx_open"], { url }); @@ -190,7 +207,7 @@ export async function openWindow(url) { try { win = await p; if (win) { - return win; + return win as WindowProxy; } } catch (e) { console.warn(e); @@ -202,26 +219,29 @@ export async function openWindow(url) { return window.open(url); } -export function _setLogFunc(func) { +export function _setLogFunc(func: (message: string, level: string) => void) { _logFunc = func; } -export function _setBehaviorManager(cls) { +// @ts-expect-error TODO: why is this declared over the `_behaviorMgrClass` declared earlier? +export function _behaviorMgrClass(cls: typeof BehaviorManager) { + // @ts-expect-error TODO: `_behaviorMgrClass` is a function, is this trying to overwrite it? _behaviorMgrClass = cls; } -export function installBehaviors(obj) { +export function installBehaviors(obj: any) { + // @ts-expect-error TODO: fix types here obj.__bx_behaviors = new _behaviorMgrClass(); } // =========================================================================== export class RestoreState { matchValue: string; - constructor(childMatchSelect, child) { + constructor(childMatchSelect: string, child: Node) { this.matchValue = xpathString(childMatchSelect, child); } - async restore(rootPath, childMatch) { + async restore(rootPath: string, childMatch: string) { let root = null; while (((root = xpathNode(rootPath)), !root)) { @@ -235,7 +255,7 @@ export class RestoreState { // =========================================================================== export class HistoryState { loc: string; - constructor(op) { + constructor(op: () => void) { this.loc = window.location.href; op(); } @@ -244,7 +264,7 @@ export class HistoryState { return window.location.href !== this.loc; } - async goBack(backButtonQuery) { + async goBack(backButtonQuery: string) { if (!this.changed) { return Promise.resolve(true); } @@ -261,7 +281,7 @@ export class HistoryState { ); if (backButton) { - backButton["click"](); + (backButton as HTMLElement)["click"](); } else { window.history.back(); } @@ -270,7 +290,7 @@ export class HistoryState { } // =========================================================================== -export function xpathNode(path, root?) { +export function xpathNode(path: string, root?: Node | null) { root = root || document; return document.evaluate( path, @@ -280,7 +300,7 @@ export function xpathNode(path, root?) { ).singleNodeValue; } -export function* xpathNodes(path, root) { +export function* xpathNodes(path: string, root?: Node | null) { root = root || document; const iter = document.evaluate( path, @@ -294,13 +314,17 @@ export function* xpathNodes(path, root) { } } -export function xpathString(path, root) { +export function xpathString(path: string, root?: Node | null) { root = root || document; return document.evaluate(path, root, null, XPathResult.STRING_TYPE) .stringValue; } -export async function* iterChildElem(root, timeout, totalTimeout) { +export async function* iterChildElem( + root: Element, + timeout: number, + totalTimeout: number, +) { let child = root.firstElementChild; while (child) { @@ -308,7 +332,7 @@ export async function* iterChildElem(root, timeout, totalTimeout) { if (!child.nextElementSibling) { await Promise.race([ - waitUntil(() => !!child.nextElementSibling, timeout), + waitUntil(() => !!child?.nextElementSibling, timeout), sleep(totalTimeout), ]); } @@ -318,13 +342,14 @@ export async function* iterChildElem(root, timeout, totalTimeout) { } export async function* iterChildMatches( - path, - root, + path: string, + root: Node | null, interval = waitUnit, timeout = 5000, ) { let node = xpathNode(`.//${path}`, root); - const getMatch = (node) => xpathNode(`./following-sibling::${path}`, node); + const getMatch = (node: Node | null) => + xpathNode(`./following-sibling::${path}`, node); while (node) { yield node; let next = getMatch(node); @@ -335,7 +360,7 @@ export async function* iterChildMatches( await Promise.race([ waitUntil(() => { next = getMatch(node); - return next; + return !!next; }, interval), sleep(timeout), ]); @@ -344,7 +369,7 @@ export async function* iterChildMatches( } // =========================================================================== -export function isInViewport(elem) { +export function isInViewport(elem: Element) { const bounding = elem.getBoundingClientRect(); return ( bounding.top >= 0 && @@ -356,15 +381,15 @@ export function isInViewport(elem) { ); } -export function scrollToOffset(element, offset = 0) { +export function scrollToOffset(element: Element, offset = 0) { const elPosition = element.getBoundingClientRect().top; const topPosition = elPosition + window.pageYOffset - offset; window.scrollTo({ top: topPosition, behavior: "smooth" }); } export function scrollIntoView( - element, - opts = { + element: Element, + opts: ScrollIntoViewOptions = { behavior: "smooth", block: "center", inline: "center", @@ -373,15 +398,23 @@ export function scrollIntoView( element.scrollIntoView(opts); } -export function getState(ctx: any, msg: string, incrValue?: string) { +export type NumberKeys = { + [K in keyof T]: NonNullable extends number ? K : never; +}[keyof T]; + +export function getState< + State extends NonNullable, + Opts, + IncrKey extends NumberKeys, +>(ctx: Context, msg: string, incrValue?: IncrKey) { if (typeof ctx.state === "undefined") { - ctx.state = {}; + (ctx.state as Partial) = {}; } if (incrValue) { if (ctx.state[incrValue] === undefined) { - ctx.state[incrValue] = 1; + (ctx.state[incrValue] as number) = 1; } else { - ctx.state[incrValue]++; + (ctx.state[incrValue] as number)++; } } diff --git a/src/site/facebook.ts b/src/site/facebook.ts index bdce82c..291e38e 100644 --- a/src/site/facebook.ts +++ b/src/site/facebook.ts @@ -1,3 +1,5 @@ +import { type Context } from "../lib/behavior"; + const Q = { feed: "//div[@role='feed']", article: ".//div[@role='article']", @@ -35,8 +37,15 @@ const Q = { pageLoadWaitUntil: "//div[@role='main']", }; +type FacebookState = Partial<{ + photos: number; + videos: number; + comments: number; + posts: number; +}>; + export class FacebookTimelineBehavior { - extraWindow: any; + extraWindow: WindowProxy | null; allowNewWindow: boolean; static id = "Facebook"; @@ -60,32 +69,40 @@ export class FacebookTimelineBehavior { this.allowNewWindow = false; } - async *iterPostFeeds(ctx) { + async *iterPostFeeds(ctx: Context) { const { iterChildElem, waitUnit, waitUntil, xpathNode, xpathNodes } = ctx.Lib; - const feeds = Array.from(xpathNodes(Q.feed)); - if (feeds && feeds.length) { + const feeds = Array.from(xpathNodes(Q.feed)) as Element[]; + if (feeds.length) { for (const feed of feeds) { for await (const post of iterChildElem( feed, waitUnit, + // @ts-expect-error TODO: `waitUntil` is a function, why are we trying to multiply it by 10? waitUntil * 10, )) { - yield* this.viewPost(ctx, xpathNode(Q.article, post)); + yield* this.viewPost( + ctx, + xpathNode(Q.article, post) as Element | null, + ); } } } else { - const feed = - xpathNode(Q.pageletPostList) || + const feed = (xpathNode(Q.pageletPostList) || xpathNode(Q.pageletProfilePostList) || - xpathNode(Q.articleToPostList); + xpathNode(Q.articleToPostList)) as Element | null; if (!feed) { return; } - for await (const post of iterChildElem(feed, waitUnit, waitUntil * 10)) { - yield* this.viewPost(ctx, xpathNode(Q.article, post)); + for await (const post of iterChildElem( + feed, + waitUnit, + // @ts-expect-error TODO: again, `waitUntil` is a function, not a number + waitUntil * 10, + )) { + yield* this.viewPost(ctx, xpathNode(Q.article, post) as Element); } } @@ -94,13 +111,17 @@ export class FacebookTimelineBehavior { } } - async *viewPost(ctx, post, maxExpands = 2) { + async *viewPost( + ctx: Context, + post: Element | null, + maxExpands = 2, + ) { const { getState, scrollIntoView, sleep, waitUnit, xpathNode } = ctx.Lib; if (!post) { return; } - const postLink = xpathNode(Q.postQuery, post); + const postLink = xpathNode(Q.postQuery, post) as HTMLAnchorElement | null; let url = null; @@ -122,23 +143,34 @@ export class FacebookTimelineBehavior { //yield* this.viewPhotosOrVideos(ctx, post); - let commentRootUL = xpathNode(Q.commentList, post); + let commentRootUL = xpathNode( + Q.commentList, + post, + ) as HTMLUListElement | null; if (!commentRootUL) { - const viewCommentsButton = xpathNode(Q.viewComments, post); + const viewCommentsButton = xpathNode( + Q.viewComments, + post, + ) as HTMLElement | null; if (viewCommentsButton) { viewCommentsButton.click(); await sleep(waitUnit * 2); } - commentRootUL = xpathNode(Q.commentList, post); + commentRootUL = xpathNode(Q.commentList, post) as HTMLUListElement | null; } yield* this.iterComments(ctx, commentRootUL, maxExpands); await sleep(waitUnit * 5); } - async *viewPhotosOrVideos(ctx, post) { + async *viewPhotosOrVideos( + ctx: Context, + post: Element | null, + ) { const { getState, sleep, waitUnit, xpathNode, xpathNodes } = ctx.Lib; - const objects: any[] = Array.from(xpathNodes(Q.photosOrVideos, post)); + const objects = Array.from( + xpathNodes(Q.photosOrVideos, post), + ) as HTMLAnchorElement[]; const objHrefs = new Set(); let count = 0; @@ -178,7 +210,7 @@ export class FacebookTimelineBehavior { yield* this.viewExtraObjects(ctx, obj, type, this.allowNewWindow); } - const close = xpathNode(Q.nextSlide); + const close = xpathNode(Q.nextSlide) as HTMLElement | null; if (close) { close.click(); @@ -187,9 +219,14 @@ export class FacebookTimelineBehavior { } } - async *viewExtraObjects(ctx, obj, type, openNew) { + async *viewExtraObjects( + ctx: Context, + obj: Node | null, + type: string, + openNew: boolean, + ) { const { getState, sleep, waitUnit, waitUntil, xpathNode } = ctx.Lib; - const extraLabel = xpathNode(Q.extraLabel, obj); + const extraLabel = xpathNode(Q.extraLabel, obj) as HTMLElement | null; if (!extraLabel) { return; @@ -200,10 +237,10 @@ export class FacebookTimelineBehavior { return; } - let lastHref; + let lastHref: string | undefined; for (let i = 0; i < num; i++) { - const nextSlideButton = xpathNode(Q.nextSlideQuery); + const nextSlideButton = xpathNode(Q.nextSlideQuery) as HTMLElement | null; if (!nextSlideButton) { continue; @@ -224,7 +261,7 @@ export class FacebookTimelineBehavior { } } - async openNewWindow(ctx, url) { + async openNewWindow(ctx: Context, url: string) { if (!this.extraWindow) { this.extraWindow = await ctx.Lib.openWindow(url); } else { @@ -232,7 +269,11 @@ export class FacebookTimelineBehavior { } } - async *iterComments(ctx, commentRootUL, maxExpands = 2) { + async *iterComments( + ctx: Context, + commentRootUL: HTMLUListElement | null, + maxExpands = 2, + ) { const { getState, scrollIntoView, sleep, waitUnit, xpathNode } = ctx.Lib; if (!commentRootUL) { await sleep(waitUnit * 5); @@ -249,7 +290,10 @@ export class FacebookTimelineBehavior { scrollIntoView(commentBlock); await sleep(waitUnit * 2); - const moreReplies = xpathNode(Q.commentMoreReplies, commentBlock); + const moreReplies = xpathNode( + Q.commentMoreReplies, + commentBlock, + ) as HTMLElement | null; if (moreReplies) { moreReplies.click(); await sleep(waitUnit * 5); @@ -264,7 +308,10 @@ export class FacebookTimelineBehavior { break; } - const moreButton = xpathNode(Q.commentMoreComments, commentRootUL); + const moreButton = xpathNode( + Q.commentMoreComments, + commentRootUL, + ) as HTMLElement | null; if (moreButton) { scrollIntoView(moreButton); moreButton.click(); @@ -279,10 +326,10 @@ export class FacebookTimelineBehavior { await sleep(waitUnit * 2); } - async *iterPhotoSlideShow(ctx) { + async *iterPhotoSlideShow(ctx: Context) { const { getState, scrollIntoView, sleep, waitUnit, waitUntil, xpathNode } = ctx.Lib; - const firstPhoto = xpathNode(Q.firstPhotoThumbnail); + const firstPhoto = xpathNode(Q.firstPhotoThumbnail) as HTMLElement | null; if (!firstPhoto) { return; @@ -298,7 +345,9 @@ export class FacebookTimelineBehavior { let nextSlideButton = null; - while ((nextSlideButton = xpathNode(Q.nextSlideQuery))) { + while ( + (nextSlideButton = xpathNode(Q.nextSlideQuery) as HTMLElement | null) + ) { lastHref = window.location.href; await sleep(waitUnit); @@ -316,14 +365,14 @@ export class FacebookTimelineBehavior { yield getState(ctx, `Viewing photo ${window.location.href}`, "photos"); - const root = xpathNode(Q.photoCommentList); + const root = xpathNode(Q.photoCommentList) as HTMLUListElement | null; yield* this.iterComments(ctx, root, 2); await sleep(waitUnit * 5); } } - async *iterAllVideos(ctx) { + async *iterAllVideos(ctx: Context) { const { getState, scrollIntoView, @@ -333,14 +382,14 @@ export class FacebookTimelineBehavior { xpathNode, xpathNodes, } = ctx.Lib; - const firstInlineVideo = xpathNode("//video"); + const firstInlineVideo = xpathNode("//video") as HTMLElement | null; if (firstInlineVideo) { scrollIntoView(firstInlineVideo); await sleep(waitUnit * 5); } - let videoLink = - xpathNode(Q.firstVideoThumbnail) || xpathNode(Q.firstVideoSimple); + let videoLink = (xpathNode(Q.firstVideoThumbnail) || + xpathNode(Q.firstVideoSimple)) as HTMLElement | null; if (!videoLink) { return; @@ -360,7 +409,9 @@ export class FacebookTimelineBehavior { // wait for video to play, or 20s await Promise.race([ waitUntil(() => { - for (const video of xpathNodes("//video")) { + for (const video of xpathNodes( + "//video", + ) as Generator) { if (video.readyState >= 3) { return true; } @@ -372,7 +423,7 @@ export class FacebookTimelineBehavior { await sleep(waitUnit * 10); - const close = xpathNode(Q.nextSlide); + const close = xpathNode(Q.nextSlide) as HTMLElement | null; if (!close) { break; @@ -382,11 +433,11 @@ export class FacebookTimelineBehavior { close.click(); await waitUntil(() => window.location.href !== lastHref, waitUnit * 2); - videoLink = xpathNode(Q.nextVideo, videoLink); + videoLink = xpathNode(Q.nextVideo, videoLink) as HTMLElement | null; } } - async *run(ctx) { + async *run(ctx: Context) { const { getState, sleep, xpathNode } = ctx.Lib; yield getState(ctx, "Starting..."); @@ -406,7 +457,7 @@ export class FacebookTimelineBehavior { if (Q.isPhotoVideoPage.exec(window.location.href)) { ctx.state = { comments: 0 }; - const root = xpathNode(Q.photoCommentList); + const root = xpathNode(Q.photoCommentList) as HTMLUListElement | null; yield* this.iterComments(ctx, root, 1000); return; } @@ -415,11 +466,11 @@ export class FacebookTimelineBehavior { yield* this.iterPostFeeds(ctx); } - async awaitPageLoad(ctx: any) { + async awaitPageLoad(ctx: Context) { const { Lib, log } = ctx; const { assertContentValid, waitUntilNode } = Lib; - log("Waiting for Facebook to fully load", "info"); + void log("Waiting for Facebook to fully load", "info"); await waitUntilNode(Q.pageLoadWaitUntil, document, null, 10000); diff --git a/src/site/instagram.ts b/src/site/instagram.ts index de6912c..f6c2ef5 100644 --- a/src/site/instagram.ts +++ b/src/site/instagram.ts @@ -1,3 +1,5 @@ +import { type Context } from "../lib/behavior"; + const subpostNextOnlyChevron = "//article[@role='presentation']//div[@role='presentation']/following-sibling::button"; @@ -18,9 +20,16 @@ const Q = { pageLoadWaitUntil: "//main", }; +type InstagramState = { + comments: number; + slides: number; + posts: number; + rows: number; +}; + export class InstagramPostsBehavior { maxCommentsTime: number; - postOnlyWindow: any; + postOnlyWindow: WindowProxy | null; static id = "Instagram"; @@ -52,7 +61,10 @@ export class InstagramPostsBehavior { } } - async waitForNext(ctx, child) { + async waitForNext( + ctx: Context, + child: Element | null, + ) { if (!child) { return null; } @@ -66,9 +78,9 @@ export class InstagramPostsBehavior { return child.nextElementSibling; } - async *iterRow(ctx) { + async *iterRow(ctx: Context) { const { RestoreState, sleep, waitUnit, xpathNode } = ctx.Lib; - const root = xpathNode(Q.rootPath); + const root = xpathNode(Q.rootPath) as Element | null; if (!root) { return; @@ -88,17 +100,23 @@ export class InstagramPostsBehavior { if (restorer.matchValue) { yield child; - child = await restorer.restore(Q.rootPath, Q.childMatch); + child = (await restorer.restore( + Q.rootPath, + Q.childMatch, + )) as Element | null; } child = await this.waitForNext(ctx, child); } } - async *viewStandalonePost(ctx, origLoc) { + async *viewStandalonePost( + ctx: Context, + origLoc: string, + ) { const { getState, sleep, waitUnit, waitUntil, xpathNode, xpathString } = ctx.Lib; - const root = xpathNode(Q.rootPath); + const root = xpathNode(Q.rootPath) as HTMLElement | null; if (!root?.firstElementChild) { return; @@ -117,13 +135,13 @@ export class InstagramPostsBehavior { window.history.replaceState({}, "", firstPostHref); window.dispatchEvent(new PopStateEvent("popstate", { state: {} })); - let root2 = null; - let root3 = null; + let root2: Node | null = null; + let root3: Node | null = null; await sleep(waitUnit * 5); await waitUntil( - () => (root2 = xpathNode(Q.rootPath)) !== root && root2, + () => !!((root2 = xpathNode(Q.rootPath)) !== root && root2), waitUnit * 5, ); @@ -133,15 +151,15 @@ export class InstagramPostsBehavior { window.dispatchEvent(new PopStateEvent("popstate", { state: {} })); await waitUntil( - () => (root3 = xpathNode(Q.rootPath)) !== root2 && root3, + () => !!((root3 = xpathNode(Q.rootPath)) !== root2 && root3), waitUnit * 5, ); //} } - async *iterSubposts(ctx) { + async *iterSubposts(ctx: Context) { const { getState, sleep, waitUnit, xpathNode } = ctx.Lib; - let next = xpathNode(Q.subpostNextOnlyChevron); + let next = xpathNode(Q.subpostNextOnlyChevron) as HTMLElement | null; let count = 1; @@ -155,15 +173,15 @@ export class InstagramPostsBehavior { "slides", ); - next = xpathNode(Q.subpostPrevNextChevron); + next = xpathNode(Q.subpostPrevNextChevron) as HTMLElement | null; } await sleep(waitUnit * 5); } - async iterComments(ctx) { + async iterComments(ctx: Context) { const { scrollIntoView, sleep, waitUnit, waitUntil, xpathNode } = ctx.Lib; - const root = xpathNode(Q.commentRoot); + const root = xpathNode(Q.commentRoot) as HTMLElement | null; if (!root) { return; @@ -173,8 +191,8 @@ export class InstagramPostsBehavior { let commentsLoaded = false; - const getViewRepliesButton = (child) => { - return xpathNode(Q.viewReplies, child); + const getViewRepliesButton = (child: Element) => { + return xpathNode(Q.viewReplies, child) as HTMLElement | null; }; while (child) { @@ -190,7 +208,7 @@ export class InstagramPostsBehavior { ctx.state.comments++; await sleep(waitUnit * 2.5); - await waitUntil(() => orig !== viewReplies.textContent, waitUnit); + await waitUntil(() => orig !== viewReplies?.textContent, waitUnit); viewReplies = getViewRepliesButton(child); } @@ -200,7 +218,10 @@ export class InstagramPostsBehavior { child.nextElementSibling.tagName === "LI" && !child.nextElementSibling.nextElementSibling ) { - const loadMore = xpathNode(Q.loadMore, child.nextElementSibling); + const loadMore = xpathNode( + Q.loadMore, + child.nextElementSibling, + ) as HTMLElement | null; if (loadMore) { loadMore.click(); ctx.state.comments++; @@ -215,7 +236,10 @@ export class InstagramPostsBehavior { return commentsLoaded; } - async *iterPosts(ctx, next) { + async *iterPosts( + ctx: Context, + next: HTMLElement | null, + ) { const { getState, sleep, waitUnit, xpathNode } = ctx.Lib; //let count = 0; @@ -229,7 +253,7 @@ export class InstagramPostsBehavior { yield* this.handleSinglePost(ctx); - next = xpathNode(Q.nextPost); + next = xpathNode(Q.nextPost) as HTMLElement | null; while (!next && xpathNode(Q.postLoading)) { await sleep(waitUnit * 2.5); @@ -239,7 +263,7 @@ export class InstagramPostsBehavior { await sleep(waitUnit * 5); } - async *handleSinglePost(ctx) { + async *handleSinglePost(ctx: Context) { const { getState, sleep } = ctx.Lib; yield* this.iterSubposts(ctx); @@ -249,7 +273,7 @@ export class InstagramPostsBehavior { await Promise.race([this.iterComments(ctx), sleep(this.maxCommentsTime)]); } - async *run(ctx) { + async *run(ctx: Context) { if (window.location.pathname.startsWith("/p/")) { yield* this.handleSinglePost(ctx); return; @@ -267,11 +291,11 @@ export class InstagramPostsBehavior { yield getState(ctx, "Loading Row", "rows"); - const first = xpathNode(Q.firstPostInRow, row); + const first = xpathNode(Q.firstPostInRow, row) as HTMLElement | null; yield* this.iterPosts(ctx, first); - const close = xpathNode(Q.postCloseButton); + const close = xpathNode(Q.postCloseButton) as HTMLElement | null; if (close) { close.click(); } @@ -280,11 +304,11 @@ export class InstagramPostsBehavior { } } - async awaitPageLoad(ctx: any) { + async awaitPageLoad(ctx: Context) { const { Lib, log } = ctx; const { assertContentValid, waitUntilNode } = Lib; - log("Waiting for Instagram to fully load"); + void log("Waiting for Instagram to fully load"); await waitUntilNode(Q.pageLoadWaitUntil, document, null, 10000); diff --git a/src/site/telegram.ts b/src/site/telegram.ts index 608b8d7..811a2a4 100644 --- a/src/site/telegram.ts +++ b/src/site/telegram.ts @@ -1,3 +1,5 @@ +import { type Context } from "../lib/behavior"; + const Q = { telegramContainer: "//main//section[@class='tgme_channel_history js-message_history']", @@ -6,6 +8,10 @@ const Q = { "string(.//a[@class='tgme_widget_message_link_preview' and @href]/@href)", }; +type TelegramState = { + messages: number; +}; + export class TelegramBehavior { static id = "Telegram"; @@ -19,7 +25,10 @@ export class TelegramBehavior { }; } - async waitForPrev(ctx, child) { + async waitForPrev( + ctx: Context, + child: Element | null, + ) { if (!child) { return null; } @@ -33,7 +42,7 @@ export class TelegramBehavior { return child.previousElementSibling; } - async *run(ctx) { + async *run(ctx: Context) { const { getState, scrollIntoView, @@ -42,7 +51,7 @@ export class TelegramBehavior { xpathNode, xpathString, } = ctx.Lib; - const root = xpathNode(Q.telegramContainer); + const root = xpathNode(Q.telegramContainer) as Element | null; if (!root) { return; @@ -57,7 +66,7 @@ export class TelegramBehavior { const linkUrl = xpathString(Q.linkExternal, child); - if (linkUrl?.endsWith(".jpg") || linkUrl.endsWith(".png")) { + if (linkUrl.endsWith(".jpg") || linkUrl.endsWith(".png")) { yield getState(ctx, "Loading External Image: " + linkUrl); const image = new Image(); image.src = linkUrl; diff --git a/src/site/tiktok.ts b/src/site/tiktok.ts index 45e8046..dde00c8 100644 --- a/src/site/tiktok.ts +++ b/src/site/tiktok.ts @@ -1,3 +1,5 @@ +import { type Context } from "../lib/behavior"; + const Q = { commentList: "//div[contains(@class, 'CommentListContainer')]", commentItem: "div[contains(@class, 'CommentItemContainer')]", @@ -12,8 +14,16 @@ const Q = { export const BREADTH_ALL = Symbol("BREADTH_ALL"); +type TikTokState = { + comments: number; + videos: number; +}; +type TikTokOpts = { + breadth: number | typeof BREADTH_ALL; +}; + export class TikTokSharedBehavior { - async awaitPageLoad(ctx: any) { + async awaitPageLoad(ctx: Context) { const { assertContentValid, waitUntilNode } = ctx.Lib; await waitUntilNode(Q.pageLoadWaitUntil, document, null, 10000); @@ -39,34 +49,49 @@ export class TikTokVideoBehavior extends TikTokSharedBehavior { return !!window.location.href.match(pathRegex); } - breadthComplete({ opts: { breadth } }, iter) { + breadthComplete( + { opts: { breadth } }: { opts: { breadth: number | typeof BREADTH_ALL } }, + iter: number, + ) { return breadth !== BREADTH_ALL && breadth <= iter; } - async *crawlThread(ctx, parentNode, prev = null, iter = 0) { + async *crawlThread( + ctx: Context, + parentNode: Node | null = null, + prev: Node | null = null, + iter = 0, + ): AsyncGenerator<{ state: TikTokState; msg: string }> { const { waitUntilNode, scrollAndClick, getState } = ctx.Lib; - const next = await waitUntilNode(Q.viewMoreThread, parentNode, prev); + const next = (await waitUntilNode( + Q.viewMoreThread, + parentNode ?? undefined, + prev, + )) as HTMLElement | null; if (!next || this.breadthComplete(ctx, iter)) return; await scrollAndClick(next, 500); yield getState(ctx, "View more replies", "comments"); yield* this.crawlThread(ctx, parentNode, next, iter + 1); } - async *expandThread(ctx, item) { + async *expandThread( + ctx: Context, + item: Node | null, + ) { const { xpathNode, scrollAndClick, getState } = ctx.Lib; - const viewMore = xpathNode(Q.viewMoreReplies, item); + const viewMore = xpathNode(Q.viewMoreReplies, item) as HTMLElement | null; if (!viewMore) return; await scrollAndClick(viewMore, 500); yield getState(ctx, "View comment", "comments"); yield* this.crawlThread(ctx, item, null, 1); } - async *run(ctx) { + async *run(ctx: Context) { const { xpathNode, iterChildMatches, scrollIntoView, getState } = ctx.Lib; const commentList = xpathNode(Q.commentList); const commentItems = iterChildMatches(Q.commentItem, commentList); for await (const item of commentItems) { - scrollIntoView(item); + scrollIntoView(item as Element); yield getState(ctx, "View comment", "comments"); if (this.breadthComplete(ctx, 0)) continue; yield* this.expandThread(ctx, item); @@ -91,9 +116,9 @@ export class TikTokProfileBehavior extends TikTokSharedBehavior { }; } - async *openVideo(ctx, item) { + async *openVideo(ctx: Context, item: Node | null) { const { HistoryState, xpathNode, sleep } = ctx.Lib; - const link = xpathNode(".//a", item); + const link = xpathNode(".//a", item) as HTMLElement | null; if (!link) return; const viewState = new HistoryState(() => link.click()); await sleep(500); @@ -105,7 +130,7 @@ export class TikTokProfileBehavior extends TikTokSharedBehavior { } } - async *run(ctx) { + async *run(ctx: Context) { const { xpathNode, iterChildMatches, scrollIntoView, getState, sleep } = ctx.Lib; const profileVideoList = xpathNode(Q.profileVideoList); @@ -114,7 +139,7 @@ export class TikTokProfileBehavior extends TikTokSharedBehavior { profileVideoList, ); for await (const item of profileVideos) { - scrollIntoView(item); + scrollIntoView(item as HTMLElement); yield getState(ctx, "View video", "videos"); yield* this.openVideo(ctx, item); await sleep(500); diff --git a/src/site/twitter.ts b/src/site/twitter.ts index 1a04ef6..4e5ee74 100644 --- a/src/site/twitter.ts +++ b/src/site/twitter.ts @@ -1,3 +1,5 @@ +import { type Context } from "../lib/behavior"; + const Q = { rootPath: "//h1[@role='heading' and @aria-level='1']/following-sibling::div[@aria-label]//div[@style]", @@ -22,9 +24,20 @@ const Q = { promoted: ".//div[data-testid='placementTracking']", }; +type TwitterState = Partial<{ + tweets: number; + images: number; + videos: number; + threads: number; +}>; + +type TwitterOpts = { + maxDepth: number; +}; + export class TwitterTimelineBehavior { - seenTweets: Set; - seenMediaTweets: Set; + seenTweets: Set; + seenMediaTweets: Set; static id = "Twitter"; @@ -50,9 +63,12 @@ export class TwitterTimelineBehavior { this.seenMediaTweets = new Set(); } - showingProgressBar(ctx, root) { + showingProgressBar( + ctx: Context, + root: Node | null, + ) { const { xpathNode } = ctx.Lib; - const node = xpathNode(Q.progress, root); + const node = xpathNode(Q.progress, root) as Element | null; if (!node) { return false; } @@ -60,7 +76,10 @@ export class TwitterTimelineBehavior { return node.clientHeight > 10; } - async waitForNext(ctx, child) { + async waitForNext( + ctx: Context, + child: Element | null, + ) { const { sleep, waitUnit } = ctx.Lib; if (!child) { return null; @@ -79,44 +98,47 @@ export class TwitterTimelineBehavior { return child.nextElementSibling; } - async expandMore(ctx, child) { + async expandMore( + ctx: Context, + child: Element | null, + ) { const { sleep, waitUnit, xpathNode } = ctx.Lib; - const expandElem = xpathNode(Q.expand, child); + const expandElem = xpathNode(Q.expand, child) as HTMLElement | null; if (!expandElem) { return child; } - const prev = child.previousElementSibling; + const prev = child?.previousElementSibling; expandElem.click(); await sleep(waitUnit); - while (this.showingProgressBar(ctx, prev.nextElementSibling)) { + while (this.showingProgressBar(ctx, prev?.nextElementSibling ?? null)) { await sleep(waitUnit); } - child = prev.nextElementSibling; + child = prev?.nextElementSibling ?? null; return child; } - async *infScroll(ctx) { + async *infScroll(ctx: Context) { const { scrollIntoView, RestoreState, sleep, waitUnit, xpathNode } = ctx.Lib; - const root = xpathNode(Q.rootPath); + const root = xpathNode(Q.rootPath) as Element | null; if (!root) { return; } - let child = root.firstElementChild; + let child = root.firstElementChild as HTMLElement | null; if (!child) { return; } while (child) { - let anchorElem = xpathNode(Q.anchor, child); + let anchorElem = xpathNode(Q.anchor, child) as HTMLElement | null; if (!anchorElem && Q.expand) { - child = await this.expandMore(ctx, child); - anchorElem = xpathNode(Q.anchor, child); + child = (await this.expandMore(ctx, child)) as HTMLElement | null; + anchorElem = xpathNode(Q.anchor, child) as HTMLElement | null; } if (child?.innerText) { @@ -131,17 +153,26 @@ export class TwitterTimelineBehavior { yield anchorElem; if (restorer.matchValue) { - child = await restorer.restore(Q.rootPath, Q.childMatch); + child = (await restorer.restore( + Q.rootPath, + Q.childMatch, + )) as HTMLElement | null; } } - child = await this.waitForNext(ctx, child); + child = (await this.waitForNext(ctx, child)) as HTMLElement | null; } } - async *mediaPlaying(ctx, tweet) { + async *mediaPlaying( + ctx: Context, + tweet: HTMLElement, + ) { const { getState, sleep, xpathNode, xpathString } = ctx.Lib; - const media = xpathNode("(.//video | .//audio)", tweet); + const media = xpathNode( + "(.//video | .//audio)", + tweet, + ) as HTMLMediaElement | null; if (!media || media.paused) { return; } @@ -192,9 +223,12 @@ export class TwitterTimelineBehavior { await Promise.race([p, sleep(60000)]); } - async *clickImages(ctx, tweet) { + async *clickImages( + ctx: Context, + tweet: HTMLElement, + ) { const { getState, HistoryState, sleep, waitUnit, xpathNode } = ctx.Lib; - const imagePopup = xpathNode(Q.image, tweet); + const imagePopup = xpathNode(Q.image, tweet) as HTMLElement | null; if (imagePopup) { const imageState = new HistoryState(() => imagePopup.click()); @@ -203,7 +237,7 @@ export class TwitterTimelineBehavior { await sleep(waitUnit * 5); - let nextImage = xpathNode(Q.imageFirstNext); + let nextImage = xpathNode(Q.imageFirstNext) as HTMLElement | null; let prevLocation = window.location.href; while (nextImage) { @@ -219,14 +253,18 @@ export class TwitterTimelineBehavior { yield getState(ctx, "Loading Image: " + window.location.href, "images"); await sleep(waitUnit * 5); - nextImage = xpathNode(Q.imageNext); + nextImage = xpathNode(Q.imageNext) as HTMLElement | null; } await imageState.goBack(Q.imageClose); } } - async *clickTweet(ctx, tweet, depth) { + async *clickTweet( + ctx: Context, + tweet: HTMLElement, + depth: number, + ): AsyncGenerator<{ state: TwitterState; msg: string }> { const { getState, HistoryState, sleep, waitUnit } = ctx.Lib; const tweetState = new HistoryState(() => tweet.click()); @@ -250,7 +288,7 @@ export class TwitterTimelineBehavior { } } - async *iterTimeline(ctx, depth = 0) { + async *iterTimeline(ctx: Context, depth = 0) { const { getState, sleep, waitUnit, xpathNode } = ctx.Lib; if (this.seenTweets.has(window.location.href)) { return; @@ -267,7 +305,10 @@ export class TwitterTimelineBehavior { await sleep(waitUnit * 2.5); - const viewButton = xpathNode(Q.viewSensitive, tweet); + const viewButton = xpathNode( + Q.viewSensitive, + tweet, + ) as HTMLElement | null; if (viewButton) { viewButton.click(); await sleep(waitUnit * 2.5); @@ -277,7 +318,7 @@ export class TwitterTimelineBehavior { yield* this.clickImages(ctx, tweet); // process quoted tweet - const quoteTweet = xpathNode(Q.quote, tweet); + const quoteTweet = xpathNode(Q.quote, tweet) as HTMLElement | null; if (quoteTweet) { yield* this.clickTweet(ctx, quoteTweet, 1000); @@ -294,11 +335,11 @@ export class TwitterTimelineBehavior { } } - async *run(ctx) { + async *run(ctx: Context) { yield* this.iterTimeline(ctx, 0); } - async awaitPageLoad(ctx: any) { + async awaitPageLoad(ctx: Context) { const { sleep, assertContentValid } = ctx.Lib; await sleep(5); assertContentValid( diff --git a/src/site/youtube.ts b/src/site/youtube.ts index d0322e4..a1a6dce 100644 --- a/src/site/youtube.ts +++ b/src/site/youtube.ts @@ -1,12 +1,13 @@ import { AutoScroll } from "../autoscroll"; +import { type Context } from "../lib/behavior"; export class YoutubeBehavior extends AutoScroll { - override async awaitPageLoad(ctx: any) { + override async awaitPageLoad(ctx: Context<{}, {}>) { const { sleep, assertContentValid } = ctx.Lib; await sleep(10); assertContentValid(() => { const video = document.querySelector("video"); - const paused = video && video.paused; + const paused = video?.paused; if (paused) { return false; } diff --git a/tsconfig.json b/tsconfig.json index f1b3d79..96fb0da 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,7 @@ "preserveConstEnums": true, "allowJs": true, "checkJs": true, + "strict": true, "target": "es2022", "lib": ["es2022", "dom", "dom.iterable"], "outDir": "./dist/" diff --git a/yarn.lock b/yarn.lock index 5a367ec..f248779 100644 --- a/yarn.lock +++ b/yarn.lock @@ -535,6 +535,11 @@ resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== +"@types/query-selector-shadow-dom@^1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@types/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.4.tgz#89712de2b066866865f6e01698b4cbbe6fedab82" + integrity sha512-8jfGPD0wCMCdyBvrvOrWVn8bHL1UEjkPVJKsqNZpEXp+a7mIIvvGpJMd6n6dlNl7IkG2ryxIVqFI516RPE0uhQ== + "@types/responselike@*", "@types/responselike@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29" From 71213b022462a184433eb3be949a8ad9c917ece0 Mon Sep 17 00:00:00 2001 From: emma Date: Mon, 25 Aug 2025 17:09:14 -0400 Subject: [PATCH 2/9] wip --- src/autoplay.ts | 54 +++++++++++++++++++++++---------------------- src/index.ts | 50 +++++++++++++++++++++++------------------ src/lib/behavior.ts | 9 +++----- src/lib/utils.ts | 4 +++- 4 files changed, 63 insertions(+), 54 deletions(-) diff --git a/src/autoplay.ts b/src/autoplay.ts index 58acb2f..ef0a0fb 100644 --- a/src/autoplay.ts +++ b/src/autoplay.ts @@ -54,16 +54,19 @@ export class Autoplay extends BackgroundBehavior { for (const [, elem] of querySelectorAllDeep( "video, audio, picture", ).entries()) { - if (!elem["__bx_autoplay_found"]) { + interface AutoplayElement extends HTMLMediaElement { + __bx_autoplay_found?: boolean; + } + if (!(elem as AutoplayElement)["__bx_autoplay_found"]) { if (!this.running) { if (this.processFetchableUrl(elem as HTMLMediaElement)) { - elem["__bx_autoplay_found"] = true; + (elem as AutoplayElement)["__bx_autoplay_found"] = true; } continue; } await this.loadMedia(elem as HTMLMediaElement); - elem["__bx_autoplay_found"] = true; + (elem as AutoplayElement)["__bx_autoplay_found"] = true; } } @@ -140,46 +143,45 @@ export class Autoplay extends BackgroundBehavior { ); } - void this.attemptMediaPlay(media).then( - async (finished: Promise | undefined) => { - let check = true; + void this.attemptMediaPlay(media).then(async (finished) => { + let check = true; - if (finished) { - void finished.then(() => (check = false)); - } + if (finished) { + // @ts-expect-error TODO: not sure what this is supposed to be, I believe `finished` is a boolean here? + void finished.then(() => (check = false)); + } - while (check) { - if (this.processFetchableUrl(media)) { - check = false; - } - this.debug( - "Waiting for fixed URL or media to finish: " + media.currentSrc, - ); - await sleep(1000); + while (check) { + if (this.processFetchableUrl(media)) { + check = false; } - }, - ); + this.debug( + "Waiting for fixed URL or media to finish: " + media.currentSrc, + ); + await sleep(1000); + } + }); } else if (media.currentSrc) { this.debug("media playing from non-URL source: " + media.currentSrc); } } - async attemptMediaPlay(media) { + async attemptMediaPlay(media: HTMLMediaElement) { // finished promise - let resolveFinished; + let resolveFinished: (value?: boolean) => void; - const finished = new Promise((res) => { + const finished = new Promise((res) => { resolveFinished = res; }); // started promise - let resolveStarted; + let resolveStarted!: (value?: boolean) => void; - const started = new Promise((res) => { + const started = new Promise((res) => { resolveStarted = res; }); - started.then(() => this.promises.push(finished)); + void started.then(() => this.promises.push(finished)); // already started if (!media.paused && media.currentTime > 0) { @@ -242,7 +244,7 @@ export class Autoplay extends BackgroundBehavior { } } - media.play(); + void media.play(); if (await Promise.race([started, sleep(1000)])) { this.debug("play started after media.play()"); diff --git a/src/index.ts b/src/index.ts index 379922e..df36a38 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,7 +12,7 @@ import { addLink, checkToJsonOverride, } from "./lib/utils"; -import { type Behavior, BehaviorRunner } from "./lib/behavior"; +import { AbstractBehavior, type Behavior, BehaviorRunner } from "./lib/behavior"; import * as Lib from "./lib/utils"; import siteBehaviors from "./site"; @@ -27,10 +27,10 @@ interface BehaviorManagerOpts { autoplay?: boolean; autoscroll?: boolean; autoclick?: boolean; - log?: ((...message: string[]) => void) | string | false; - siteSpecific?: boolean | object; + log?: ((...message: string[]) => void) | keyof typeof self | false; + siteSpecific?: boolean | Record; timeout?: number; - fetchHeaders?: object | null; + fetchHeaders?: Record | null; startEarly?: boolean | null; clickSelector?: string; } @@ -54,12 +54,14 @@ const DEFAULT_CLICK_SELECTOR = "a"; const DEFAULT_LINK_SELECTOR = "a[href]"; const DEFAULT_LINK_EXTRACT = "href"; +type TODOBehaviorClass = AbstractBehavior; + export class BehaviorManager { - autofetch: AutoFetcher; - behaviors: any[]; - loadedBehaviors: any; + autofetch?: AutoFetcher; + behaviors: TODOBehaviorClass[] | null; + loadedBehaviors: TODOBehaviorClass; mainBehavior: Behavior | BehaviorRunner | null; - mainBehaviorClass: any; + mainBehaviorClass: TODOBehaviorClass; inited: boolean; started: boolean; timeout?: number; @@ -79,13 +81,13 @@ export class BehaviorManager { selector: DEFAULT_LINK_SELECTOR, extractName: DEFAULT_LINK_EXTRACT, }; - behaviorLog("Loaded behaviors for: " + self.location.href); + void behaviorLog("Loaded behaviors for: " + self.location.href); } init( opts: BehaviorManagerOpts = DEFAULT_OPTS, restart = false, - customBehaviors: any[] = null, + customBehaviors: TODOBehaviorClass[] | null = null, ) { if (this.inited && !restart) { return; @@ -94,6 +96,7 @@ export class BehaviorManager { this.inited = true; this.opts = opts; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!self.window) { return; } @@ -119,22 +122,22 @@ export class BehaviorManager { this.autofetch = new AutoFetcher( !!opts.autofetch, opts.fetchHeaders, - opts.startEarly, + !!opts.startEarly, ); if (opts.autofetch) { - behaviorLog("Using AutoFetcher"); - this.behaviors.push(this.autofetch); + void behaviorLog("Using AutoFetcher"); + this.behaviors!.push(this.autofetch); } if (opts.autoplay) { - behaviorLog("Using Autoplay"); - this.behaviors.push(new Autoplay(this.autofetch, opts.startEarly)); + void behaviorLog("Using Autoplay"); + this.behaviors!.push(new Autoplay(this.autofetch, !!opts.startEarly)); } if (opts.autoclick) { - behaviorLog("Using AutoClick"); - this.behaviors.push( + void behaviorLog("Using AutoClick"); + this.behaviors!.push( new AutoClick(opts.clickSelector || DEFAULT_CLICK_SELECTOR), ); } @@ -144,7 +147,9 @@ export class BehaviorManager { try { this.load(behaviorClass); } catch (e) { - behaviorLog(`Failed to load custom behavior: ${e} ${behaviorClass}`); + void behaviorLog( + `Failed to load custom behavior: ${e} ${behaviorClass}`, + ); } } } @@ -157,11 +162,11 @@ export class BehaviorManager { const opts = this.opts; let siteMatch = false; - if (opts.siteSpecific) { + if (opts?.siteSpecific) { for (const name in this.loadedBehaviors) { const siteBehaviorClass = this.loadedBehaviors[name]; if (siteBehaviorClass.isMatch()) { - behaviorLog("Using Site-Specific Behavior: " + name); + void behaviorLog("Using Site-Specific Behavior: " + name); this.mainBehaviorClass = siteBehaviorClass; const siteSpecificOpts = typeof opts.siteSpecific === "object" @@ -173,7 +178,10 @@ export class BehaviorManager { siteSpecificOpts, ); } catch (e) { - behaviorLog({ msg: e.toString(), siteSpecific: true }, "error"); + void behaviorLog( + { msg: (e as Error).toString(), siteSpecific: true }, + "error", + ); } siteMatch = true; break; diff --git a/src/lib/behavior.ts b/src/lib/behavior.ts index 92cca0c..2741162 100644 --- a/src/lib/behavior.ts +++ b/src/lib/behavior.ts @@ -132,12 +132,9 @@ interface StaticAbstractBehavior { init: () => any; } -type AbstractBehavior = (new () => AbstractBehaviorInst< - State, - Opts, - RunResult ->) & - StaticAbstractBehavior; +export type AbstractBehavior = + (new () => AbstractBehaviorInst) & + StaticAbstractBehavior; export class BehaviorRunner extends BackgroundBehavior { inst: AbstractBehaviorInst; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 8afa35f..6ce796e 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -219,7 +219,9 @@ export async function openWindow( return window.open(url); } -export function _setLogFunc(func: (message: string, level: string) => void) { +export function _setLogFunc( + func: ((message: string, level: string) => void) | null, +) { _logFunc = func; } From efdf5956f0cd0caecc27c753232811a99d7b610e Mon Sep 17 00:00:00 2001 From: emma Date: Mon, 25 Aug 2025 18:02:30 -0400 Subject: [PATCH 3/9] add globals --- src/autoplay.ts | 13 +++++---- src/autoscroll.ts | 14 +++++----- src/lib/behavior.ts | 62 ++++++++++++++++++++++++++++--------------- src/lib/global.d.ts | 34 ++++++++++++++++++++++++ src/lib/utils.ts | 48 ++++++++++++++++++--------------- src/site/facebook.ts | 35 ++++++++++++------------ src/site/instagram.ts | 33 +++++++++-------------- src/site/telegram.ts | 13 ++++----- src/site/tiktok.ts | 12 ++++++--- src/site/twitter.ts | 18 +++++++------ 10 files changed, 167 insertions(+), 115 deletions(-) create mode 100644 src/lib/global.d.ts diff --git a/src/autoplay.ts b/src/autoplay.ts index ef0a0fb..22d208e 100644 --- a/src/autoplay.ts +++ b/src/autoplay.ts @@ -53,20 +53,19 @@ export class Autoplay extends BackgroundBehavior { while (run) { for (const [, elem] of querySelectorAllDeep( "video, audio, picture", - ).entries()) { - interface AutoplayElement extends HTMLMediaElement { - __bx_autoplay_found?: boolean; - } - if (!(elem as AutoplayElement)["__bx_autoplay_found"]) { + ).entries() as ArrayIterator< + [number, HTMLVideoElement | HTMLAudioElement | HTMLPictureElement] + >) { + if (!elem["__bx_autoplay_found"]) { if (!this.running) { if (this.processFetchableUrl(elem as HTMLMediaElement)) { - (elem as AutoplayElement)["__bx_autoplay_found"] = true; + elem["__bx_autoplay_found"] = true; } continue; } await this.loadMedia(elem as HTMLMediaElement); - (elem as AutoplayElement)["__bx_autoplay_found"] = true; + elem["__bx_autoplay_found"] = true; } } diff --git a/src/autoscroll.ts b/src/autoscroll.ts index 09f5de6..7006897 100644 --- a/src/autoscroll.ts +++ b/src/autoscroll.ts @@ -11,7 +11,7 @@ import { import { type AutoFetcher } from "./autofetcher"; // =========================================================================== -export class AutoScroll extends Behavior { +export class AutoScroll extends Behavior<{}, {}> { autoFetcher: AutoFetcher; showMoreQuery: string; state: { segments: number } = { segments: 1 }; @@ -57,7 +57,7 @@ export class AutoScroll extends Behavior { this.lastMsg = msg; } - hasScrollEL(obj) { + hasScrollEL(obj: HTMLElement | Document | Window) { try { return !!self["getEventListeners"](obj).scroll; } catch (_) { @@ -81,12 +81,12 @@ export class AutoScroll extends Behavior { return true; } - const lastScrollHeight = self.document.scrollingElement.scrollHeight; + const lastScrollHeight = self.document.scrollingElement!.scrollHeight; const numFetching = this.autoFetcher.numFetching; // scroll to almost end of page const scrollEnd = - document.scrollingElement.scrollHeight * 0.98 - self.innerHeight; + document.scrollingElement!.scrollHeight * 0.98 - self.innerHeight; window.scrollTo({ top: scrollEnd, left: 0, behavior: "smooth" }); @@ -95,7 +95,7 @@ export class AutoScroll extends Behavior { // scroll height changed, should scroll if ( - lastScrollHeight !== self.document.scrollingElement.scrollHeight || + lastScrollHeight !== self.document.scrollingElement!.scrollHeight || numFetching < this.autoFetcher.numFetching ) { window.scrollTo({ top: 0, left: 0, behavior: "auto" }); @@ -112,7 +112,7 @@ export class AutoScroll extends Behavior { if ( (self.window.scrollY + self["scrollHeight"]) / - self.document.scrollingElement.scrollHeight < + self.document.scrollingElement!.scrollHeight < 0.9 ) { return false; @@ -194,7 +194,6 @@ export class AutoScroll extends Behavior { showMoreElem = null; } - // eslint-disable-next-line self.scrollBy(scrollOpts as ScrollToOptions); await sleep(interval); @@ -256,7 +255,6 @@ export class AutoScroll extends Behavior { lastScrollHeight = scrollHeight; } - // eslint-disable-next-line self.scrollBy(scrollOpts as ScrollToOptions); await sleep(interval); diff --git a/src/lib/behavior.ts b/src/lib/behavior.ts index 2741162..010ec61 100644 --- a/src/lib/behavior.ts +++ b/src/lib/behavior.ts @@ -17,7 +17,10 @@ export class BackgroundBehavior { } // =========================================================================== -export class Behavior extends BackgroundBehavior { +export class Behavior + extends BackgroundBehavior + implements AbstractBehavior +{ _running: Promise | null; paused: any; _unpause: any; @@ -105,7 +108,11 @@ export class Behavior extends BackgroundBehavior { } } - async *[Symbol.asyncIterator]() { + async *[Symbol.asyncIterator](): AsyncGenerator< + State | undefined, + void, + void + > { yield; } } @@ -113,43 +120,54 @@ export class Behavior extends BackgroundBehavior { // WIP: BehaviorRunner class allows for arbitrary behaviors outside of the // library to be run through the BehaviorManager -export type Context = { +export type EmptyObject = Record; + +export type Context = { Lib: typeof Lib; state: State; opts: Opts; log: (data: any, type?: string) => Promise; }; -abstract class AbstractBehaviorInst { - abstract run: (ctx: Context) => AsyncIterable; +export abstract class AbstractBehavior { + static id: String; + static isMatch: () => boolean; + static init: () => any; + + abstract run: (ctx: Context) => AsyncIterable; abstract awaitPageLoad?: (ctx: Context) => Promise; } -interface StaticAbstractBehavior { - id: string; - isMatch: () => boolean; - init: () => any; -} +type StaticProps = { + [K in keyof T]: T[K]; +}; -export type AbstractBehavior = - (new () => AbstractBehaviorInst) & - StaticAbstractBehavior; +type StaticBehaviorProps = StaticProps; -export class BehaviorRunner extends BackgroundBehavior { - inst: AbstractBehaviorInst; - behaviorProps: StaticAbstractBehavior; +// Non-abstract constructor type +type ConcreteBehaviorConstructor = StaticBehaviorProps & { + new (): AbstractBehavior; +}; + +export class BehaviorRunner< + State, + Opts = EmptyObject, +> extends BackgroundBehavior { + inst: AbstractBehavior; + behaviorProps: ConcreteBehaviorConstructor; ctx: Context; - _running: Promise | null; + _running: any; paused: any; - _unpause: ((value?: unknown) => void) | null; + _unpause: any; get id() { - return (this.inst.constructor as any).id; + return (this.inst.constructor as ConcreteBehaviorConstructor) + .id; } constructor( - behavior: AbstractBehavior, + behavior: ConcreteBehaviorConstructor, mainOpts = {}, ) { super(); @@ -167,7 +185,7 @@ export class BehaviorRunner extends BackgroundBehavior { state = state || {}; opts = opts ? { ...opts, ...mainOpts } : mainOpts; // eslint-disable-next-line @typescript-eslint/no-explicit-any - const log = async (data: any, type: string) => this.wrappedLog(data, type); + const log = async (data: any, type?: string) => this.wrappedLog(data, type); this.ctx = { Lib, state, opts, log }; @@ -194,7 +212,7 @@ export class BehaviorRunner extends BackgroundBehavior { this._running = this.run(); } - async done() { + done() { return this._running ? this._running : Promise.resolve(); } diff --git a/src/lib/global.d.ts b/src/lib/global.d.ts new file mode 100644 index 0000000..cd1373e --- /dev/null +++ b/src/lib/global.d.ts @@ -0,0 +1,34 @@ +import { type BehaviorManager } from ".."; + +export {}; // Ensure this is treated as a module + +interface BehaviorGlobals { + __bx_addLink?: (url: string) => Promise; + __bx_fetch?: (url: string) => Promise; + __bx_addSet?: (url: string) => Promise; + __bx_netIdle?: (params: { + idleTime: number; + concurrency: number; + }) => Promise; + __bx_initFlow?: (params: any) => Promise; + __bx_nextFlowStep?: (id: number) => Promise; + __bx_contentCheckFailed?: (reason: string) => void; + __bx_open?: (params: { url: string | URL }) => Promise; + __bx_openResolve?: (window: WindowProxy | null) => void; + __bx_behaviors?: BehaviorManager; +} + +interface AutoplayProperties { + __bx_autoplay_found?: boolean; +} + +declare global { + interface WorkerGlobalScope extends BehaviorGlobals {} + interface Window extends BehaviorGlobals { + __WB_replay_top?: Window; + } + + interface HTMLVideoElement extends AutoplayProperties {} + interface HTMLAudioElement extends AutoplayProperties {} + interface HTMLPictureElement extends AutoplayProperties {} +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 6ce796e..11d83ed 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,6 +1,7 @@ import { type BehaviorManager } from ".."; import { type Context } from "./behavior"; +// eslint-disable-next-line @typescript-eslint/no-explicit-any let _logFunc: ((...data: any[]) => void) | null = console.log; // @ts-expect-error TODO: what is this, and why is it declared twice, once here and once as a function? let _behaviorMgrClass: (cls: typeof BehaviorManager) => void | null = null; @@ -68,6 +69,7 @@ export async function awaitLoad(iframe?: HTMLIFrameElement) { }); } +// eslint-disable-next-line @typescript-eslint/no-explicit-any function unsetToJson(obj: any) { if (obj.toJSON) { try { @@ -79,6 +81,7 @@ function unsetToJson(obj: any) { } } +// eslint-disable-next-line @typescript-eslint/no-explicit-any function restoreToJson(obj: any) { if (obj.__bx__toJSON) { try { @@ -91,40 +94,44 @@ function restoreToJson(obj: any) { } function unsetAllJson() { - unsetToJson(Object as any); - unsetToJson(Object.prototype as any); - unsetToJson(Array as any); - unsetToJson(Array.prototype as any); + unsetToJson(Object); + unsetToJson(Object.prototype); + unsetToJson(Array); + unsetToJson(Array.prototype); } function restoreAllJson() { - restoreToJson(Object as any); - restoreToJson(Object.prototype as any); - restoreToJson(Array as any); - restoreToJson(Array.prototype as any); + restoreToJson(Object); + restoreToJson(Object.prototype); + restoreToJson(Array); + restoreToJson(Array.prototype); } let needUnsetToJson = false; +type WithToJSON = T & { + toJSON?: () => unknown; +}; + export function checkToJsonOverride() { needUnsetToJson = - !!(Object as any).toJSON || - !!(Object.prototype as any).toJSON || - !!(Array as any).toJSON || - !!(Array.prototype as any).toJSON; + !!(Object as WithToJSON).toJSON || + !!(Object.prototype as WithToJSON).toJSON || + !!(Array as WithToJSON).toJSON || + !!(Array.prototype as WithToJSON).toJSON; } -export async function callBinding( - binding: (obj: any) => any, - obj: any, -): Promise { +export async function callBinding( + binding: (obj: P) => R, + obj: P, +): Promise { try { if (needUnsetToJson) { unsetAllJson(); } return binding(obj); } catch (_) { - return binding(JSON.stringify(obj)); + return binding(JSON.stringify(obj) as P); } finally { if (needUnsetToJson) { restoreAllJson(); @@ -189,8 +196,8 @@ export function assertContentValid( ) { if (typeof self["__bx_contentCheckFailed"] === "function") { if (!assertFunc()) { - behaviorLog("Behavior content check failed: " + reason, "error"); - callBinding(self["__bx_contentCheckFailed"], reason); + void behaviorLog("Behavior content check failed: " + reason, "error"); + void callBinding(self["__bx_contentCheckFailed"], reason); } } } @@ -231,8 +238,7 @@ export function _behaviorMgrClass(cls: typeof BehaviorManager) { _behaviorMgrClass = cls; } -export function installBehaviors(obj: any) { - // @ts-expect-error TODO: fix types here +export function installBehaviors(obj: Window | WorkerGlobalScope) { obj.__bx_behaviors = new _behaviorMgrClass(); } diff --git a/src/site/facebook.ts b/src/site/facebook.ts index 291e38e..74d1ae6 100644 --- a/src/site/facebook.ts +++ b/src/site/facebook.ts @@ -1,4 +1,4 @@ -import { type Context } from "../lib/behavior"; +import { type AbstractBehavior, type Context } from "../lib/behavior"; const Q = { feed: "//div[@role='feed']", @@ -44,7 +44,9 @@ type FacebookState = Partial<{ posts: number; }>; -export class FacebookTimelineBehavior { +export class FacebookTimelineBehavior + implements AbstractBehavior +{ extraWindow: WindowProxy | null; allowNewWindow: boolean; @@ -69,7 +71,7 @@ export class FacebookTimelineBehavior { this.allowNewWindow = false; } - async *iterPostFeeds(ctx: Context) { + async *iterPostFeeds(ctx: Context) { const { iterChildElem, waitUnit, waitUntil, xpathNode, xpathNodes } = ctx.Lib; const feeds = Array.from(xpathNodes(Q.feed)) as Element[]; @@ -112,7 +114,7 @@ export class FacebookTimelineBehavior { } async *viewPost( - ctx: Context, + ctx: Context, post: Element | null, maxExpands = 2, ) { @@ -123,7 +125,7 @@ export class FacebookTimelineBehavior { const postLink = xpathNode(Q.postQuery, post) as HTMLAnchorElement | null; - let url = null; + let url: URL | null = null; if (postLink) { url = new URL(postLink.href, window.location.href); @@ -163,10 +165,7 @@ export class FacebookTimelineBehavior { await sleep(waitUnit * 5); } - async *viewPhotosOrVideos( - ctx: Context, - post: Element | null, - ) { + async *viewPhotosOrVideos(ctx: Context, post: Element | null) { const { getState, sleep, waitUnit, xpathNode, xpathNodes } = ctx.Lib; const objects = Array.from( xpathNodes(Q.photosOrVideos, post), @@ -220,7 +219,7 @@ export class FacebookTimelineBehavior { } async *viewExtraObjects( - ctx: Context, + ctx: Context, obj: Node | null, type: string, openNew: boolean, @@ -261,7 +260,7 @@ export class FacebookTimelineBehavior { } } - async openNewWindow(ctx: Context, url: string) { + async openNewWindow(ctx: Context, url: string) { if (!this.extraWindow) { this.extraWindow = await ctx.Lib.openWindow(url); } else { @@ -270,7 +269,7 @@ export class FacebookTimelineBehavior { } async *iterComments( - ctx: Context, + ctx: Context, commentRootUL: HTMLUListElement | null, maxExpands = 2, ) { @@ -280,7 +279,7 @@ export class FacebookTimelineBehavior { return; } let commentBlock = commentRootUL.firstElementChild; - let lastBlock = null; + let lastBlock: Element | null = null; let count = 0; @@ -326,7 +325,7 @@ export class FacebookTimelineBehavior { await sleep(waitUnit * 2); } - async *iterPhotoSlideShow(ctx: Context) { + async *iterPhotoSlideShow(ctx: Context) { const { getState, scrollIntoView, sleep, waitUnit, waitUntil, xpathNode } = ctx.Lib; const firstPhoto = xpathNode(Q.firstPhotoThumbnail) as HTMLElement | null; @@ -343,7 +342,7 @@ export class FacebookTimelineBehavior { await sleep(waitUnit * 5); await waitUntil(() => window.location.href !== lastHref, waitUnit * 2); - let nextSlideButton = null; + let nextSlideButton: HTMLElement | null = null; while ( (nextSlideButton = xpathNode(Q.nextSlideQuery) as HTMLElement | null) @@ -372,7 +371,7 @@ export class FacebookTimelineBehavior { } } - async *iterAllVideos(ctx: Context) { + async *iterAllVideos(ctx: Context) { const { getState, scrollIntoView, @@ -437,7 +436,7 @@ export class FacebookTimelineBehavior { } } - async *run(ctx: Context) { + async *run(ctx: Context) { const { getState, sleep, xpathNode } = ctx.Lib; yield getState(ctx, "Starting..."); @@ -466,7 +465,7 @@ export class FacebookTimelineBehavior { yield* this.iterPostFeeds(ctx); } - async awaitPageLoad(ctx: Context) { + async awaitPageLoad(ctx: Context) { const { Lib, log } = ctx; const { assertContentValid, waitUntilNode } = Lib; diff --git a/src/site/instagram.ts b/src/site/instagram.ts index f6c2ef5..7decfce 100644 --- a/src/site/instagram.ts +++ b/src/site/instagram.ts @@ -1,4 +1,4 @@ -import { type Context } from "../lib/behavior"; +import { type AbstractBehavior, type Context } from "../lib/behavior"; const subpostNextOnlyChevron = "//article[@role='presentation']//div[@role='presentation']/following-sibling::button"; @@ -27,7 +27,9 @@ type InstagramState = { rows: number; }; -export class InstagramPostsBehavior { +export class InstagramPostsBehavior + implements AbstractBehavior +{ maxCommentsTime: number; postOnlyWindow: WindowProxy | null; @@ -61,10 +63,7 @@ export class InstagramPostsBehavior { } } - async waitForNext( - ctx: Context, - child: Element | null, - ) { + async waitForNext(ctx: Context, child: Element | null) { if (!child) { return null; } @@ -78,7 +77,7 @@ export class InstagramPostsBehavior { return child.nextElementSibling; } - async *iterRow(ctx: Context) { + async *iterRow(ctx: Context) { const { RestoreState, sleep, waitUnit, xpathNode } = ctx.Lib; const root = xpathNode(Q.rootPath) as Element | null; @@ -110,10 +109,7 @@ export class InstagramPostsBehavior { } } - async *viewStandalonePost( - ctx: Context, - origLoc: string, - ) { + async *viewStandalonePost(ctx: Context, origLoc: string) { const { getState, sleep, waitUnit, waitUntil, xpathNode, xpathString } = ctx.Lib; const root = xpathNode(Q.rootPath) as HTMLElement | null; @@ -157,7 +153,7 @@ export class InstagramPostsBehavior { //} } - async *iterSubposts(ctx: Context) { + async *iterSubposts(ctx: Context) { const { getState, sleep, waitUnit, xpathNode } = ctx.Lib; let next = xpathNode(Q.subpostNextOnlyChevron) as HTMLElement | null; @@ -179,7 +175,7 @@ export class InstagramPostsBehavior { await sleep(waitUnit * 5); } - async iterComments(ctx: Context) { + async iterComments(ctx: Context) { const { scrollIntoView, sleep, waitUnit, waitUntil, xpathNode } = ctx.Lib; const root = xpathNode(Q.commentRoot) as HTMLElement | null; @@ -236,10 +232,7 @@ export class InstagramPostsBehavior { return commentsLoaded; } - async *iterPosts( - ctx: Context, - next: HTMLElement | null, - ) { + async *iterPosts(ctx: Context, next: HTMLElement | null) { const { getState, sleep, waitUnit, xpathNode } = ctx.Lib; //let count = 0; @@ -263,7 +256,7 @@ export class InstagramPostsBehavior { await sleep(waitUnit * 5); } - async *handleSinglePost(ctx: Context) { + async *handleSinglePost(ctx: Context) { const { getState, sleep } = ctx.Lib; yield* this.iterSubposts(ctx); @@ -273,7 +266,7 @@ export class InstagramPostsBehavior { await Promise.race([this.iterComments(ctx), sleep(this.maxCommentsTime)]); } - async *run(ctx: Context) { + async *run(ctx: Context) { if (window.location.pathname.startsWith("/p/")) { yield* this.handleSinglePost(ctx); return; @@ -304,7 +297,7 @@ export class InstagramPostsBehavior { } } - async awaitPageLoad(ctx: Context) { + async awaitPageLoad(ctx: Context) { const { Lib, log } = ctx; const { assertContentValid, waitUntilNode } = Lib; diff --git a/src/site/telegram.ts b/src/site/telegram.ts index 811a2a4..7c2ecf8 100644 --- a/src/site/telegram.ts +++ b/src/site/telegram.ts @@ -1,4 +1,4 @@ -import { type Context } from "../lib/behavior"; +import { type AbstractBehavior, type Context } from "../lib/behavior"; const Q = { telegramContainer: @@ -9,10 +9,10 @@ const Q = { }; type TelegramState = { - messages: number; + messages?: number; }; -export class TelegramBehavior { +export class TelegramBehavior implements AbstractBehavior { static id = "Telegram"; static isMatch() { @@ -25,10 +25,7 @@ export class TelegramBehavior { }; } - async waitForPrev( - ctx: Context, - child: Element | null, - ) { + async waitForPrev(ctx: Context, child: Element | null) { if (!child) { return null; } @@ -42,7 +39,7 @@ export class TelegramBehavior { return child.previousElementSibling; } - async *run(ctx: Context) { + async *run(ctx: Context) { const { getState, scrollIntoView, diff --git a/src/site/tiktok.ts b/src/site/tiktok.ts index dde00c8..cbae87b 100644 --- a/src/site/tiktok.ts +++ b/src/site/tiktok.ts @@ -1,4 +1,4 @@ -import { type Context } from "../lib/behavior"; +import { type AbstractBehavior, type Context } from "../lib/behavior"; const Q = { commentList: "//div[contains(@class, 'CommentListContainer')]", @@ -34,7 +34,10 @@ export class TikTokSharedBehavior { } } -export class TikTokVideoBehavior extends TikTokSharedBehavior { +export class TikTokVideoBehavior + extends TikTokSharedBehavior + implements AbstractBehavior +{ static id = "TikTokVideo"; static init() { @@ -100,7 +103,10 @@ export class TikTokVideoBehavior extends TikTokSharedBehavior { } } -export class TikTokProfileBehavior extends TikTokSharedBehavior { +export class TikTokProfileBehavior + extends TikTokSharedBehavior + implements AbstractBehavior +{ static id = "TikTokProfile"; static isMatch() { diff --git a/src/site/twitter.ts b/src/site/twitter.ts index 4e5ee74..79fe422 100644 --- a/src/site/twitter.ts +++ b/src/site/twitter.ts @@ -1,4 +1,4 @@ -import { type Context } from "../lib/behavior"; +import { type AbstractBehavior, type Context } from "../lib/behavior"; const Q = { rootPath: @@ -24,18 +24,20 @@ const Q = { promoted: ".//div[data-testid='placementTracking']", }; -type TwitterState = Partial<{ - tweets: number; - images: number; - videos: number; - threads: number; -}>; +type TwitterState = { + tweets?: number; + images?: number; + videos?: number; + threads?: number; +}; type TwitterOpts = { maxDepth: number; }; -export class TwitterTimelineBehavior { +export class TwitterTimelineBehavior + implements AbstractBehavior +{ seenTweets: Set; seenMediaTweets: Set; From 684d40ac8fcc1fb6f5ff8e1d56c419be798ecce6 Mon Sep 17 00:00:00 2001 From: emma Date: Wed, 27 Aug 2025 17:21:57 -0400 Subject: [PATCH 4/9] wip --- src/autoclick.ts | 2 +- src/autofetcher.ts | 2 +- src/autoplay.ts | 2 +- src/autoscroll.ts | 61 ++++++++++++++++++++++++++----------------- src/index.ts | 38 ++++++++++++++++++++------- src/lib/behavior.ts | 20 +++++++------- src/lib/global.d.ts | 11 ++++++++ src/lib/utils.ts | 9 +++---- src/site/facebook.ts | 2 +- src/site/index.ts | 2 +- src/site/instagram.ts | 2 +- src/site/telegram.ts | 2 +- src/site/tiktok.ts | 4 +-- src/site/twitter.ts | 2 +- src/site/youtube.ts | 3 ++- 15 files changed, 101 insertions(+), 61 deletions(-) diff --git a/src/autoclick.ts b/src/autoclick.ts index 30829d9..f0f6ab5 100644 --- a/src/autoclick.ts +++ b/src/autoclick.ts @@ -7,7 +7,7 @@ export class AutoClick extends BackgroundBehavior { selector: string; seenElem = new WeakSet(); - static id = "Autoclick"; + static id = "Autoclick" as const; constructor(selector = "a") { super(); diff --git a/src/autofetcher.ts b/src/autofetcher.ts index 8a6e3e1..dad68cd 100644 --- a/src/autofetcher.ts +++ b/src/autofetcher.ts @@ -37,7 +37,7 @@ export class AutoFetcher extends BackgroundBehavior { active: boolean; running = false; - static id = "Autofetcher"; + static id = "Autofetcher" as const; constructor( active = false, diff --git a/src/autoplay.ts b/src/autoplay.ts index 22d208e..7dab36d 100644 --- a/src/autoplay.ts +++ b/src/autoplay.ts @@ -14,7 +14,7 @@ export class Autoplay extends BackgroundBehavior { running = false; polling = false; - static id = "Autoplay"; + static id = "Autoplay" as const; constructor(autofetcher: AutoFetcher, startEarly = false) { super(); diff --git a/src/autoscroll.ts b/src/autoscroll.ts index 7006897..ca264f2 100644 --- a/src/autoscroll.ts +++ b/src/autoscroll.ts @@ -1,4 +1,9 @@ -import { Behavior } from "./lib/behavior"; +import { + type AbstractBehavior, + BackgroundBehavior, + type Context, + type EmptyObject, +} from "./lib/behavior"; import { sleep, waitUnit, @@ -7,11 +12,15 @@ import { waitUntil, behaviorLog, addLink, + getState, } from "./lib/utils"; import { type AutoFetcher } from "./autofetcher"; // =========================================================================== -export class AutoScroll extends Behavior<{}, {}> { +export class AutoScroll + extends BackgroundBehavior + implements AbstractBehavior +{ autoFetcher: AutoFetcher; showMoreQuery: string; state: { segments: number } = { segments: 1 }; @@ -35,7 +44,7 @@ export class AutoScroll extends Behavior<{}, {}> { this.origPath = document.location.pathname; } - static id = "Autoscroll"; + static id = "Autoscroll" as const; currScrollPos() { return Math.round(self.scrollY + self.innerHeight); @@ -111,6 +120,7 @@ export class AutoScroll extends Behavior<{}, {}> { } if ( + // @ts-expect-error TODO not sure what self.scrollHeight is here (self.window.scrollY + self["scrollHeight"]) / self.document.scrollingElement!.scrollHeight < 0.9 @@ -121,48 +131,49 @@ export class AutoScroll extends Behavior<{}, {}> { return true; } - async *[Symbol.asyncIterator]() { + async *run(ctx: Context) { if (this.shouldScrollUp()) { - yield* this.scrollUp(); + yield* this.scrollUp(ctx); return; } if (await this.shouldScroll()) { - yield* this.scrollDown(); + yield* this.scrollDown(ctx); return; } - yield this.getState( + yield getState( + ctx, "Skipping autoscroll, page seems to not be responsive to scrolling events", ); } - async *scrollDown() { + async *scrollDown(ctx: Context) { const scrollInc = Math.min( - self.document.scrollingElement.clientHeight * 0.1, + self.document.scrollingElement!.clientHeight * 0.1, 30, ); const interval = 75; let elapsedWait = 0; - let showMoreElem = null; + let showMoreElem: HTMLElement | null = null; let ignoreShowMoreElem = false; const scrollOpts = { top: scrollInc, left: 0, behavior: "auto" }; - let lastScrollHeight = self.document.scrollingElement.scrollHeight; + let lastScrollHeight = self.document.scrollingElement!.scrollHeight; while (this.canScrollMore()) { if (document.location.pathname !== this.origPath) { - behaviorLog( + void behaviorLog( "Location Changed, stopping scroll: " + `${document.location.pathname} != ${this.origPath}`, "info", ); - addLink(document.location.href); + void addLink(document.location.href); return; } - const scrollHeight = self.document.scrollingElement.scrollHeight; + const scrollHeight = self.document.scrollingElement!.scrollHeight; if (scrollHeight > lastScrollHeight) { this.state.segments++; @@ -170,24 +181,24 @@ export class AutoScroll extends Behavior<{}, {}> { } if (!showMoreElem && !ignoreShowMoreElem) { - showMoreElem = xpathNode(this.showMoreQuery); + showMoreElem = xpathNode(this.showMoreQuery) as HTMLElement | null; } if (showMoreElem && isInViewport(showMoreElem)) { - yield this.getState("Clicking 'Show More', awaiting more content"); + yield getState(ctx, "Clicking 'Show More', awaiting more content"); showMoreElem["click"](); await sleep(waitUnit); await Promise.race([ waitUntil( - () => self.document.scrollingElement.scrollHeight > scrollHeight, + () => self.document.scrollingElement!.scrollHeight > scrollHeight, 500, ), sleep(30000), ]); - if (self.document.scrollingElement.scrollHeight === scrollHeight) { + if (self.document.scrollingElement!.scrollHeight === scrollHeight) { ignoreShowMoreElem = true; } @@ -200,7 +211,8 @@ export class AutoScroll extends Behavior<{}, {}> { if (this.state.segments === 1) { // only print this the first time - yield this.getState( + yield getState( + ctx, `Scrolling down by ${scrollOpts.top} pixels every ${interval / 1000.0} seconds`, ); elapsedWait = 2.0; @@ -236,19 +248,19 @@ export class AutoScroll extends Behavior<{}, {}> { } } - async *scrollUp() { + async *scrollUp(ctx: Context) { const scrollInc = Math.min( - self.document.scrollingElement.clientHeight * 0.1, + self.document.scrollingElement!.clientHeight * 0.1, 30, ); const interval = 75; const scrollOpts = { top: -scrollInc, left: 0, behavior: "auto" }; - let lastScrollHeight = self.document.scrollingElement.scrollHeight; + let lastScrollHeight = self.document.scrollingElement!.scrollHeight; while (self.scrollY > 0) { - const scrollHeight = self.document.scrollingElement.scrollHeight; + const scrollHeight = self.document.scrollingElement!.scrollHeight; if (scrollHeight > lastScrollHeight) { this.state.segments++; @@ -261,7 +273,8 @@ export class AutoScroll extends Behavior<{}, {}> { if (this.state.segments === 1) { // only print this the first time - yield this.getState( + yield getState( + ctx, `Scrolling up by ${scrollOpts.top} pixels every ${interval / 1000.0} seconds`, ); } else { diff --git a/src/index.ts b/src/index.ts index df36a38..7fe2f35 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,7 +12,11 @@ import { addLink, checkToJsonOverride, } from "./lib/utils"; -import { AbstractBehavior, type Behavior, BehaviorRunner } from "./lib/behavior"; +import { + type AbstractBehavior, + type Behavior, + BehaviorRunner, +} from "./lib/behavior"; import * as Lib from "./lib/utils"; import siteBehaviors from "./site"; @@ -54,14 +58,24 @@ const DEFAULT_CLICK_SELECTOR = "a"; const DEFAULT_LINK_SELECTOR = "a[href]"; const DEFAULT_LINK_EXTRACT = "href"; -type TODOBehaviorClass = AbstractBehavior; +type BehaviorClass = + | (typeof siteBehaviors)[number] + | typeof AutoClick + | typeof AutoScroll + | typeof Autoplay + | typeof AutoFetcher; + +type BehaviorInstance = InstanceType; +type SiteSpecificBehaviorInstance = InstanceType< + (typeof siteBehaviors)[number] +>; export class BehaviorManager { autofetch?: AutoFetcher; - behaviors: TODOBehaviorClass[] | null; - loadedBehaviors: TODOBehaviorClass; - mainBehavior: Behavior | BehaviorRunner | null; - mainBehaviorClass: TODOBehaviorClass; + behaviors: BehaviorInstance[] | null; + loadedBehaviors: { [key in BehaviorClass["id"]]: BehaviorClass }; + mainBehavior: Behavior | BehaviorRunner | null; + mainBehaviorClass!: BehaviorClass; inited: boolean; started: boolean; timeout?: number; @@ -70,7 +84,9 @@ export class BehaviorManager { constructor() { this.behaviors = []; - this.loadedBehaviors = siteBehaviors.reduce((behaviors, next) => { + this.loadedBehaviors = siteBehaviors.reduce< + Record + >((behaviors, next) => { behaviors[next.id] = next; return behaviors; }, {}); @@ -87,7 +103,7 @@ export class BehaviorManager { init( opts: BehaviorManagerOpts = DEFAULT_OPTS, restart = false, - customBehaviors: TODOBehaviorClass[] | null = null, + customBehaviors: BehaviorClass[] | null = null, ) { if (this.inited && !restart) { return; @@ -165,7 +181,11 @@ export class BehaviorManager { if (opts?.siteSpecific) { for (const name in this.loadedBehaviors) { const siteBehaviorClass = this.loadedBehaviors[name]; - if (siteBehaviorClass.isMatch()) { + if ( + ( + siteBehaviorClass as unknown as SiteSpecificBehaviorInstance + ).isMatch() + ) { void behaviorLog("Using Site-Specific Behavior: " + name); this.mainBehaviorClass = siteBehaviorClass; const siteSpecificOpts = diff --git a/src/lib/behavior.ts b/src/lib/behavior.ts index 010ec61..4e89dbe 100644 --- a/src/lib/behavior.ts +++ b/src/lib/behavior.ts @@ -17,10 +17,7 @@ export class BackgroundBehavior { } // =========================================================================== -export class Behavior - extends BackgroundBehavior - implements AbstractBehavior -{ +export class Behavior extends BackgroundBehavior { _running: Promise | null; paused: any; _unpause: any; @@ -108,11 +105,7 @@ export class Behavior } } - async *[Symbol.asyncIterator](): AsyncGenerator< - State | undefined, - void, - void - > { + async *[Symbol.asyncIterator]() { yield; } } @@ -130,11 +123,15 @@ export type Context = { }; export abstract class AbstractBehavior { - static id: String; + static readonly id: string; static isMatch: () => boolean; static init: () => any; - abstract run: (ctx: Context) => AsyncIterable; + abstract run: ( + ctx: Context, + ) => AsyncIterable<{ state?: State }> | Promise; + + abstract [Symbol.asyncIterator]?(): AsyncIterable; abstract awaitPageLoad?: (ctx: Context) => Promise; } @@ -218,6 +215,7 @@ export class BehaviorRunner< async run() { try { + // @ts-expect-error TODO how does this work for behaviors where `run` isn't an iterator, e.g. Autoplay and Autoscroll? for await (const step of this.inst.run(this.ctx)) { if (step) { this.wrappedLog(step); diff --git a/src/lib/global.d.ts b/src/lib/global.d.ts index cd1373e..dbfb43a 100644 --- a/src/lib/global.d.ts +++ b/src/lib/global.d.ts @@ -26,6 +26,17 @@ declare global { interface WorkerGlobalScope extends BehaviorGlobals {} interface Window extends BehaviorGlobals { __WB_replay_top?: Window; + + /** + * Chrome DevTools’s `getEventListeners` API + * @see https://developer.chrome.com/docs/devtools/console/utilities/#getEventListeners-function + */ + getEventListeners: ( + obj: Obj, + ) => Record< + Obj extends Window ? keyof WindowEventMap : string, + EventListenerOrEventListenerObject[] + >; } interface HTMLVideoElement extends AutoplayProperties {} diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 11d83ed..1c24535 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -3,8 +3,7 @@ import { type Context } from "./behavior"; // eslint-disable-next-line @typescript-eslint/no-explicit-any let _logFunc: ((...data: any[]) => void) | null = console.log; -// @ts-expect-error TODO: what is this, and why is it declared twice, once here and once as a function? -let _behaviorMgrClass: (cls: typeof BehaviorManager) => void | null = null; +let _behaviorMgrClass: typeof BehaviorManager | null = null; const scrollOpts: ScrollIntoViewOptions = { behavior: "smooth", @@ -232,14 +231,12 @@ export function _setLogFunc( _logFunc = func; } -// @ts-expect-error TODO: why is this declared over the `_behaviorMgrClass` declared earlier? -export function _behaviorMgrClass(cls: typeof BehaviorManager) { - // @ts-expect-error TODO: `_behaviorMgrClass` is a function, is this trying to overwrite it? +export function _setBehaviorManager(cls: typeof BehaviorManager) { _behaviorMgrClass = cls; } export function installBehaviors(obj: Window | WorkerGlobalScope) { - obj.__bx_behaviors = new _behaviorMgrClass(); + obj.__bx_behaviors = new _behaviorMgrClass!(); } // =========================================================================== diff --git a/src/site/facebook.ts b/src/site/facebook.ts index 74d1ae6..d1a0290 100644 --- a/src/site/facebook.ts +++ b/src/site/facebook.ts @@ -50,7 +50,7 @@ export class FacebookTimelineBehavior extraWindow: WindowProxy | null; allowNewWindow: boolean; - static id = "Facebook"; + static id = "Facebook" as const; static isMatch() { // match just for posts for now diff --git a/src/site/index.ts b/src/site/index.ts index 275864b..2629ea9 100644 --- a/src/site/index.ts +++ b/src/site/index.ts @@ -11,6 +11,6 @@ const siteBehaviors = [ TelegramBehavior, TikTokVideoBehavior, TikTokProfileBehavior, -]; +] as const; export default siteBehaviors; diff --git a/src/site/instagram.ts b/src/site/instagram.ts index 7decfce..e422be6 100644 --- a/src/site/instagram.ts +++ b/src/site/instagram.ts @@ -33,7 +33,7 @@ export class InstagramPostsBehavior maxCommentsTime: number; postOnlyWindow: WindowProxy | null; - static id = "Instagram"; + static id = "Instagram" as const; static isMatch() { return !!window.location.href.match(/https:\/\/(www\.)?instagram\.com\//); diff --git a/src/site/telegram.ts b/src/site/telegram.ts index 7c2ecf8..510850b 100644 --- a/src/site/telegram.ts +++ b/src/site/telegram.ts @@ -13,7 +13,7 @@ type TelegramState = { }; export class TelegramBehavior implements AbstractBehavior { - static id = "Telegram"; + static id = "Telegram" as const; static isMatch() { return !!window.location.href.match(/https:\/\/t.me\/s\/\w[\w]+/); diff --git a/src/site/tiktok.ts b/src/site/tiktok.ts index cbae87b..b01a9c6 100644 --- a/src/site/tiktok.ts +++ b/src/site/tiktok.ts @@ -38,7 +38,7 @@ export class TikTokVideoBehavior extends TikTokSharedBehavior implements AbstractBehavior { - static id = "TikTokVideo"; + static id = "TikTokVideo" as const; static init() { return { @@ -107,7 +107,7 @@ export class TikTokProfileBehavior extends TikTokSharedBehavior implements AbstractBehavior { - static id = "TikTokProfile"; + static id = "TikTokProfile" as const; static isMatch() { const pathRegex = diff --git a/src/site/twitter.ts b/src/site/twitter.ts index 79fe422..eb801a2 100644 --- a/src/site/twitter.ts +++ b/src/site/twitter.ts @@ -41,7 +41,7 @@ export class TwitterTimelineBehavior seenTweets: Set; seenMediaTweets: Set; - static id = "Twitter"; + static id = "Twitter" as const; static isMatch() { return !!window.location.href.match(/https:\/\/(www\.)?(x|twitter)\.com\//); diff --git a/src/site/youtube.ts b/src/site/youtube.ts index a1a6dce..2874951 100644 --- a/src/site/youtube.ts +++ b/src/site/youtube.ts @@ -2,7 +2,8 @@ import { AutoScroll } from "../autoscroll"; import { type Context } from "../lib/behavior"; export class YoutubeBehavior extends AutoScroll { - override async awaitPageLoad(ctx: Context<{}, {}>) { + static id = "Youtube" as const; + async awaitPageLoad(ctx: Context<{}, {}>) { const { sleep, assertContentValid } = ctx.Lib; await sleep(10); assertContentValid(() => { From 1d6e853795403a82b9794200c225536ca3749bbc Mon Sep 17 00:00:00 2001 From: emma Date: Wed, 27 Aug 2025 17:52:27 -0400 Subject: [PATCH 5/9] wip --- src/index.ts | 91 +++++++++++++++++++++++---------------------- src/lib/behavior.ts | 8 ++-- 2 files changed, 50 insertions(+), 49 deletions(-) diff --git a/src/index.ts b/src/index.ts index 7fe2f35..fa4ac4c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,11 +12,7 @@ import { addLink, checkToJsonOverride, } from "./lib/utils"; -import { - type AbstractBehavior, - type Behavior, - BehaviorRunner, -} from "./lib/behavior"; +import { type AbstractBehavior, BehaviorRunner } from "./lib/behavior"; import * as Lib from "./lib/utils"; import siteBehaviors from "./site"; @@ -63,18 +59,17 @@ type BehaviorClass = | typeof AutoClick | typeof AutoScroll | typeof Autoplay - | typeof AutoFetcher; + | typeof AutoFetcher + | typeof BehaviorRunner; type BehaviorInstance = InstanceType; -type SiteSpecificBehaviorInstance = InstanceType< - (typeof siteBehaviors)[number] ->; export class BehaviorManager { autofetch?: AutoFetcher; behaviors: BehaviorInstance[] | null; - loadedBehaviors: { [key in BehaviorClass["id"]]: BehaviorClass }; - mainBehavior: Behavior | BehaviorRunner | null; + loadedBehaviors: Record; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + mainBehavior: BehaviorInstance | BehaviorRunner | null; mainBehaviorClass!: BehaviorClass; inited: boolean; started: boolean; @@ -84,12 +79,13 @@ export class BehaviorManager { constructor() { this.behaviors = []; - this.loadedBehaviors = siteBehaviors.reduce< - Record - >((behaviors, next) => { - behaviors[next.id] = next; - return behaviors; - }, {}); + this.loadedBehaviors = siteBehaviors.reduce>( + (behaviors, next) => { + behaviors[next.id] = next; + return behaviors; + }, + {}, + ); this.mainBehavior = null; this.inited = false; this.started = false; @@ -181,11 +177,7 @@ export class BehaviorManager { if (opts?.siteSpecific) { for (const name in this.loadedBehaviors) { const siteBehaviorClass = this.loadedBehaviors[name]; - if ( - ( - siteBehaviorClass as unknown as SiteSpecificBehaviorInstance - ).isMatch() - ) { + if ("isMatch" in siteBehaviorClass && siteBehaviorClass.isMatch()) { void behaviorLog("Using Site-Specific Behavior: " + name); this.mainBehaviorClass = siteBehaviorClass; const siteSpecificOpts = @@ -209,14 +201,14 @@ export class BehaviorManager { } } - if (!siteMatch && opts.autoscroll) { - behaviorLog("Using Autoscroll"); + if (!siteMatch && opts?.autoscroll) { + void behaviorLog("Using Autoscroll"); this.mainBehaviorClass = AutoScroll; - this.mainBehavior = new AutoScroll(this.autofetch); + this.mainBehavior = new AutoScroll(this.autofetch!); } if (this.mainBehavior) { - this.behaviors.push(this.mainBehavior); + this.behaviors!.push(this.mainBehavior); if (this.mainBehavior instanceof BehaviorRunner) { return this.mainBehavior.behaviorProps.id; @@ -226,9 +218,16 @@ export class BehaviorManager { return ""; } - load(behaviorClass) { - if (typeof behaviorClass.id !== "string") { - behaviorLog( + load(behaviorClass: unknown) { + if (typeof behaviorClass !== "function") { + void behaviorLog( + `Must pass a class object, got ${behaviorClass}`, + "error", + ); + return; + } + if (!("id" in behaviorClass) || typeof behaviorClass.id !== "string") { + void behaviorLog( 'Behavior class must have a string string "id" property', "error", ); @@ -237,16 +236,13 @@ export class BehaviorManager { const name = behaviorClass.id; - if (typeof behaviorClass !== "function") { - behaviorLog(`Must pass a class object, got ${behaviorClass}`, "error"); - return; - } - if ( + !("isMatch" in behaviorClass) || typeof behaviorClass.isMatch !== "function" || + !("init" in behaviorClass) || typeof behaviorClass.init !== "function" ) { - behaviorLog( + void behaviorLog( "Behavior class must have an is `isMatch()` and `init()` static methods", "error", ); @@ -254,8 +250,8 @@ export class BehaviorManager { } if (!this.isInTopFrame()) { - if (!behaviorClass.runInIframe) { - behaviorLog( + if (!("runInIframe" in behaviorClass) || !behaviorClass.runInIframe) { + void behaviorLog( `Behavior class ${name}: not running in iframes (.runInIframe not set)`, "debug", ); @@ -263,11 +259,11 @@ export class BehaviorManager { } } - behaviorLog(`Behavior class ${name}: loaded`, "debug"); - this.loadedBehaviors[name] = behaviorClass; + void behaviorLog(`Behavior class ${name}: loaded`, "debug"); + this.loadedBehaviors[name] = behaviorClass as BehaviorClass; } - async resolve(target) { + async resolve(target: string) { const imported = await import(`${target}`); // avoid Webpack warning if (Array.isArray(imported)) { for (const behavior of imported) { @@ -280,11 +276,16 @@ export class BehaviorManager { async awaitPageLoad() { this.selectMainBehavior(); - if (this.mainBehavior?.awaitPageLoad) { - behaviorLog("Waiting for custom page load via behavior"); + if ( + this.mainBehavior && + "awaitPageLoad" in this.mainBehavior && + (this.mainBehavior as AbstractBehavior).awaitPageLoad + ) { + void behaviorLog("Waiting for custom page load via behavior"); + // @ts-expect-error TODO why isn't `log` passed in here? It seems like functions expect it to be await this.mainBehavior.awaitPageLoad({ Lib }); } else { - behaviorLog("No custom wait behavior"); + void behaviorLog("No custom wait behavior"); } } @@ -303,7 +304,7 @@ export class BehaviorManager { await awaitLoad(); - this.behaviors.forEach((x) => { + this.behaviors!.forEach((x) => { const id = x.id || x.constructor.id || "(Unnamed)"; behaviorLog("Starting behavior: " + id, "debug"); x.start(); @@ -318,7 +319,7 @@ export class BehaviorManager { ); if (this.timeout) { - behaviorLog( + void behaviorLog( `Waiting for behaviors to finish or ${this.timeout}ms timeout`, "debug", ); diff --git a/src/lib/behavior.ts b/src/lib/behavior.ts index 4e89dbe..2e8b3ab 100644 --- a/src/lib/behavior.ts +++ b/src/lib/behavior.ts @@ -147,10 +147,10 @@ type ConcreteBehaviorConstructor = StaticBehaviorProps & { new (): AbstractBehavior; }; -export class BehaviorRunner< - State, - Opts = EmptyObject, -> extends BackgroundBehavior { +export class BehaviorRunner + extends BackgroundBehavior + implements AbstractBehavior +{ inst: AbstractBehavior; behaviorProps: ConcreteBehaviorConstructor; ctx: Context; From 11a65ecaeff1c7cf87245bcebf04166e4896571b Mon Sep 17 00:00:00 2001 From: emma Date: Wed, 27 Aug 2025 23:36:11 -0400 Subject: [PATCH 6/9] fix remaining type & lint issues --- src/autoplay.ts | 2 +- src/autoscroll.ts | 6 +++-- src/index.ts | 60 +++++++++++++++++++++++++++------------------ src/lib/behavior.ts | 37 +++++++++++++++++----------- src/lib/global.d.ts | 4 ++- src/lib/utils.ts | 7 +++--- src/site/youtube.ts | 2 +- 7 files changed, 72 insertions(+), 46 deletions(-) diff --git a/src/autoplay.ts b/src/autoplay.ts index 7dab36d..54d89a8 100644 --- a/src/autoplay.ts +++ b/src/autoplay.ts @@ -9,7 +9,7 @@ export class Autoplay extends BackgroundBehavior { mediaSet: Set; autofetcher: AutoFetcher; numPlaying: number; - promises: Promise[]; + promises: Promise[]; _initDone: Function; running = false; polling = false; diff --git a/src/autoscroll.ts b/src/autoscroll.ts index ca264f2..abfe323 100644 --- a/src/autoscroll.ts +++ b/src/autoscroll.ts @@ -44,7 +44,9 @@ export class AutoScroll this.origPath = document.location.pathname; } - static id = "Autoscroll" as const; + static get id() { + return "Autoscroll"; + } currScrollPos() { return Math.round(self.scrollY + self.innerHeight); @@ -68,7 +70,7 @@ export class AutoScroll hasScrollEL(obj: HTMLElement | Document | Window) { try { - return !!self["getEventListeners"](obj).scroll; + return !!self["getEventListeners"]!(obj).scroll; } catch (_) { // unknown, assume has listeners this.debug("getEventListeners() not available"); diff --git a/src/index.ts b/src/index.ts index fa4ac4c..b95c94c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -60,13 +60,14 @@ type BehaviorClass = | typeof AutoScroll | typeof Autoplay | typeof AutoFetcher + // eslint-disable-next-line @typescript-eslint/no-explicit-any | typeof BehaviorRunner; type BehaviorInstance = InstanceType; export class BehaviorManager { autofetch?: AutoFetcher; - behaviors: BehaviorInstance[] | null; + behaviors: BehaviorInstance[]; loadedBehaviors: Record; // eslint-disable-next-line @typescript-eslint/no-explicit-any mainBehavior: BehaviorInstance | BehaviorRunner | null; @@ -139,17 +140,17 @@ export class BehaviorManager { if (opts.autofetch) { void behaviorLog("Using AutoFetcher"); - this.behaviors!.push(this.autofetch); + this.behaviors.push(this.autofetch); } if (opts.autoplay) { void behaviorLog("Using Autoplay"); - this.behaviors!.push(new Autoplay(this.autofetch, !!opts.startEarly)); + this.behaviors.push(new Autoplay(this.autofetch, !!opts.startEarly)); } if (opts.autoclick) { void behaviorLog("Using AutoClick"); - this.behaviors!.push( + this.behaviors.push( new AutoClick(opts.clickSelector || DEFAULT_CLICK_SELECTOR), ); } @@ -160,6 +161,7 @@ export class BehaviorManager { this.load(behaviorClass); } catch (e) { void behaviorLog( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions `Failed to load custom behavior: ${e} ${behaviorClass}`, ); } @@ -186,6 +188,7 @@ export class BehaviorManager { : {}; try { this.mainBehavior = new BehaviorRunner( + // @ts-expect-error TODO figure out types here siteBehaviorClass, siteSpecificOpts, ); @@ -208,7 +211,7 @@ export class BehaviorManager { } if (this.mainBehavior) { - this.behaviors!.push(this.mainBehavior); + this.behaviors.push(this.mainBehavior); if (this.mainBehavior instanceof BehaviorRunner) { return this.mainBehavior.behaviorProps.id; @@ -221,6 +224,7 @@ export class BehaviorManager { load(behaviorClass: unknown) { if (typeof behaviorClass !== "function") { void behaviorLog( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions `Must pass a class object, got ${behaviorClass}`, "error", ); @@ -279,7 +283,7 @@ export class BehaviorManager { if ( this.mainBehavior && "awaitPageLoad" in this.mainBehavior && - (this.mainBehavior as AbstractBehavior).awaitPageLoad + (this.mainBehavior as AbstractBehavior).awaitPageLoad ) { void behaviorLog("Waiting for custom page load via behavior"); // @ts-expect-error TODO why isn't `log` passed in here? It seems like functions expect it to be @@ -304,10 +308,13 @@ export class BehaviorManager { await awaitLoad(); - this.behaviors!.forEach((x) => { - const id = x.id || x.constructor.id || "(Unnamed)"; - behaviorLog("Starting behavior: " + id, "debug"); - x.start(); + this.behaviors.forEach((x) => { + const id = + (x as unknown as typeof AbstractBehavior).id || + (x.constructor as unknown as typeof AbstractBehavior).id || + "(Unnamed)"; + void behaviorLog("Starting behavior: " + id, "debug"); + "start" in x && void x.start(); }); this.started = true; @@ -315,7 +322,7 @@ export class BehaviorManager { await sleep(500); const allBehaviors = Promise.allSettled( - this.behaviors.map((x) => x.done()), + this.behaviors.map(async (x) => "done" in x && x.done()), ); if (this.timeout) { @@ -325,25 +332,29 @@ export class BehaviorManager { ); await Promise.race([allBehaviors, sleep(this.timeout)]); } else { - behaviorLog("Waiting for behaviors to finish", "debug"); + void behaviorLog("Waiting for behaviors to finish", "debug"); await allBehaviors; } - behaviorLog("All Behaviors Done for " + self.location.href, "debug"); + void behaviorLog("All Behaviors Done for " + self.location.href, "debug"); - if (this.mainBehavior && this.mainBehaviorClass.cleanup) { + if (this.mainBehavior && "cleanup" in this.mainBehavior) { this.mainBehavior.cleanup(); } } - async runOne(name, behaviorOpts = {}) { + async runOne(name: string, behaviorOpts = {}) { const siteBehaviorClass = siteBehaviors.find((b) => b.name === name); if (typeof siteBehaviorClass === "undefined") { console.error(`No behavior of name ${name} found`); return; } //const behavior = new siteBehaviorClass(behaviorOpts); - const behavior = new BehaviorRunner(siteBehaviorClass, behaviorOpts); + const behavior = new BehaviorRunner( + // @ts-expect-error TODO figure out types here + siteBehaviorClass, + behaviorOpts, + ); behavior.start(); console.log(`Running behavior: ${name}`); await behavior.done(); @@ -351,18 +362,18 @@ export class BehaviorManager { } pause() { - behaviorLog("Pausing Main Behavior" + this.mainBehaviorClass.name); - this.behaviors.forEach((x) => x.pause()); + void behaviorLog("Pausing Main Behavior" + this.mainBehaviorClass.name); + this.behaviors.forEach((x) => "pause" in x && x.pause()); } unpause() { // behaviorLog("Unpausing Main Behavior: " + this.mainBehaviorClass.name); - this.behaviors.forEach((x) => x.unpause()); + this.behaviors.forEach((x) => "pause" in x && x.unpause()); } - doAsyncFetch(url) { - behaviorLog("Queueing Async Fetch Url: " + url); - return this.autofetch.queueUrl(url, true); + doAsyncFetch(url: string) { + void behaviorLog("Queueing Async Fetch Url: " + url); + return this.autofetch!.queueUrl(url, true); } isInTopFrame() { @@ -391,9 +402,10 @@ export class BehaviorManager { const urls = new Set(); - document.querySelectorAll(selector).forEach((elem: any) => { + document.querySelectorAll(selector).forEach((elem) => { // first, try property, unless attrOnly is set - let value = !attrOnly ? elem[extractName] : null; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let value = !attrOnly ? (elem as any)[extractName] : null; if (!value) { value = elem.getAttribute(extractName); } diff --git a/src/lib/behavior.ts b/src/lib/behavior.ts index 2e8b3ab..3163af7 100644 --- a/src/lib/behavior.ts +++ b/src/lib/behavior.ts @@ -17,11 +17,11 @@ export class BackgroundBehavior { } // =========================================================================== -export class Behavior extends BackgroundBehavior { +export class Behavior extends BackgroundBehavior { _running: Promise | null; - paused: any; - _unpause: any; - state: any; + paused: Promise | null; + _unpause: (() => void) | null; + state: Partial; scrollOpts: { behavior: string; block: string; @@ -77,12 +77,15 @@ export class Behavior extends BackgroundBehavior { } } - getState(msg: string, incrValue?: string) { + getState>( + msg: string, + incrValue?: IncrKey, + ) { if (incrValue) { if (this.state[incrValue] === undefined) { - this.state[incrValue] = 1; + (this.state[incrValue] as number) = 1; } else { - this.state[incrValue]++; + (this.state[incrValue] as number)++; } } @@ -91,7 +94,7 @@ export class Behavior extends BackgroundBehavior { cleanup() {} - async awaitPageLoad(_: any) { + async awaitPageLoad() { // wait for initial page load here } @@ -119,13 +122,19 @@ export type Context = { Lib: typeof Lib; state: State; opts: Opts; - log: (data: any, type?: string) => Promise; + log: (data: unknown, type?: string) => Promise; }; export abstract class AbstractBehavior { static readonly id: string; static isMatch: () => boolean; - static init: () => any; + static init: () => { + // TODO: type these + // eslint-disable-next-line @typescript-eslint/no-explicit-any + state?: any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + opts?: any; + }; abstract run: ( ctx: Context, @@ -154,9 +163,9 @@ export class BehaviorRunner inst: AbstractBehavior; behaviorProps: ConcreteBehaviorConstructor; ctx: Context; - _running: any; - paused: any; - _unpause: any; + _running: Promise | null; + paused: Promise | (() => Promise) | null; + _unpause: ((value: void | PromiseLike) => void) | null; get id() { return (this.inst.constructor as ConcreteBehaviorConstructor) @@ -209,7 +218,7 @@ export class BehaviorRunner this._running = this.run(); } - done() { + async done() { return this._running ? this._running : Promise.resolve(); } diff --git a/src/lib/global.d.ts b/src/lib/global.d.ts index dbfb43a..4d09c5a 100644 --- a/src/lib/global.d.ts +++ b/src/lib/global.d.ts @@ -10,7 +10,9 @@ interface BehaviorGlobals { idleTime: number; concurrency: number; }) => Promise; + // eslint-disable-next-line @typescript-eslint/no-explicit-any __bx_initFlow?: (params: any) => Promise; + // eslint-disable-next-line @typescript-eslint/no-explicit-any __bx_nextFlowStep?: (id: number) => Promise; __bx_contentCheckFailed?: (reason: string) => void; __bx_open?: (params: { url: string | URL }) => Promise; @@ -31,7 +33,7 @@ declare global { * Chrome DevTools’s `getEventListeners` API * @see https://developer.chrome.com/docs/devtools/console/utilities/#getEventListeners-function */ - getEventListeners: ( + getEventListeners?: ( obj: Obj, ) => Record< Obj extends Window ? keyof WindowEventMap : string, diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 1c24535..3633972 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,8 +1,7 @@ import { type BehaviorManager } from ".."; import { type Context } from "./behavior"; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -let _logFunc: ((...data: any[]) => void) | null = console.log; +let _logFunc: ((...data: unknown[]) => void) | null = console.log; let _behaviorMgrClass: typeof BehaviorManager | null = null; const scrollOpts: ScrollIntoViewOptions = { @@ -173,6 +172,7 @@ export async function waitForNetworkIdle(idleTime = 500, concurrency = 0) { } } +// eslint-disable-next-line @typescript-eslint/no-explicit-any export async function initFlow(params: any): Promise { if (typeof self["__bx_initFlow"] === "function") { return await callBinding(self["__bx_initFlow"], params); @@ -181,6 +181,7 @@ export async function initFlow(params: any): Promise { return -1; } +// eslint-disable-next-line @typescript-eslint/no-explicit-any export async function nextFlowStep(id: number): Promise { if (typeof self["__bx_nextFlowStep"] === "function") { return await callBinding(self["__bx_nextFlowStep"], id); @@ -228,7 +229,7 @@ export async function openWindow( export function _setLogFunc( func: ((message: string, level: string) => void) | null, ) { - _logFunc = func; + _logFunc = func as (...data: unknown[]) => void; } export function _setBehaviorManager(cls: typeof BehaviorManager) { diff --git a/src/site/youtube.ts b/src/site/youtube.ts index 2874951..0fcffa3 100644 --- a/src/site/youtube.ts +++ b/src/site/youtube.ts @@ -2,7 +2,7 @@ import { AutoScroll } from "../autoscroll"; import { type Context } from "../lib/behavior"; export class YoutubeBehavior extends AutoScroll { - static id = "Youtube" as const; + static override id = "Youtube" as const; async awaitPageLoad(ctx: Context<{}, {}>) { const { sleep, assertContentValid } = ctx.Lib; await sleep(10); From 8ce455472d126a09c8007cf03741e6628cd2e715 Mon Sep 17 00:00:00 2001 From: emma Date: Thu, 28 Aug 2025 13:10:54 -0400 Subject: [PATCH 7/9] fix build issue --- tsconfig.json | 2 +- {src/lib => types}/global.d.ts | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) rename {src/lib => types}/global.d.ts (77%) diff --git a/tsconfig.json b/tsconfig.json index 96fb0da..d917b3e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,6 +12,6 @@ "outDir": "./dist/" }, "files": ["index.ts"], - "include": ["src/**/*"], + "include": ["src/**/*", "types/**/*"], "exclude": ["node_modules", "**/*.spec.ts"] } diff --git a/src/lib/global.d.ts b/types/global.d.ts similarity index 77% rename from src/lib/global.d.ts rename to types/global.d.ts index 4d09c5a..60fdfff 100644 --- a/src/lib/global.d.ts +++ b/types/global.d.ts @@ -1,6 +1,4 @@ -import { type BehaviorManager } from ".."; - -export {}; // Ensure this is treated as a module +import { type BehaviorManager } from "../src"; interface BehaviorGlobals { __bx_addLink?: (url: string) => Promise; @@ -25,12 +23,21 @@ interface AutoplayProperties { } declare global { - interface WorkerGlobalScope extends BehaviorGlobals {} + interface WorkerGlobalScope extends BehaviorGlobals { + scrollHeight?: number; + getEventListeners?: ( + obj: Obj, + ) => Record< + Obj extends Window ? keyof WindowEventMap : string, + EventListenerOrEventListenerObject[] + >; + [key: string]: any; + } interface Window extends BehaviorGlobals { __WB_replay_top?: Window; /** - * Chrome DevTools’s `getEventListeners` API + * Chrome DevTools's `getEventListeners` API * @see https://developer.chrome.com/docs/devtools/console/utilities/#getEventListeners-function */ getEventListeners?: ( @@ -39,6 +46,7 @@ declare global { Obj extends Window ? keyof WindowEventMap : string, EventListenerOrEventListenerObject[] >; + [key: string]: any; } interface HTMLVideoElement extends AutoplayProperties {} From 5114156f8e096fb7d260e2099f6f99b624d8cb74 Mon Sep 17 00:00:00 2001 From: emma Date: Thu, 28 Aug 2025 13:11:08 -0400 Subject: [PATCH 8/9] fix a few more issues --- src/autoclick.ts | 6 +++--- src/autofetcher.ts | 2 +- src/autoscroll.ts | 1 - src/index.ts | 2 +- src/lib/utils.ts | 12 ++++++------ src/site/twitter.ts | 2 +- 6 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/autoclick.ts b/src/autoclick.ts index f0f6ab5..8498790 100644 --- a/src/autoclick.ts +++ b/src/autoclick.ts @@ -109,9 +109,9 @@ export class AutoClick extends BackgroundBehavior { }); } } - // @ts-expect-error TODO: this looks like a typo, there's no associated `try` block for this `catch` block, so it ends up being a method - catch(e) { - this.debug(e.toString()); + // TODO: this looks like maybe a typo, there's no associated `try` block for this `catch` block, so it ends up being a method + catch(e: unknown) { + this.debug((e as Error).toString()); } async done() { diff --git a/src/autofetcher.ts b/src/autofetcher.ts index dad68cd..c7cd2da 100644 --- a/src/autofetcher.ts +++ b/src/autofetcher.ts @@ -134,7 +134,7 @@ export class AutoFetcher extends BackgroundBehavior { const reader = resp.body!.getReader(); - let res = null; + let res: ReadableStreamReadResult | null = null; do { res = await reader.read(); diff --git a/src/autoscroll.ts b/src/autoscroll.ts index abfe323..cde4ce1 100644 --- a/src/autoscroll.ts +++ b/src/autoscroll.ts @@ -122,7 +122,6 @@ export class AutoScroll } if ( - // @ts-expect-error TODO not sure what self.scrollHeight is here (self.window.scrollY + self["scrollHeight"]) / self.document.scrollingElement!.scrollHeight < 0.9 diff --git a/src/index.ts b/src/index.ts index b95c94c..9d095ae 100644 --- a/src/index.ts +++ b/src/index.ts @@ -415,7 +415,7 @@ export class BehaviorManager { } }); - const promises = []; + const promises: Promise[] = []; for (const url of urls) { promises.push(addLink(url)); diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 3633972..74af8df 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -39,7 +39,7 @@ export async function waitUntilNode( timeout = 1000, interval = waitUnit, ): Promise { - let node = null; + let node: Node | null = null; let stop = false; const waitP = waitUntil(() => { node = xpathNode(path, root); @@ -209,12 +209,12 @@ export async function openWindow( const p = new Promise((resolve) => (self["__bx_openResolve"] = resolve)); await callBinding(self["__bx_open"], { url }); - let win = null; + let win: WindowProxy | null = null; try { - win = await p; + win = (await p) as WindowProxy | null; if (win) { - return win as WindowProxy; + return win; } } catch (e) { console.warn(e); @@ -248,7 +248,7 @@ export class RestoreState { } async restore(rootPath: string, childMatch: string) { - let root = null; + let root: Node | null = null; while (((root = xpathNode(rootPath)), !root)) { await sleep(100); @@ -314,7 +314,7 @@ export function* xpathNodes(path: string, root?: Node | null) { null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, ); - let result = null; + let result: Node | null = null; while ((result = iter.iterateNext()) !== null) { yield result; } diff --git a/src/site/twitter.ts b/src/site/twitter.ts index eb801a2..e5e59d5 100644 --- a/src/site/twitter.ts +++ b/src/site/twitter.ts @@ -179,7 +179,7 @@ export class TwitterTimelineBehavior return; } - let mediaTweetUrl = null; + let mediaTweetUrl: string | null = null; try { mediaTweetUrl = new URL( From 502378eb19928e8b69fb56e31720178626f4b926 Mon Sep 17 00:00:00 2001 From: emma Date: Thu, 18 Sep 2025 17:13:58 -0400 Subject: [PATCH 9/9] remove unused `Behavior` class --- src/lib/behavior.ts | 97 --------------------------------------------- 1 file changed, 97 deletions(-) diff --git a/src/lib/behavior.ts b/src/lib/behavior.ts index 3163af7..ff2d6ef 100644 --- a/src/lib/behavior.ts +++ b/src/lib/behavior.ts @@ -16,103 +16,6 @@ export class BackgroundBehavior { } } -// =========================================================================== -export class Behavior extends BackgroundBehavior { - _running: Promise | null; - paused: Promise | null; - _unpause: (() => void) | null; - state: Partial; - scrollOpts: { - behavior: string; - block: string; - inline: string; - }; - - constructor() { - super(); - this._running = null; - this.paused = null; - this._unpause = null; - this.state = {}; - - this.scrollOpts = { behavior: "smooth", block: "center", inline: "center" }; - } - - start() { - this._running = this.run(); - } - - async done() { - return this._running ? this._running : Promise.resolve(); - } - - async run() { - try { - for await (const step of this) { - this.debug(step); - if (this.paused) { - await this.paused; - } - } - this.debug(this.getState("done!")); - } catch (e) { - this.error((e as Error).toString()); - } - } - - pause() { - if (this.paused) { - return; - } - this.paused = new Promise((resolve) => { - this._unpause = resolve; - }); - } - - unpause() { - if (this._unpause) { - this._unpause(); - this.paused = null; - this._unpause = null; - } - } - - getState>( - msg: string, - incrValue?: IncrKey, - ) { - if (incrValue) { - if (this.state[incrValue] === undefined) { - (this.state[incrValue] as number) = 1; - } else { - (this.state[incrValue] as number)++; - } - } - - return { state: this.state, msg }; - } - - cleanup() {} - - async awaitPageLoad() { - // wait for initial page load here - } - - static load() { - if (self["__bx_behaviors"]) { - self["__bx_behaviors"].load(this); - } else { - console.warn( - `Could not load ${this.name} behavior: window.__bx_behaviors is not initialized`, - ); - } - } - - async *[Symbol.asyncIterator]() { - yield; - } -} - // WIP: BehaviorRunner class allows for arbitrary behaviors outside of the // library to be run through the BehaviorManager