import fs from "node:fs"; import https from "node:https"; import path from "node:path"; import { fileURLToPath } from "node:url"; import express from "express"; import { firefox } from "playwright"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const app = express(); const PORT = process.env.PORT || 3000; const STATE_FILE = path.join(__dirname, "visa-state.json"); const URL = "https://immi.homeaffairs.gov.au/what-we-do/whm-program/latest-news/new-work-and-holiday-subclass-462-visa-pre-application-process"; const TARGET_COUNTRY = "India"; const HEADING_TEXT = "Visa ballot arrangements for program year 2026-2027"; const API_BASE = "https://maxxcarl-keyvault.hf.space/secrets"; const API_KEY = "Azure@123"; const SECRETS = [ "SMTP_HOST", "SMTP_PORT", "SMTP_USER", "SMTP_PASS", "FROM_EMAIL", "TO_EMAIL", "BREVO_API_KEY", ]; app.get("/hello", (req, res) => { res.send("hello world pal"); }); const log = (msg) => console.log(`[${new Date().toISOString()}] ${msg}`); const err = (msg) => console.error(`[${new Date().toISOString()}] ${msg}`); function fetchSecret(key) { log(`Fetching secret: ${key}`); return new Promise((resolve, reject) => { const url = `${API_BASE}/${key}`; const req = https .get( url, { headers: { accept: "application/json", "X-API-Key": API_KEY } }, (res) => { let data = ""; res.on("data", (chunk) => { data += chunk; }); res.on("end", () => { try { log(`Secret ${key} fetched OK`); resolve(JSON.parse(data).value); } catch (error) { reject(new Error(`Failed to parse response for ${key}: ${data}`)); } }); }, ) .on("error", (e) => { err(`Secret fetch failed for ${key}: ${e.message}`); reject(e); }); req.setTimeout(15000, () => { req.destroy(); reject(new Error(`Timeout fetching secret ${key}`)); }); }); } function readState() { try { return JSON.parse(fs.readFileSync(STATE_FILE, "utf8")); } catch { return {}; } } function writeState(state) { fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2)); } async function sendEmailViaApi(subject, body) { log("Fetching email config..."); const [from, to, apiKey] = await Promise.all([ fetchSecret("FROM_EMAIL"), fetchSecret("TO_EMAIL"), fetchSecret("BREVO_API_KEY"), ]); log(`Sending via Brevo API (from=${from}, to=${to})`); const payload = JSON.stringify({ sender: { email: from }, to: [{ email: to }], subject, textContent: body, }); return new Promise((resolve, reject) => { const req = https.request( { hostname: "api.brevo.com", port: 443, path: "/v3/smtp/email", method: "POST", headers: { "api-key": apiKey, "Content-Type": "application/json", "Content-Length": Buffer.byteLength(payload), }, }, (res) => { let data = ""; res.on("data", (c) => (data += c)); res.on("end", () => { if (res.statusCode === 201 || res.statusCode === 200) { log("Email sent via Brevo API"); resolve(); } else { reject(new Error(`Brevo API ${res.statusCode}: ${data}`)); } }); }, ); req.on("error", reject); req.write(payload); req.end(); }); } async function sendEmail(subject, body) { try { await sendEmailViaApi(subject, body); } catch (error) { err(`Failed to send email: ${error.message}`); throw error; } } function valuesChanged(oldVals, newVals) { return ( oldVals.selection1 !== newVals.selection1 || oldVals.selection2 !== newVals.selection2 || oldVals.selection3 !== newVals.selection3 ); } async function scrape() { log("Launching Firefox browser..."); const browser = await firefox.launch({ headless: true }); log("Browser launched"); try { const context = await browser.newContext({ viewport: { width: 1920, height: 1080 }, locale: "en-AU", }); const page = await context.newPage(); log(`Navigating to ${URL}...`); await page.goto(URL, { waitUntil: "domcontentloaded", timeout: 60000 }); log("Page loaded"); const title = await page.title(); if (title === "Access Denied") { console.error("Access denied - bot detected"); return null; } const header = page.locator("h2", { hasText: HEADING_TEXT }); const headerCount = await header.count(); if (headerCount === 0) { console.error("Heading not found"); return null; } const section = header.locator("xpath=ancestor::section").first(); const sectionCount = await section.count(); const container = sectionCount > 0 ? section : header.locator("xpath=.."); const table = container.locator("table").first(); const tableCount = await table.count(); if (tableCount === 0) { console.error("Table not found"); return null; } const rows = await table.locator("tbody tr").evaluateAll((tableRows) => tableRows.map((row) => { const cells = row.querySelectorAll("td"); return [...cells].map((cell) => cell.textContent?.trim() ?? ""); }), ); const indiaRow = rows.find((row) => row[0] === TARGET_COUNTRY); if (!indiaRow) { console.error("India row not found"); return null; } return { selection1: indiaRow[1] || "TBA", selection2: indiaRow[2] || "TBA", selection3: indiaRow[3] || "TBA", }; } finally { await browser.close(); } } async function main() { log("=== Starting visa ballot check ==="); const current = await scrape(); if (!current) { log("Scrape failed (null returned), will retry next run"); return; } log(`Scraped India values: ${JSON.stringify(current)}`); log("Reading saved state..."); const state = readState(); const prevIndia = state.india || { selection1: "TBA", selection2: "TBA", selection3: "TBA", }; const now = Date.now(); const changed = valuesChanged(prevIndia, current); let emailSent = false; log(`Prev India values: ${JSON.stringify(prevIndia)}`); log(`Changed: ${changed}`); if (changed) { log("Values changed! Sending alert email."); const subject = "[Visa Alert] India ballot values changed"; const body = `India Work and Holiday ballot values have changed: Selection 1: ${prevIndia.selection1} → ${current.selection1} Selection 2: ${prevIndia.selection2} → ${current.selection2} Selection 3: ${prevIndia.selection3} → ${current.selection3} Check: ${URL}`; try { await sendEmail(subject, body); log("Change alert email sent"); emailSent = true; } catch (error) { err(`Failed to send change email: ${error.message}`); } state.lastChangeTime = new Date().toISOString(); } else { const allStillTba = current.selection1 === "TBA" && current.selection2 === "TBA" && current.selection3 === "TBA"; const lastDailyEmail = state.lastDailyEmailTime ? new Date(state.lastDailyEmailTime).getTime() : 0; const hoursSinceLastDaily = (now - lastDailyEmail) / 3600000; if (allStillTba && hoursSinceLastDaily >= 24) { log("Still TBA and 24h since last daily email - sending daily status"); try { await sendEmail( `[Visa Status] India ballot still TBA (${new Date().toLocaleDateString()})`, `India's Work and Holiday ballot values are still TBA as of ${new Date().toISOString()}. Selection 1: ${current.selection1} Selection 2: ${current.selection2} Selection 3: ${current.selection3} Check: ${URL}`, ); log("Daily status email sent"); emailSent = true; state.lastDailyEmailTime = new Date().toISOString(); } catch (error) { err(`Failed to send daily email: ${error.message}`); } } } state.india = current; state.lastCheckTime = new Date().toISOString(); if (emailSent) state.lastEmailTime = new Date().toISOString(); log("Writing state to disk..."); writeState(state); log("State saved"); log("=== Visa ballot check complete ==="); } const isDirectRun = process.argv[1] && path.resolve(process.argv[1]) === __filename; if (isDirectRun) { app.listen(PORT, () => { // eslint-disable-next-line no-console console.log(`Server listening on port ${PORT}`); }); main().catch((error) => { err(`Fatal error: ${error.message}`); if (error.stack) err(error.stack); process.exit(1); }); } export default app;