import "dotenv/config"; import fs from "node:fs/promises"; import { chromium } from "playwright-extra"; import StealthPlugin from "puppeteer-extra-plugin-stealth"; import { App } from "./app"; import { BrowserContext, Page } from "playwright"; const ROOT_FOLDER = "~/.zygon"; export const printDuration = (duration: number) => duration > 1000 ? `${duration / 1000}s` : `${duration}ms`; export const step = async ( step: () => Promise | Result, startMessage: string, endMessage: string, options: Options, ): Promise => { const startTime = new Date().getTime(); try { options.log(`- ${startMessage}`); const result = await step(); options.log( `✓ ${endMessage}${options.noTimer ? "" : ` - ${printDuration(new Date().getTime() - startTime)}`}`, ); return result; } catch (error) { options.log( `✘ ${endMessage}${options.noTimer ? "" : ` - ${printDuration(new Date().getTime() - startTime)}`}`, ); if (error && typeof error === "object" && "message" in error) { options.log(" ", error.message); } throw error; } }; export const auth = async ( integration: App, user: User, page: Page, context: BrowserContext, options: Options, ) => { try { await fs.access( `${ROOT_FOLDER}/auth-cache/${user.email}`, fs.constants.R_OK | fs.constants.W_OK, ); } catch { await fs.mkdir(`${ROOT_FOLDER}/auth-cache/${user.email}`, { recursive: true, }); } const authCache = `${ROOT_FOLDER}/auth-cache/${user.email}/${integration.name}.json`; try { await fs.access(authCache, fs.constants.R_OK | fs.constants.W_OK); // Auth cache await step( async () => { const cache: AuthCache = JSON.parse( await fs.readFile(authCache, "utf-8"), ); await integration.restoreAuthCache(cache, page, context); }, "Restoring user session from cache", "Restored user session from cache", options, ); } catch { // No auth cache await step( async () => { await integration.login(user, page, context); }, "Logging in", "Logged in", options, ); } // We save/refresh the auth cache await step( async () => { const cache = await integration.cacheAuth(page, context); await fs.writeFile(authCache, JSON.stringify(cache)); }, "Saving the authentication cache", "Saved the authentification cache", options, ); }; export type Options = { log: (...content: Array) => void; noTimer?: boolean; }; export const run = async ( integration: App, user: User, action: (page: Page, context: BrowserContext) => Promise, options: Options, ): Promise => { chromium.use(StealthPlugin()); const browser = await chromium.launch({ headless: process.env.HEADFULL !== "true", timeout: process.env.HEADFULL === "true" ? 90000 : 30000, }); const context = await browser.newContext(); const page = await context.newPage(); await page.setViewportSize({ width: 800, height: 600 }); let result; try { await auth(integration, user, page, context, options); result = await action(page, context); } catch (error) { const pages = context.pages(); try { await fs.access( `${ROOT_FOLDER}/screenshots/${integration.name}`, fs.constants.R_OK | fs.constants.W_OK, ); } catch { await fs.mkdir(`${ROOT_FOLDER}/screenshots/${integration.name}`, { recursive: true, }); } for (const page of pages) { const hostname = new URL(page.url()).hostname; if (hostname) { await page.screenshot({ path: `${ROOT_FOLDER}/screenshots/${integration.name}/${hostname}.png`, fullPage: true, }); } } options.log( `An error occured when fetching users. A screenshot of each page has been taken and is available in ${ROOT_FOLDER}/screenshots/${integration.name}.`, ); throw error; } await browser.close(); return result; };