Banner creatives must be images: server-side image check (extension or image/* HEAD), form hint, chatbot note

Campaign #134 (NAS ad 2806) had the member's join-page link in the image field and served a broken
banner 141 times on the network; #102 did the same with an imgbb page link. imageCheck() accepts
/uploads/ and instantadpay.com/banners files and image extensions outright, otherwise HEADs the URL
(one redirect) and requires image/*. Wired into member and house campaign creation for banners and
login-ad creatives. Form placeholder + hint point at Promo tools > Banners > Copy image URL.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-16 13:56:30 -05:00
parent d6bc79c9d6
commit 5641582709
4 changed files with 31 additions and 2 deletions
+26
View File
@@ -342,6 +342,30 @@ function serveJoinPage(res, tok, angle, ang, setCookies) {
res.writeHead(200, baseHeaders(headers));
res.end(html);
}
// A banner creative must be an IMAGE (2026-09-16: campaign #134 had the member's join-page link in the
// image field and served a broken banner 141 times on the network). Accept /uploads/ files and
// image extensions outright; otherwise HEAD the URL and require an image/* content-type.
async function imageCheck(url) {
const u = String(url || '').trim();
if (/^\/uploads\//.test(u) || /^https?:\/\/instantadpay\.com\/(uploads|banners)\//i.test(u)) return { ok: true };
if (/\.(png|jpe?g|gif|webp|svg)([?#].*)?$/i.test(u)) return { ok: true };
try {
const h = await headUrl(u);
if (h && /^image\//i.test(String(h.contentType || ''))) return { ok: true };
return { ok: false, reason: 'That link is a web page, not an image. Paste the direct image link (it usually ends in .png or .jpg), or use Promo tools > Banners > Copy image URL.' };
} catch (e) { return { ok: false, reason: 'Could not load that image link. Paste the direct image link (ends in .png or .jpg), or use Promo tools > Banners > Copy image URL.' }; }
}
function headUrl(url) {
return new Promise((resolve, reject) => {
let u; try { u = new URL(url); } catch (e) { return reject(new Error('bad url')); }
const mod = u.protocol === 'http:' ? require('http') : require('https');
const req = mod.request({ method: 'HEAD', hostname: u.hostname, port: u.port || undefined, path: u.pathname + u.search, timeout: 6000, headers: { 'User-Agent': 'Mozilla/5.0 (compatible; InstantAdPay-ImageCheck/1.0)' } }, r => {
if (r.statusCode >= 300 && r.statusCode < 400 && r.headers.location && !u.searchParams.has('_r')) { try { const nx = new URL(r.headers.location, url); nx.searchParams.set('_r', '1'); return resolve(headUrl(nx.toString())); } catch (e) {} }
resolve({ status: r.statusCode, contentType: r.headers['content-type'] || '' }); r.resume();
});
req.on('error', reject); req.on('timeout', () => req.destroy(new Error('timeout'))); req.end();
});
}
async function frameCheck(url) {
const h = await frameFetch(url, 0);
if (h.error) return { ok: false, reason: 'We checked your URL and ' + h.error + '. Fix the URL and try again.' };
@@ -2616,6 +2640,7 @@ const server = http.createServer(async (req, res) => {
const memberId = await auth.refreshMemberId(s); // 0 is fine: earned credits fund banner/text
const b = await readBody(req);
if (!['login', 'solo', 'video', 'featured'].includes(String(b.type || ''))) { // banner/text surf views frame the target; login/video/solo/featured open in a new tab or play in our own player
if (b.type === 'banner' || (b.type === 'login' && b.imageUrl)) { const ic = await imageCheck(b.imageUrl); if (!ic.ok) return json(res, 400, { error: ic.reason }); }
const fc = await frameCheck(b.targetUrl);
if (!fc.ok) return json(res, 400, { error: fc.reason });
}
@@ -2744,6 +2769,7 @@ const server = http.createServer(async (req, res) => {
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
const b = await readBody(req);
if (!['login', 'solo', 'video', 'featured'].includes(String(b.type || ''))) {
if (b.type === 'banner' || (b.type === 'login' && b.imageUrl)) { const ic = await imageCheck(b.imageUrl); if (!ic.ok) return json(res, 400, { error: ic.reason }); }
const fc = await frameCheck(b.targetUrl);
if (!fc.ok) return json(res, 400, { error: fc.reason });
}