memory / server.js
gcharanteja
b
d6293df
Raw History Blame Contribute Delete
8.73 kB
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;