Badges reach Telegram too: a posted badge is recorded, and the sweep posts any unrecorded badge of every hunter active since the cutoff (gated, Telegram down, restart)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-21 08:46:58 -05:00
parent 48c334dd73
commit 1c2cd11453
+20 -1
View File
@@ -62,8 +62,13 @@ async function notify(kind, p) {
if (kind === 'paid') { const ok = await telegram('\u{1F3AF} <b>PolHunter</b> · ' + (p.username ? '@' + p.username : '#' + p.memberId) + ' found it on ' + p.site + ' and got <b>' + fmt(p.pol) + ' POL</b> · <a href="' + explorer() + '/tx/' + p.tx + '">verify</a>\n<a href="' + SITE + '">Hunt yours</a>'); if (ok && p.id) rewards.mark(p.id, { posted: Date.now() }); return ok; } if (kind === 'paid') { const ok = await telegram('\u{1F3AF} <b>PolHunter</b> · ' + (p.username ? '@' + p.username : '#' + p.memberId) + ' found it on ' + p.site + ' and got <b>' + fmt(p.pol) + ' POL</b> · <a href="' + explorer() + '/tx/' + p.tx + '">verify</a>\n<a href="' + SITE + '">Hunt yours</a>'); if (ok && p.id) rewards.mark(p.id, { posted: Date.now() }); return ok; }
if (kind === 'low') return telegram('⚠️ <b>PolHunter faucet is low</b>: ' + fmt(p.balance) + ' POL left in ' + p.address + ' (alert threshold ' + fmt(p.threshold) + '). Top up from Receiver B.'); if (kind === 'low') return telegram('⚠️ <b>PolHunter faucet is low</b>: ' + fmt(p.balance) + ' POL left in ' + p.address + ' (alert threshold ' + fmt(p.threshold) + '). Top up from Receiver B.');
if (kind === 'failed') return telegram('❌ <b>PolHunter</b> · drip to #' + p.memberId + ' failed: ' + p.error); if (kind === 'failed') return telegram('❌ <b>PolHunter</b> · drip to #' + p.memberId + ' failed: ' + p.error);
if (kind === 'badge') { const b = social.BADGES.find(x => x.id === p.id); if (!b) return false; const who = social.nameOf(p.me); const file = await badge.render(p.id, p.me.username || '#' + p.me.memberId); const cap = '\u{1F3C6} <b>PolHunter</b> \u00b7 <b>' + (p.me.username ? '@' + p.me.username : '#' + p.me.memberId) + '</b> unlocked <b>' + b.name + '</b>: ' + b.why + '\n' + SITE + '/b/' + who + '/' + p.id; if (!file) return telegram(cap); const ok = await telegramPhoto(file, cap); if (String(process.env.HUNT_TG_BADGE_GENERAL || 'on') === 'on') await telegramPhoto(file, cap, true); return ok; } if (kind === 'badge') { const ok = await postBadge(p); if (ok) store.update('badges-posted', {}, all => { const k = String(p.me.memberId); all[k] = Array.from(new Set([...(all[k] || []), p.id])); return all; }); return ok; }
if (kind === 'prize') { const medal = ['\u{1F947}', '\u{1F948}', '\u{1F949}']; return telegram('\u{1F3C6} <b>PolHunter weekly prizes</b> for the week of ' + p.week + '\n' + p.winners.map(w => (medal[w.rank - 1] || '#' + w.rank) + ' ' + w.who + ' \u00b7 ' + w.finds + ' finds \u00b7 <b>' + fmt(w.prizePol) + ' POL</b>').join('\n') + '\n<a href="' + SITE + '/leaders">Leaderboard</a>'); } if (kind === 'prize') { const medal = ['\u{1F947}', '\u{1F948}', '\u{1F949}']; return telegram('\u{1F3C6} <b>PolHunter weekly prizes</b> for the week of ' + p.week + '\n' + p.winners.map(w => (medal[w.rank - 1] || '#' + w.rank) + ' ' + w.who + ' \u00b7 ' + w.finds + ' finds \u00b7 <b>' + fmt(w.prizePol) + ' POL</b>').join('\n') + '\n<a href="' + SITE + '/leaders">Leaderboard</a>'); }
return false;
}
async function postBadge(p) {
{ const b = social.BADGES.find(x => x.id === p.id); if (!b) return false; const who = social.nameOf(p.me); const file = await badge.render(p.id, p.me.username || '#' + p.me.memberId); const cap = '\u{1F3C6} <b>PolHunter</b> \u00b7 <b>' + (p.me.username ? '@' + p.me.username : '#' + p.me.memberId) + '</b> unlocked <b>' + b.name + '</b>: ' + b.why + '\n' + SITE + '/b/' + who + '/' + p.id; if (!file) return telegram(cap); const ok = await telegramPhoto(file, cap); if (String(process.env.HUNT_TG_BADGE_GENERAL || 'on') === 'on') await telegramPhoto(file, cap, true); return ok; }
return false;
} }
// Marty's rule (2026-09-19): when the day's pool is spent, say so; claims reopen at midnight Central // Marty's rule (2026-09-19): when the day's pool is spent, say so; claims reopen at midnight Central
const dailyMsg = d => 'That is your ' + ['', 'one', 'two', 'three', 'four', 'five'][d.limit] + ' for today. Fresh missions at midnight Central.'; const dailyMsg = d => 'That is your ' + ['', 'one', 'two', 'three', 'four', 'five'][d.limit] + ' for today. Fresh missions at midnight Central.';
@@ -286,6 +291,20 @@ async function postMissedFinds() {
try { for (const p of rewards.unposted(POST_SINCE, 15)) { if (await notify('paid', p)) n++; else break; } } try { for (const p of rewards.unposted(POST_SINCE, 15)) { if (await notify('paid', p)) n++; else break; } }
catch (e) { console.error('find sweep', e.message); } finally { sweeping = false; } catch (e) { console.error('find sweep', e.message); } finally { sweeping = false; }
if (n) console.log('posted', n, 'missed find(s) to Telegram'); if (n) console.log('posted', n, 'missed find(s) to Telegram');
await postMissedBadges().catch(e => console.error('badge sweep', e.message));
return n;
}
// badges of every hunter active since the cutoff that were never posted (gated, Telegram down, restart)
async function postMissedBadges() {
if (!outbound()) return 0;
const since = POST_SINCE; const recs = store.read('payouts', []).filter(x => (x.at || 0) >= since && x.status !== 'failed');
const seen = new Map(); for (const r of recs) seen.set(r.memberId, { memberId: r.memberId, username: r.username || null });
const posted = store.read('badges-posted', {}); let n = 0;
for (const me of seen.values()) {
const have = new Set(posted[String(me.memberId)] || []);
for (const b of social.badgesFor(me.memberId)) { if (have.has(b.id)) continue; if (n >= 10) return n; if (await notify('badge', { me, id: b.id })) n++; else return n; }
}
if (n) console.log('posted', n, 'missed badge(s) to Telegram');
return n; return n;
} }
setInterval(() => postMissedFinds().catch(() => {}), 2 * 60000); setTimeout(() => postMissedFinds().catch(() => {}), 20000); setInterval(() => postMissedFinds().catch(() => {}), 2 * 60000); setTimeout(() => postMissedFinds().catch(() => {}), 20000);