Milestones post a card, and a dead target stops being silent
The 600-member milestone went out as plain text into the shared payments topic and Marty scrolled straight past it. Every hundred now posts a picture. The cards are pre-rendered for the whole ladder (600 through 10,000) and carry no live figures on purpose: a number baked into an image rendered weeks early is wrong by the time the mark is reached, so the numbers stay in the caption. The send falls back to plain text if the card for a mark is missing or the caption runs past Telegram's 1024. Separately: a target listed in snapshotTargets with no chat id was skipped without a word, so the "feed" half of every milestone and every daily snapshot has been going nowhere for weeks while the log said it sent. It now warns. telegramChatId is still unset, so feed is still dark until Marty says where it should point, but at least it says so. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 110 KiB |
|
After Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 107 KiB |
@@ -447,7 +447,9 @@ async function boot() {
|
||||
coach.init({ dataDir: DATA_DIR, chain, accounts, mailer, ads, tank, lb: () => leaderboard, siteConfig });
|
||||
pipeline.init({ dataDir: DATA_DIR, accounts, coach, chain });
|
||||
badge.init({ publicDir: PUBLIC_DIR });
|
||||
snapshot.init({ dataDir: DATA_DIR, db, chain, siteConfig, send: async (c, t, th) => { await telegramSend(c, t, th); return true; } });
|
||||
snapshot.init({ dataDir: DATA_DIR, db, chain, siteConfig, publicDir: PUBLIC_DIR,
|
||||
send: async (c, t, th) => { await telegramSend(c, t, th); return true; },
|
||||
sendPhoto: (c, jpeg, cap, th) => telegramSendPhoto(c, jpeg, cap, th) });
|
||||
friday.init({ dataDir: DATA_DIR, siteConfig, chain, ads, accounts, mailer, drip,
|
||||
telegram: async text => { const sc = siteConfig(); if (!sc.telegramBotToken || !sc.telegramEchoChatId) return false; return telegramSend(sc.telegramEchoChatId, '\u{1F7E0} <b>InstantAdPay</b> \u00b7 ' + text, sc.telegramEchoTopicId); },
|
||||
notify: async (memberId, subject, text, kind) => { const a = await accounts.byMemberId(memberId); if (!a || !a.email) return; const html = '<p>' + String(text).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/(https:\/\/[^\s]+)/g, '<a href="$1" target="_blank" rel="noopener">$1</a>').split('\n\n').join('</p><p>').replace(/\n/g, '<br>') + '</p>'; await messages.deliver(1, ADMIN_EMAIL || 'house@instantadpay.com', [a.email], subject, html, kind || 'notice'); } });
|
||||
@@ -1189,7 +1191,15 @@ const server = http.createServer(async (req, res) => {
|
||||
const promo = promos.norm(u.searchParams.get('promo')); if (promo) set.push('iap.promo=' + promo + cookieTail); // partner code, redeemed at signup
|
||||
if (promo) { const pref = String(req.headers.referer || '').replace(/^https?:\/\//, '').split('/')[0].toLowerCase().slice(0, 80); if (pref) set.push('iap.promoref=' + encodeURIComponent(pref) + cookieTail); } // which partner page it came from
|
||||
if (ang) set.push('iap.angle=' + angle + cookieTail);
|
||||
if (!cookies['iap.ref']) set.push('iap.ref=' + encodeURIComponent(coach.refHost(req.headers.referer)) + cookieTail); // first-touch source
|
||||
// First-touch source. A PolHunter share link carries ?from=polhunter in the URL itself, which
|
||||
// survives the referrer being stripped (in-app browsers, a link pasted into chat). PolHunter
|
||||
// pays a referral bounty only for people it actually sent, so that proof has to be reliable
|
||||
// rather than dependent on a header (Marty, 2026-09-24: "not just members that come on board
|
||||
// IAP and work their way over to POL Hunter").
|
||||
if (!cookies['iap.ref']) {
|
||||
const host = u.searchParams.get('from') === 'polhunter' ? 'polhunter.com' : coach.refHost(req.headers.referer);
|
||||
set.push('iap.ref=' + encodeURIComponent(host) + cookieTail);
|
||||
}
|
||||
return serveJoinPage(res, tok, ang ? angle : '', ang, set);
|
||||
}
|
||||
// -- legacy bridge: /from/faucetwave | /from/tieroneads [?seg=advertiser]. Same squeeze page
|
||||
@@ -2246,7 +2256,9 @@ const server = http.createServer(async (req, res) => {
|
||||
// person who gets it has to be the one this member actually joined under here, not a cookie
|
||||
// PolHunter guessed at. sponsorRef is the upline's username or code, matching /join/<ref>.
|
||||
let sponsorRef = (acct && acct.sponsorRef) || '';
|
||||
const payload = JSON.stringify({ iat: Date.now(), exp: Date.now() + 5 * 60000, nonce: crypto.randomBytes(12).toString('hex'), memberId: Number(memberId), email: s.email, wallet: (acct && acct.address) || s.address || null, username: (acct && acct.username) || null, sponsorRef: sponsorRef || null, joinedAt: (acct && acct.created) || null });
|
||||
const payload = JSON.stringify({ iat: Date.now(), exp: Date.now() + 5 * 60000, nonce: crypto.randomBytes(12).toString('hex'), memberId: Number(memberId), email: s.email, wallet: (acct && acct.address) || s.address || null, username: (acct && acct.username) || null, sponsorRef: sponsorRef || null, joinedAt: (acct && acct.created) || null,
|
||||
// where they first arrived from, so PolHunter can pay a bounty only for people it sent
|
||||
joinedRef: (acct && acct.joinedRef) || null });
|
||||
const tok = b64u(payload) + '.' + b64u(crypto.createHmac('sha256', secret).update(payload).digest());
|
||||
res.writeHead(302, { Location: huntUrl + '/auth?t=' + tok, 'Cache-Control': 'no-store', 'Set-Cookie': 'iap.return=; Path=/; SameSite=Lax; Max-Age=0' + (IS_PROD ? '; Secure' : '') }); return res.end(); // the loop is closed
|
||||
}
|
||||
|
||||
@@ -160,9 +160,26 @@ async function milestoneTick() {
|
||||
const mark = due[due.length - 1];
|
||||
const text = composeMilestone(mark, await gather(), sc.telegramCtaUrl);
|
||||
const targets = String(sc.snapshotTargets || 'feed,echo').split(',').map(x => x.trim());
|
||||
// A milestone read as plain text in a busy topic and got scrolled straight past (Marty,
|
||||
// 2026-09-24), so it goes out as a card now. The cards are pre-rendered for the whole ladder
|
||||
// and deliberately carry NO live figures: the numbers stay in the caption, where they cannot
|
||||
// go stale between rendering and the mark actually being reached.
|
||||
let jpeg = null;
|
||||
try { jpeg = fs.readFileSync(path.join(X.publicDir || '', 'milestones', mark + '.jpg')); } catch (e) {}
|
||||
const caption = text.length <= 1024 ? text : null; // Telegram caps a photo caption at 1024
|
||||
let sent = 0;
|
||||
if (targets.includes('feed') && sc.telegramChatId) { if (await X.send(sc.telegramChatId, text, sc.telegramTopicId)) sent++; }
|
||||
if (targets.includes('echo') && sc.telegramEchoChatId) { if (await X.send(sc.telegramEchoChatId, text, sc.telegramEchoTopicId)) sent++; }
|
||||
const post = async (chat, thread) => {
|
||||
if (jpeg && caption && X.sendPhoto && await X.sendPhoto(chat, jpeg, caption, thread)) return true;
|
||||
return X.send(chat, text, thread);
|
||||
};
|
||||
// A target that is listed but has no chat id used to fail silently, so the feed half of every
|
||||
// milestone and every daily snapshot went nowhere for weeks without a line in the log.
|
||||
for (const [name, chat, thread] of [['feed', sc.telegramChatId, sc.telegramTopicId],
|
||||
['echo', sc.telegramEchoChatId, sc.telegramEchoTopicId]]) {
|
||||
if (!targets.includes(name)) continue;
|
||||
if (!chat) { console.warn('snapshot: target "' + name + '" is listed but has no chat id, nothing sent'); continue; }
|
||||
if (await post(chat, thread)) sent++;
|
||||
}
|
||||
for (const m of due) done[m] = Date.now(); // lower marks crossed in the same jump are marked too, so they never post late
|
||||
st.milestones = done; setState(st);
|
||||
console.log('milestone posted', mark, 'members', count, 'sent', sent);
|
||||
|
||||