LinkSpin test area: InstantAdPay engine fork rebranded, network registry, sponsor carry-over with engine activation and claim window, rotator with /r/ redirects, link-domain mini-sites
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
+58
@@ -0,0 +1,58 @@
|
||||
// Ad reports: members flag a broken or inappropriate ad (ads are auto-approved,
|
||||
// so this is the safety valve). Stored dual-mode (MySQL when DATABASE_URL, else
|
||||
// a JSON file in the volume) and surfaced to the admin.
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const db = require('./db');
|
||||
|
||||
let DATA_DIR = null;
|
||||
const J = {
|
||||
db: null,
|
||||
FILE: () => path.join(DATA_DIR, 'ad-reports.json'),
|
||||
load() { try { this.db = JSON.parse(fs.readFileSync(this.FILE(), 'utf8')); } catch (e) { this.db = { nextId: 1, items: [] }; } },
|
||||
save() { try { fs.writeFileSync(this.FILE(), JSON.stringify(this.db)); } catch (e) {} }
|
||||
};
|
||||
function init(opts) { DATA_DIR = opts.dataDir; }
|
||||
|
||||
const REASONS = ['broken', 'inappropriate', 'spam', 'scam', 'other'];
|
||||
|
||||
async function add(campaignId, reporterEmail, reason, note) {
|
||||
const now = Date.now();
|
||||
const r = REASONS.includes(String(reason)) ? String(reason) : 'other';
|
||||
const n = String(note || '').slice(0, 500);
|
||||
const cid = Number(campaignId) || 0;
|
||||
const who = String(reporterEmail || '').toLowerCase();
|
||||
if (db.enabled()) {
|
||||
const ins = await db.q('INSERT INTO ad_reports (campaign_id,reporter,reason,note,ts) VALUES (?,?,?,?,?)',
|
||||
[cid, who, r, n, now]);
|
||||
return { id: ins.insertId, campaignId: cid, reason: r };
|
||||
}
|
||||
if (!J.db) J.load();
|
||||
const id = J.db.nextId++;
|
||||
J.db.items.push({ id, campaignId: cid, reporter: who, reason: r, note: n, ts: now, resolved: false });
|
||||
J.save();
|
||||
return { id, campaignId: cid, reason: r };
|
||||
}
|
||||
// recent reports for the admin view (newest first)
|
||||
async function list(limit = 200) {
|
||||
if (db.enabled()) {
|
||||
const rows = await db.q('SELECT id,campaign_id,reporter,reason,note,ts,resolved FROM ad_reports ORDER BY ts DESC LIMIT ?', [limit]);
|
||||
return rows.map(x => ({ id: x.id, campaignId: x.campaign_id, reporter: x.reporter, reason: x.reason, note: x.note, ts: Number(x.ts), resolved: !!x.resolved }));
|
||||
}
|
||||
if (!J.db) J.load();
|
||||
return J.db.items.slice().sort((a, b) => b.ts - a.ts).slice(0, limit);
|
||||
}
|
||||
async function resolve(id) {
|
||||
if (db.enabled()) { await db.q('UPDATE ad_reports SET resolved=1 WHERE id=?', [Number(id)]); return { ok: true }; }
|
||||
if (!J.db) J.load();
|
||||
const it = J.db.items.find(x => x.id === Number(id)); if (it) { it.resolved = true; J.save(); }
|
||||
return { ok: true };
|
||||
}
|
||||
// how many unresolved (for an admin badge)
|
||||
async function openCount() {
|
||||
if (db.enabled()) { const r = await db.q('SELECT COUNT(*) n FROM ad_reports WHERE resolved=0'); return r[0].n; }
|
||||
if (!J.db) J.load();
|
||||
return J.db.items.filter(x => !x.resolved).length;
|
||||
}
|
||||
|
||||
module.exports = { init, add, list, resolve, openCount, REASONS };
|
||||
Reference in New Issue
Block a user