← back to Pattern Vault
wpb-uploaders/lib/base.js
68 lines
/*
* Shared uploader base for every WPB marketplace. Each platform module supplies a
* config { key, name, sessionFile, healthUrl, uploadUrl, uploadFn } and calls run().
*
* HARD SAFETY (matches the Fernwick lessons):
* - DISARMED by default: an upload only fires live if the platform's ARMED file exists
* (wpb-uploaders/platforms/<key>.ARMED). Absent => dry-run (no live action). Arming is
* Steve-gated.
* - ANTI-BOT CADENCE: at most MAX_PER_DAY (4) designs per run; human-typed fields.
* - FAIL-LOUD: a design only counts as uploaded if the platform's uploadFn returns a real
* id/url. No id => it goes to failed[] with the reason — never a false-green.
*/
const path = require('path'), fs = require('fs'), os = require('os');
const CHROMIUM = '/Users/macstudio3/.npm-global/lib/node_modules/playwright';
const { chromium } = require(CHROMIUM);
const { rnd, sleep } = require('./human');
const MAX_PER_DAY = 4; // anti-bot cadence hard cap
const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML,like Gecko) Chrome/126 Safari/537.36';
async function run(cfg, manifestPath) {
const HERE = __dirname;
const armedFile = path.join(HERE, '..', 'platforms', `${cfg.key}.ARMED`);
const ARMED = fs.existsSync(armedFile);
const result = { platform: cfg.key, armed: ARMED, uploaded: [], failed: [], skipped: 0, sessionExpired: false };
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
let designs = (manifest.designs || []).slice(0, MAX_PER_DAY); // enforce cadence cap
if ((manifest.designs || []).length > MAX_PER_DAY) result.skipped = manifest.designs.length - MAX_PER_DAY;
if (!fs.existsSync(cfg.sessionFile)) {
result.sessionExpired = true;
result.error = `no session file for ${cfg.name} (${cfg.sessionFile}) — capture login first`;
console.log(JSON.stringify(result, null, 2));
return result;
}
const b = await chromium.launch({ headless: process.env.WPB_HEADLESS === '1' ? true : !ARMED });
const ctx = await b.newContext({ storageState: cfg.sessionFile, userAgent: UA });
const pg = await ctx.newPage();
try {
// session health
await pg.goto(cfg.healthUrl, { waitUntil: 'domcontentloaded', timeout: 45000 }).catch(() => {});
await sleep(rnd(2000, 3500));
if (pg.url().includes('/login') || pg.url().includes('signin')) {
result.sessionExpired = true; console.log(JSON.stringify(result, null, 2)); await b.close(); return result;
}
for (const d of designs) {
try {
if (!ARMED) { result.uploaded.push({ title: d.title, dryRun: true, id: null }); await sleep(rnd(400, 900)); continue; }
// platform-specific upload → must return { id, url } or throw / return null
const r = await cfg.uploadFn(pg, d, { rnd, sleep });
if (r && r.id) result.uploaded.push({ title: d.title, id: r.id, url: r.url || null, private: true });
else result.failed.push({ title: d.title, reason: (r && r.reason) || 'no design id returned (fail-loud)' });
await sleep(rnd(30000, 90000)); // sporadic human gap between uploads (anti-bot)
} catch (e) {
result.failed.push({ title: d.title, reason: String(e).slice(0, 180) });
}
}
} catch (e) { result.error = String(e).slice(0, 200); }
await b.close();
console.log(JSON.stringify(result, null, 2));
return result;
}
module.exports = { run, MAX_PER_DAY, UA };