Earn: video stops when leaving the tab/pane, 'all watched today' message, open earn tokens survive a redeploy; admin member search matches as you type, clearer old-site line; QA earn harness follows the done screen
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -72,10 +72,40 @@ fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||
const UPLOADS_DIR = path.join(DATA_DIR, 'uploads'); // solo-ad media lives on the volume
|
||||
fs.mkdirSync(UPLOADS_DIR, { recursive: true });
|
||||
const uploadCounts = new Map(); // email:day -> uploads today
|
||||
const gauntletTokens = new Map(); // email -> welcome-tour token (server-clock dwell floor)
|
||||
const videoTokens = new Map(); // email -> watch-to-earn video token (server-clock watch floor)
|
||||
// open earn tokens (ad view / video / visit / tour) live in memory but mirror to the volume so a
|
||||
// redeploy mid-watch does not lose them (2026-09-13: a member's video watch died in a restart)
|
||||
const OPEN_TOKENS_FILE = () => path.join(DATA_DIR, 'open-tokens.json');
|
||||
const persistedMaps = {};
|
||||
let tokenSaveTimer = null;
|
||||
function saveOpenTokens() {
|
||||
tokenSaveTimer = null;
|
||||
try {
|
||||
const out = {};
|
||||
for (const [name, m] of Object.entries(persistedMaps)) out[name] = Object.fromEntries(m);
|
||||
fs.writeFileSync(OPEN_TOKENS_FILE(), JSON.stringify(out));
|
||||
} catch (e) {}
|
||||
}
|
||||
function persistedMap(name) {
|
||||
const m = new Map();
|
||||
const touch = () => { if (!tokenSaveTimer) tokenSaveTimer = setTimeout(saveOpenTokens, 500); };
|
||||
const set = m.set.bind(m), del = m.delete.bind(m);
|
||||
m.set = (k, v) => { set(k, v); touch(); return m; };
|
||||
m.delete = k => { const r = del(k); if (r) touch(); return r; };
|
||||
persistedMaps[name] = m;
|
||||
return m;
|
||||
}
|
||||
function loadOpenTokens() {
|
||||
let saved = null; try { saved = JSON.parse(fs.readFileSync(OPEN_TOKENS_FILE(), 'utf8')); } catch (e) { return; }
|
||||
const cutoff = Date.now() - 2 * 3600 * 1000; let n = 0;
|
||||
for (const [name, m] of Object.entries(persistedMaps)) {
|
||||
for (const [k, v] of Object.entries(saved[name] || {})) if (v && Number(v.ts || 0) > cutoff) { Map.prototype.set.call(m, k, v); n++; }
|
||||
}
|
||||
if (n) console.log('open earn tokens restored:', n);
|
||||
}
|
||||
const gauntletTokens = persistedMap('gauntlet'); // email -> welcome-tour token (server-clock dwell floor)
|
||||
const videoTokens = persistedMap('video'); // email -> watch-to-earn video token (server-clock watch floor)
|
||||
const faucetHits = new Map(); // address -> last faucet ts (rehearsal test-POL faucet rate limit)
|
||||
const visitTokens = new Map(); // email -> verified-visit token (dwell + captcha floor)
|
||||
const visitTokens = persistedMap('visit'); // email -> verified-visit token (dwell + captcha floor)
|
||||
// walk the referral chain upward via sponsorRef (code/username/member id)
|
||||
async function uplineSlides(email, depth = 3) {
|
||||
const out = [];
|
||||
@@ -189,7 +219,7 @@ function codeTrip(req, ip) {
|
||||
else if (ADMIN_EMAIL && mailer.hasKey()) mailer.send(ADMIN_EMAIL, 'InstantAdPay: sign-up guard tripped', text).catch(() => {});
|
||||
}
|
||||
// earn-view tokens: emailLower -> {token, ts} (one live token per member)
|
||||
const earnTokens = new Map();
|
||||
const earnTokens = persistedMap('earn');
|
||||
// human-check pairs for the view verifier: [emoji shown, word named in the prompt]
|
||||
const CAPTCHA = [['🚀', 'rocket'], ['⚡', 'lightning bolt'], ['🔑', 'key'], ['🎯', 'target'],
|
||||
['🌊', 'wave'], ['🔥', 'flame'], ['💎', 'diamond'], ['🧲', 'magnet'], ['🔔', 'bell'], ['🌙', 'moon']];
|
||||
@@ -332,6 +362,7 @@ async function boot() {
|
||||
promos.init({ dataDir: DATA_DIR });
|
||||
blog.init({ dataDir: DATA_DIR });
|
||||
adminMember.init({ accounts, ads, chain, tank, legacy, promos, messages, dataDir: DATA_DIR });
|
||||
loadOpenTokens();
|
||||
setInterval(() => tankNotifyTick().catch(e => console.error('tank notify', e.message)), 15 * 60 * 1000); // new tank arrivals -> Telegram
|
||||
geo.init({ dataDir: DATA_DIR }).catch(e => console.error('geo init', e.message));
|
||||
setInterval(() => geo.refresh().catch(e => console.error('geo refresh', e.message)), 24 * 3600 * 1000); // monthly file, checked daily
|
||||
@@ -1775,7 +1806,10 @@ const server = http.createServer(async (req, res) => {
|
||||
if (status.left <= 0) return json(res, 200, { ad: null, status });
|
||||
const orientation = String(u.searchParams.get('orientation') || ''); // 'portrait' = Shorts reel, 'landscape' = Watch videos tab
|
||||
const ad = await ads.serveVideo(Object.assign({ excludeEmail: s.email, orientation }, viewerGeo(req))); // never your own video
|
||||
if (!ad) return json(res, 200, { ad: null, status });
|
||||
if (!ad) { // distinguish "you have watched every live video today" from "nothing is live" (Marty, 2026-09-13)
|
||||
let allWatched = false; try { allWatched = !!(await ads.serveVideo(Object.assign({ excludeEmail: s.email, orientation, ignoreSeen: true }, viewerGeo(req)))); } catch (e) {}
|
||||
return json(res, 200, { ad: null, status, allWatched });
|
||||
}
|
||||
const token = crypto.randomBytes(16).toString('hex');
|
||||
videoTokens.set(s.email, { token, ts: Date.now(), id: ad.id, secs: ad.watchSecs });
|
||||
return json(res, 200, { ad, token, status });
|
||||
|
||||
Reference in New Issue
Block a user