Download server.js from maxxcarl/memory: direct link, hf CLI and curl.
- Browser
- Download file 8.73 kB
-
https://huggingface.co/spaces/maxxcarl/memory/resolve/main/server.js
- Command line
-
hf download hf://spaces/maxxcarl/memory/server.js
-
curl -L -o server.js https://huggingface.co/spaces/maxxcarl/memory/resolve/main/server.js
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; | |