← back to AbramsOS
lib/claim-openclaw-run.js
192 lines
// lib/claim-openclaw-run.js — live openclaw auto-TYPE for a settlement claim form.
// Connects to the already-running openclaw Chrome (CDP :18800) via puppeteer-core and drives
// its OWN dedicated tab (immune to whatever else is churning the shared browser's active tab).
// It navigates to the claim form, label-matches the identity + payment fields across ANY
// administrator's markup, types the values, screenshots the filled form, and STOPS.
//
// HARD LINE: never checks the penalty-of-perjury attestation, never clicks Submit/File. The tab
// is left open for Steve to review, attest, and submit himself.
const path = require('path');
const fs = require('fs');
const puppeteer = require('puppeteer-core');
const CDP = process.env.OPENCLAW_CDP || 'http://127.0.0.1:18800';
const SHOT_DIR = path.join(__dirname, '..', 'public', 'claim-shots');
// The label-matcher runs INSIDE the page. Matches inputs by their label/aria/placeholder/name,
// fills identity fields, and selects the PayPal payment radio. Never touches submit/attestation.
function inPageFill(values) {
var STATES = { AL:'Alabama',AK:'Alaska',AZ:'Arizona',AR:'Arkansas',CA:'California',CO:'Colorado',CT:'Connecticut',DE:'Delaware',FL:'Florida',GA:'Georgia',HI:'Hawaii',ID:'Idaho',IL:'Illinois',IN:'Indiana',IA:'Iowa',KS:'Kansas',KY:'Kentucky',LA:'Louisiana',ME:'Maine',MD:'Maryland',MA:'Massachusetts',MI:'Michigan',MN:'Minnesota',MS:'Mississippi',MO:'Missouri',MT:'Montana',NE:'Nebraska',NV:'Nevada',NH:'New Hampshire',NJ:'New Jersey',NM:'New Mexico',NY:'New York',NC:'North Carolina',ND:'North Dakota',OH:'Ohio',OK:'Oklahoma',OR:'Oregon',PA:'Pennsylvania',RI:'Rhode Island',SC:'South Carolina',SD:'South Dakota',TN:'Tennessee',TX:'Texas',UT:'Utah',VT:'Vermont',VA:'Virginia',WA:'Washington',WV:'West Virginia',WI:'Wisconsin',WY:'Wyoming' };
// checked in this ORDER (specific first); email before street; street excludes email/apt/line-2.
var ORDER = ['email','first_name','middle','last_name','zip','city','state','phone','street','full_name'];
var KEY = {
email: { rx: /e-?mail/i },
first_name: { rx: /first\s*(name)?|fname|given/i },
middle: { rx: /middle|m\.?i\.?|initial/i },
last_name: { rx: /last\s*(name)?|lname|surname|family/i },
full_name: { rx: /full\s*name|^\s*name\s*$/i },
street: { rx: /(street|mailing|^\s*address\s*$|address\s*1|street\s*1|addr)/i, not: /e-?mail|apartment|unit|suite|\bbox\b|street\s*2|address\s*2|line\s*2/i },
city: { rx: /city|town/i },
state: { rx: /\bstate\b|province/i },
zip: { rx: /zip|postal/i },
phone: { rx: /phone|tel(?!e?vision)|mobile/i },
};
function txt(node) { if (!node) return ''; var c = node.cloneNode(true); var kids = c.querySelectorAll ? c.querySelectorAll('input,select,textarea,button,script,style') : []; for (var j = 0; j < kids.length; j++) kids[j].remove(); return (c.innerText || c.textContent || ''); }
function labelFor(el) {
var t = '';
// 1) standard associations
try { if (el.id) { var l = document.querySelector('label[for="' + (window.CSS && CSS.escape ? CSS.escape(el.id) : el.id) + '"]'); if (l) t += ' ' + l.textContent; } } catch (e) {}
var w = el.closest && el.closest('label'); if (w) t += ' ' + txt(w);
t += ' ' + (el.getAttribute('aria-label') || '') + ' ' + (el.placeholder || '') + ' ' + (el.name || '') + ' ' + (el.id || '');
// 2) PROXIMITY — these forms use bare inputs with the label as nearby visible text
if (el.previousElementSibling) t += ' ' + txt(el.previousElementSibling);
var cell = el.closest && el.closest('td,th,div,li,p,span,section,fieldset');
if (cell) t += ' ' + txt(cell);
var td = el.closest && el.closest('td'); if (td && td.previousElementSibling) t += ' ' + txt(td.previousElementSibling);
// walk back a couple prior siblings for "label then input" layouts
var pv = el.previousElementSibling, hop = 0;
while (pv && hop < 3) { t += ' ' + txt(pv); pv = pv.previousElementSibling; hop++; }
return t.toLowerCase().replace(/\s+/g, ' ').trim().slice(0, 200);
}
var filled = [];
var usedKey = {}; // single-use keys (fill each identity field once)
var firstEl = null; // remember the input that got first_name (for positional last-name)
var els = Array.prototype.slice.call(document.querySelectorAll('input, select, textarea'));
for (var i = 0; i < els.length; i++) {
var el = els[i];
var ty = (el.type || '').toLowerCase();
if (['hidden', 'submit', 'button', 'checkbox', 'radio', 'file', 'password'].indexOf(ty) >= 0) continue;
if (el.value && el.tagName !== 'SELECT') continue; // don't overwrite what's already there
var lab = labelFor(el);
// Combined single name field ("First Name, Middle Initial, Last Name") → full name.
if (!usedKey.full_name && values.full_name && el.tagName === 'INPUT' && /first\s*name/.test(lab) && /last\s*name/.test(lab)) {
try { el.focus(); } catch (e) {}
el.value = values.full_name;
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
el.dispatchEvent(new Event('blur', { bubbles: true }));
usedKey.full_name = usedKey.first_name = usedKey.last_name = 1;
filled.push('full_name'); firstEl = el; continue;
}
for (var oi = 0; oi < ORDER.length; oi++) {
var k = ORDER[oi];
var def = KEY[k];
if (usedKey[k]) continue;
if (!def.rx.test(lab)) continue;
if (def.not && def.not.test(lab)) continue;
if (k === 'middle') { usedKey[k] = 1; break; } // claim the MI input so it's not mis-filled; no value passed
if (!values[k]) { break; } // matched but nothing to put → leave blank, stop trying other keys
if (el.tagName === 'SELECT') {
var want = String(values[k]).toLowerCase();
var full = (k === 'state' && STATES[String(values[k]).toUpperCase()] || '').toLowerCase();
for (var o = 0; o < el.options.length; o++) {
var op = el.options[o]; var ov = op.value.toLowerCase(), otx = op.textContent.trim().toLowerCase();
if (ov === want || otx === want || (full && (ov === full || otx === full))) { el.value = op.value; break; }
}
el.dispatchEvent(new Event('change', { bubbles: true }));
} else {
try { el.focus(); } catch (e) {}
el.value = values[k];
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
el.dispatchEvent(new Event('blur', { bubbles: true }));
}
usedKey[k] = 1; filled.push(k);
if (k === 'first_name') firstEl = el;
break;
}
}
// POSITIONAL last-name: forms that put "First Name · Middle Initial · Last Name" under one
// label leave the last-name input unmatched. Take the empty text inputs after the first-name
// input in the same container: [next]=middle (if 3), [last]=last name.
if (firstEl && values.last_name && !usedKey.last_name) {
try {
var cont = firstEl.closest('tr,div,fieldset,section,p') || firstEl.parentElement;
var row = Array.prototype.slice.call(cont.querySelectorAll('input')).filter(function (x) { var tt = (x.type || 'text').toLowerCase(); return ['hidden', 'submit', 'button', 'checkbox', 'radio', 'file', 'password'].indexOf(tt) < 0; });
var fi = row.indexOf(firstEl);
if (fi >= 0 && row.length >= 2) {
var lastInput = row[row.length - 1];
if (lastInput && lastInput !== firstEl && !lastInput.value) {
lastInput.focus(); lastInput.value = values.last_name;
lastInput.dispatchEvent(new Event('input', { bubbles: true }));
lastInput.dispatchEvent(new Event('change', { bubbles: true }));
lastInput.dispatchEvent(new Event('blur', { bubbles: true }));
usedKey.last_name = 1; filled.push('last_name');
}
}
} catch (e) {}
}
// payment: select PayPal option if the form has one (radio or its label)
var paid = false;
var radios = Array.prototype.slice.call(document.querySelectorAll('input[type=radio]'));
for (var r = 0; r < radios.length; r++) {
var rl = (radios[r].value || '') + ' ' + labelForRadio(radios[r]);
if (/paypal/i.test(rl)) { try { radios[r].click(); paid = true; } catch (e) {} break; }
}
function labelForRadio(el) { var w = el.closest && el.closest('label'); var t = w ? w.textContent : ''; if (el.id) { var l = document.querySelector('label[for="' + el.id + '"]'); if (l) t += ' ' + l.textContent; } return t; }
return { filled: filled, paypal_selected: paid, url: location.href, title: document.title };
}
async function runFill(claimId, formUrl, values) {
fs.mkdirSync(SHOT_DIR, { recursive: true });
let browser;
try {
browser = await puppeteer.connect({ browserURL: CDP, defaultViewport: null, protocolTimeout: 60000 });
} catch (e) {
return { ok: false, error: 'openclaw browser not reachable at ' + CDP + ' (' + e.message + ')' };
}
let page;
try {
page = await browser.newPage(); // OUR tab — not the shared active tab
await page.goto(formUrl, { waitUntil: 'domcontentloaded', timeout: 45000 });
await new Promise((r) => setTimeout(r, 3500)); // let SPA render
// If we landed on the Mode aggregator, hop to the OFFICIAL settlement site first
// (real Chrome, so Mode's bot-403 doesn't apply). Pick the strongest external claim link.
try {
if (/modeclassactionsdaily\.com/i.test(page.url())) {
const off = await page.evaluate(() => {
var here = location.hostname;
var links = Array.prototype.slice.call(document.querySelectorAll('a[href^="http"]'));
var cand = links.filter(function (e) { try { var u = new URL(e.href); return u.hostname !== here && !/modeclassaction|facebook|twitter|instagram|google|youtube/i.test(u.hostname); } catch (_) { return false; } });
var best = cand.find(function (e) { return /file (a )?claim|official (settlement )?(site|website)|claim (here|now|form)|settlement website|visit/i.test(e.textContent || ''); })
|| cand.find(function (e) { try { return /settlement|claim|litigation|classaction|pnclassaction|dataincident/i.test(new URL(e.href).hostname); } catch (_) { return false; } });
return best ? best.href : null;
});
if (off) { await page.goto(off, { waitUntil: 'domcontentloaded', timeout: 45000 }); await new Promise((r) => setTimeout(r, 3000)); }
}
} catch (e) {}
// follow an obvious "File a Claim / Submit a Claim / Claim Form" link if we landed on a landing page
try {
const RX = /file a claim|submit a claim|online claim|claim form|start (your )?claim|enter online|online filing|filing site|enter .*filing|proceed to|continue to (the )?claim|begin/i;
// Try up to 3 hops of "enter/continue" gateways (instructions page → filing site → form).
for (var hop = 0; hop < 3; hop++) {
var acted = await page.evaluate((rxSrc) => {
var rx = new RegExp(rxSrc, 'i');
var el = Array.prototype.slice.call(document.querySelectorAll('a,button,input[type=submit],input[type=button]'))
.find(function (e) { return rx.test(e.textContent || e.value || ''); });
if (!el) return null;
if (el.href && /^https?:/.test(el.href)) return { href: el.href };
el.click(); return { clicked: true };
}, RX.source);
if (!acted) break;
if (acted.href) { await page.goto(acted.href, { waitUntil: 'domcontentloaded', timeout: 45000 }); }
await new Promise((r) => setTimeout(r, 3500));
// stop hopping once we see real name/email inputs
var hasForm = await page.evaluate(() => document.querySelectorAll('input[type=text],input[type=email]').length >= 3);
if (hasForm) break;
}
} catch (e) {}
const result = await page.evaluate(inPageFill, values);
const shot = path.join(SHOT_DIR, claimId + '.png');
await page.screenshot({ path: shot, fullPage: true }).catch(() => {});
browser.disconnect(); // LEAVE the tab open for Steve to review + attest + submit
return { ok: true, filled: result.filled, paypal_selected: result.paypal_selected, on: result.url, title: result.title, screenshot: '/claim-shots/' + claimId + '.png' };
} catch (e) {
try { browser.disconnect(); } catch (_) {}
return { ok: false, error: e.message };
}
}
module.exports = { runFill };