Social layer: leaderboard (week/month/all) + six badges, /leaders and /promo pages, share panel and rank on the board, ?r= referral cookie -> member's IAP join link, OG tags, banner set served through the curtain

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-19 19:40:50 -05:00
parent cdded9d4ed
commit dd6a3a3bfa
19 changed files with 304 additions and 11 deletions
+19 -3
View File
@@ -18,6 +18,7 @@ const store = require('./lib/store');
const sso = require('./lib/sso');
const missions = require('./lib/missions');
const rewards = require('./lib/rewards');
const social = require('./lib/social');
const faucet = require('./lib/faucet');
const PORT = Number(process.env.PORT || 3000);
@@ -48,6 +49,7 @@ async function notify(kind, p) {
}
// Marty's rule (2026-09-19): when the day's pool is spent, say so; claims reopen at midnight Central
const SPENT_MSG = 'Today\u2019s POL pool is spent. No more claims today. Claims reopen at midnight Central.';
function refOf(req) { const m = /(?:^|;\s*)ph\.ref=([^;]+)/.exec(req.headers.cookie || ''); const r = m ? decodeURIComponent(m[1]) : ''; return /^[A-Za-z0-9_.-]{1,40}$/.test(r) ? r : null; }
function explorer() { return Number(process.env.HUNT_CHAIN_ID) === 80002 ? 'https://amoy.polygonscan.com' : 'https://polygonscan.com'; }
// ---- helpers -------------------------------------------------------------------------------
@@ -115,9 +117,17 @@ const server = http.createServer(async (req, res) => {
}
if (p === '/embed.js') return sendFile(res, path.join(PUBLIC_DIR, 'embed.js'), { 'Cache-Control': 'public, max-age=300', 'Access-Control-Allow-Origin': '*' });
// the coin on the code pill, fetched by mission-site visitors who hold no curtain pass
if (p === '/img/coin-sm.png' || p === '/img/coin.png') return sendFile(res, path.join(PUBLIC_DIR, p.slice(1)), { 'Cache-Control': 'public, max-age=86400' });
if (p === '/img/coin-sm.png' || p === '/img/coin.png' || p === '/img/og.jpg' || /^\/promo\/polhunter-\d+x\d+\.(jpg|png)$/.test(p)) return sendFile(res, path.join(PUBLIC_DIR, p.slice(1)), { 'Cache-Control': 'public, max-age=86400' });
if (curtained(req, res, u)) return;
// a member's share link: /?r=<their IAP username or id> becomes a 30-day cookie; the landing then
// sends sign-ups to instantadpay.com/join/<ref>, so IAP's own last-touch sponsor rule applies
if (p === '/' && u.searchParams.get('r')) {
const r = String(u.searchParams.get('r')).trim().slice(0, 40); u.searchParams.delete('r');
const h = { Location: '/' + (u.searchParams.toString() ? '?' + u.searchParams : ''), 'Cache-Control': 'no-store' };
if (/^[A-Za-z0-9_.-]{1,40}$/.test(r)) h['Set-Cookie'] = 'ph.ref=' + encodeURIComponent(r) + '; Path=/; Max-Age=2592000; SameSite=Lax; Secure';
res.writeHead(302, h); return res.end();
}
// ---- sign-in by hand-off from InstantAdPay
if (p === '/auth') {
@@ -132,7 +142,11 @@ const server = http.createServer(async (req, res) => {
if (p === '/logout') { const s = sso.fromRequest(req); if (s) sso.endSession(s.sid); res.writeHead(302, { Location: '/', 'Set-Cookie': sso.clearCookie() }); return res.end(); }
// ---- public
if (p === '/api/config') return json(res, 200, { name: 'PolHunter', signupsOpen: signupsOpen(), reward: rewards.settings(), iapUrl: 'https://instantadpay.com/my', explorer: explorer() });
if (p === '/api/config') { const ref = refOf(req); return json(res, 200, { name: 'PolHunter', signupsOpen: signupsOpen(), reward: rewards.settings(), iapUrl: 'https://instantadpay.com/my', explorer: explorer(), ref, joinUrl: ref ? 'https://instantadpay.com/join/' + encodeURIComponent(ref) + '?from=polhunter' : 'https://instantadpay.com/?from=polhunter' }); }
if (p === '/api/leaders') { const per = ['week', 'month', 'all'].includes(u.searchParams.get('period')) ? u.searchParams.get('period') : 'week'; return json(res, 200, { period: per, rows: social.leaderboard(per, 25) }, { 'Cache-Control': 'public, max-age=60' }); }
if (p === '/api/badges') return json(res, 200, { badges: social.BADGES });
if (p === '/leaders') return sendFile(res, path.join(PUBLIC_DIR, 'leaders.html'));
if (p === '/promo') return sendFile(res, path.join(PUBLIC_DIR, 'promo.html'));
if (p === '/api/ledger') { const t = rewards.totals(); return json(res, 200, { totals: t, recent: rewards.ledger(30).map(x => ({ who: x.username ? '@' + x.username : '#' + x.memberId, site: x.site, pol: x.pol, tx: x.tx, at: x.paidAt })) }); }
// ---- hunter (session required)
@@ -141,7 +155,9 @@ const server = http.createServer(async (req, res) => {
if (p === '/api/my/board') {
const done = new Set(rewards.mine(me.memberId).map(x => x.missionId));
const wdone = me.wallet ? new Set(store.read('payouts', []).filter(x => x.wallet && x.wallet.toLowerCase() === me.wallet && x.status !== 'failed').map(x => x.missionId)) : new Set();
return json(res, 200, { me: { memberId: me.memberId, username: me.username, wallet: me.wallet }, missions: missions.forMember(me.memberId).map(m => Object.assign(pubMission(m), { done: done.has(m.id) || wdone.has(m.id) })), drips: rewards.mine(me.memberId).slice(0, 20), faucet: { on: faucetOn }, pool: rewards.pool() });
return json(res, 200, { me: { memberId: me.memberId, username: me.username, wallet: me.wallet }, missions: missions.forMember(me.memberId).map(m => Object.assign(pubMission(m), { done: done.has(m.id) || wdone.has(m.id) })), drips: rewards.mine(me.memberId).slice(0, 20), faucet: { on: faucetOn }, pool: rewards.pool(),
badges: social.badgesFor(me.memberId), rank: { week: social.rankOf(me.memberId, 'week'), month: social.rankOf(me.memberId, 'month'), all: social.rankOf(me.memberId, 'all') },
share: { link: social.shareLink(SITE, me), joinUrl: 'https://instantadpay.com/join/' + encodeURIComponent(String(me.username || me.memberId)) } });
}
if (p === '/api/my/start' && req.method === 'POST') {
const b = await readBody(req); const m = missions.get(String(b.missionId || '')); if (!m || !m.active) return json(res, 404, { error: 'That mission is not open.' });