Every completed mission reaches Telegram: a paid find marks itself posted, a two-minute sweep posts any paid find not yet posted (pre-live gate, Telegram down, restart), HUNT_POST_SINCE keeps rehearsal finds quiet
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
+6
-1
@@ -79,6 +79,11 @@ function payable() {
|
|||||||
for (const p of all.filter(p => p.status === 'queued').sort((a, b) => a.at - b.at)) { if (p.pol <= room) { room -= p.pol; out.push(p); } else break; }
|
for (const p of all.filter(p => p.status === 'queued').sort((a, b) => a.at - b.at)) { if (p.pol <= room) { room -= p.pol; out.push(p); } else break; }
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
// paid drips whose find has not been posted yet (found at or after `since`), oldest paid first
|
||||||
|
function unposted(since, limit) {
|
||||||
|
return store.read('payouts', []).filter(p => p.status === 'paid' && p.tx && !p.posted && (p.at || 0) >= (since || 0))
|
||||||
|
.sort((a, b) => (a.paidAt || a.at) - (b.paidAt || b.at)).slice(0, Math.max(1, limit || 15));
|
||||||
|
}
|
||||||
function mark(id, patch) { return store.update('payouts', [], all => { const p = all.find(x => x.id === id); if (p) Object.assign(p, patch); return all; }); }
|
function mark(id, patch) { return store.update('payouts', [], all => { const p = all.find(x => x.id === id); if (p) Object.assign(p, patch); return all; }); }
|
||||||
|
|
||||||
// the day's pool: committed POL (paid, sent, due, queued; never failed) against the cap, and when it
|
// the day's pool: committed POL (paid, sent, due, queued; never failed) against the cap, and when it
|
||||||
@@ -102,4 +107,4 @@ function totals() {
|
|||||||
return { paid: paid.length, pol: Math.round(paid.reduce((n, p) => n + p.pol, 0) * 10000) / 10000, queued: all.filter(p => p.status === 'queued').length, today: paidToday(ctDay()), hunters: new Set(paid.map(p => p.memberId)).size };
|
return { paid: paid.length, pol: Math.round(paid.reduce((n, p) => n + p.pol, 0) * 10000) / 10000, queued: all.filter(p => p.status === 'queued').length, today: paidToday(ctDay()), hunters: new Set(paid.map(p => p.memberId)).size };
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { settings, setSettings, draw, grant, completed, doneToday, payable, mark, ledger, mine, totals, paidToday, pool, nextResetAt, daily, findsToday };
|
module.exports = { settings, setSettings, draw, grant, completed, doneToday, payable, mark, ledger, mine, totals, paidToday, pool, nextResetAt, daily, findsToday, unposted };
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ async function telegramPhoto(file, caption, general) {
|
|||||||
}
|
}
|
||||||
const fmt = n => Number(n).toLocaleString('en-US', { maximumFractionDigits: 4 });
|
const fmt = n => Number(n).toLocaleString('en-US', { maximumFractionDigits: 4 });
|
||||||
async function notify(kind, p) {
|
async function notify(kind, p) {
|
||||||
if (kind === 'paid') return 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 (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 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; }
|
||||||
@@ -270,6 +270,19 @@ const server = http.createServer(async (req, res) => {
|
|||||||
|
|
||||||
// the faucet pays every two minutes; nothing outward leaves unless OUTBOUND=on (telegram checks)
|
// the faucet pays every two minutes; nothing outward leaves unless OUTBOUND=on (telegram checks)
|
||||||
if (faucetOn) setInterval(() => faucet.tick(notify).catch(e => console.error('faucet', e.message)), 2 * 60000);
|
if (faucetOn) setInterval(() => faucet.tick(notify).catch(e => console.error('faucet', e.message)), 2 * 60000);
|
||||||
|
// Every completed mission reaches Telegram (Marty, 2026-09-21): a find is posted when its drip is paid, and
|
||||||
|
// this sweep posts any paid find that is not marked posted yet (the pre-live gate, Telegram down, a restart).
|
||||||
|
// Only finds made after HUNT_POST_SINCE count, so rehearsal drips from before the doors opened stay quiet.
|
||||||
|
const POST_SINCE = process.env.HUNT_POST_SINCE ? Date.parse(process.env.HUNT_POST_SINCE) : 0;
|
||||||
|
let sweeping = false;
|
||||||
|
async function postMissedFinds() {
|
||||||
|
if (sweeping || !outbound()) return 0; sweeping = true; let n = 0;
|
||||||
|
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; }
|
||||||
|
if (n) console.log('posted', n, 'missed find(s) to Telegram');
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
setInterval(() => postMissedFinds().catch(() => {}), 2 * 60000); setTimeout(() => postMissedFinds().catch(() => {}), 20000);
|
||||||
// weekly prizes: the week that just ended is awarded on the first tick after Sunday, Central
|
// weekly prizes: the week that just ended is awarded on the first tick after Sunday, Central
|
||||||
setInterval(() => { try { social.awardDue(notify); } catch (e) { console.error('prizes', e.message); } }, 2 * 60000);
|
setInterval(() => { try { social.awardDue(notify); } catch (e) { console.error('prizes', e.message); } }, 2 * 60000);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user