import * as Playwright from "playwright"; import { chromium, devices } from "playwright"; import type { Database } from "./database"; import type { TPlatform } from "./platform"; export type ImageFile = { url: string; blocks?: Array>; width?: number; height?: number; }; async function drawImage(imageFile: ImageFile): Promise { const canvas = Object.assign(document.createElement("canvas"), { width: imageFile.width, height: imageFile.height, }); const image = (await new Promise((resolve) => { Object.assign(new Image(), { crossOrigin: "use-credentials", src: imageFile.url, onload() { resolve(this); }, }); })) as HTMLImageElement; const ctx = canvas.getContext("2d")!; for (const q of imageFile.blocks!) { ctx.drawImage( image, q.destX, q.destY, q.width, q.height, q.srcX, q.srcY, q.width, q.height, ); } const dataUrl = canvas.toDataURL(); return dataUrl; } async function fetchImage(imageFile: ImageFile): Promise { const res = await fetch(imageFile.url); const blob = await res.blob(); const dataUrl: string = await new Promise((resolve, reject) => { const fileReader = Object.assign(new FileReader(), { onload(): void { resolve(this.result); }, onerror(e: ErrorEvent): void { const error = new Error(`${e.type}: ${e.message}`); reject(error); }, }); fileReader.readAsDataURL(blob); }); return dataUrl; } async function dataUrlToBlob(dataUrl: string): Promise { const res = await fetch(dataUrl); return await res.blob(); } export type Browser = { loadBrowserContext(platform: TPlatform): Promise; saveBrowserContext(platform: TPlatform, ctx: BrowserContext): Promise; newContext(): Promise; close(): Promise; drawImage( pageOrFrame: Playwright.Page | Playwright.Frame, imageFile: ImageFile, ): Promise; }; export type BrowserContext = Playwright.BrowserContext; export async function createBrowser({ db, headless = true, }: { db: Database; headless?: boolean; }): Promise { const { userAgent } = devices["Desktop Chrome"]; const browser = await chromium.launch({ headless, args: ["--disable-blink-features=AutomationControlled"], }); return { async loadBrowserContext( platform: TPlatform, ): Promise { const { secrets } = await db.get( `select secrets from platforms where name = ?`, platform, ); const storageState = JSON.parse(secrets) ?? undefined; const ctx = await browser.newContext({ storageState, userAgent }); return ctx; }, async saveBrowserContext( platform: TPlatform, ctx: BrowserContext, ): Promise { const secrets = await ctx.storageState(); await db.run( `update platforms set secrets = ? where name = ?`, JSON.stringify(secrets), platform, ); }, newContext: () => browser.newContext(), close: () => browser.close(), async drawImage( pageOrFrame: Playwright.Page | Playwright.Frame, imageFile: ImageFile, ): Promise { if (Array.isArray(imageFile.blocks) && imageFile.blocks.length > 0) { const dataUrl = await pageOrFrame.evaluate(drawImage, imageFile); return await dataUrlToBlob(dataUrl); } if (imageFile.url.startsWith("blob:")) { const dataUrl = await pageOrFrame.evaluate(fetchImage, imageFile); return await dataUrlToBlob(dataUrl); } const page = "page" in pageOrFrame ? pageOrFrame.page() : pageOrFrame; const res = await page.context().request.get(imageFile.url); const headers = res.headers(); const buffer = await res.body(); let type = headers["content-type"]; if (type === "binary/octet-stream") { const [, extension] = /^attachment *; *filename="[^"]+[.]([^.]+)" *(?:$|;)/i.exec( headers["content-disposition"], ) ?? []; switch (extension) { case "zip": type = "application/zip"; break; } } return new Blob([buffer], { type }); }, }; }