Member milestones: a celebration photo + caption to the proof channel, team feed and shared payments topic when the contract reaches 1,000 members (then 2,500 / 5,000 / 10,000), once per milestone; sendPhoto capability; admin preview route
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -160,6 +160,51 @@ function submitRateLimited(ip) {
|
||||
// Post to the team Telegram. topicId overrides the default team-build topic
|
||||
// (config.telegramTopicId) — used to fan the same event out to a second forum
|
||||
// topic (e.g. the recruiting/new-members topic) with different copy.
|
||||
// A photo with a caption to any chat/topic (member milestones). Node 22: FormData + Blob are built in.
|
||||
async function sendTelegramPhotoTo(chatId, filePath, caption, topicId) {
|
||||
const c = getConfig();
|
||||
if (!c.telegramBotToken || !chatId) return false;
|
||||
try {
|
||||
const fd = new FormData(); fd.append('chat_id', String(chatId)); fd.append('caption', caption); fd.append('parse_mode', 'HTML');
|
||||
if (topicId && /^[0-9]+$/.test(String(topicId))) fd.append('message_thread_id', String(topicId));
|
||||
fd.append('photo', new Blob([fs.readFileSync(filePath)], { type: 'image/jpeg' }), path.basename(filePath));
|
||||
const r = await fetch(`https://api.telegram.org/bot${c.telegramBotToken}/sendPhoto`, { method: 'POST', body: fd });
|
||||
if (!r.ok) console.warn('telegram sendPhoto', r.status, (await r.text()).slice(0, 200));
|
||||
return r.ok;
|
||||
} catch (e) { console.warn('telegram sendPhoto', e.message); return false; }
|
||||
}
|
||||
// Member milestones (Marty, 2026-09-21): a celebration in the proof channel, the team feed and the shared
|
||||
// payments topic when the contract passes 1,000 members (then 2,500 / 5,000 / 10,000). Once per milestone.
|
||||
const MILESTONES_FILE = path.join(DATA_DIR, 'milestones.json');
|
||||
function milestoneCaption(n, total) {
|
||||
const site = String(getConfig().siteName || 'The RM Circle');
|
||||
return '\u{1F389} <b>' + n.toLocaleString('en-US') + ' members.</b> ' + site + ' just reached ' + n.toLocaleString('en-US') + ' positions on the contract (' + total.toLocaleString('en-US') + ' and counting), every one of them registered on Polygon and every payout between them settled wallet to wallet, in public.\n\nTo everyone who brought their two, and taught their two to do the same: this is your number. \u{1F64F}\n\nNext stop: ' + (n >= 10000 ? 'the next ten thousand' : (n === 1000 ? '2,500' : n === 2500 ? '5,000' : '10,000')) + '.';
|
||||
}
|
||||
let milestoneBusy = false;
|
||||
async function milestoneCheck() {
|
||||
if (milestoneBusy) return; milestoneBusy = true;
|
||||
try {
|
||||
const c = getConfig(); if (String(c.milestonesEnabled == null ? '1' : c.milestonesEnabled) !== '1') return;
|
||||
const root = Number(c.orgRootId || 21); const d = chain.getOrgShare(root); if (!d || !d.ready) return;
|
||||
const total = Number(d.totalMembers) || 0;
|
||||
const list = String(c.memberMilestones || '1000,2500,5000,10000').split(',').map(n => Number(n.trim())).filter(n => n > 0);
|
||||
let st = {}; try { st = readJson(MILESTONES_FILE); } catch (e) {}
|
||||
st.posted = st.posted || {};
|
||||
for (const n of list) {
|
||||
if (total < n || st.posted[n]) continue;
|
||||
const caption = milestoneCaption(n, total);
|
||||
const img = path.join(ROOT, 'public', 'celebrate-' + n + '.jpg');
|
||||
const havePhoto = fs.existsSync(img);
|
||||
const post = async (chatId, topicId) => { if (!chatId) return; if (havePhoto) { if (await sendTelegramPhotoTo(chatId, img, caption, topicId)) return; } sendTelegramTo(chatId, caption, topicId, null, 'HTML', 'milestone:' + n + ':' + String(chatId) + ':' + String(topicId || '')); };
|
||||
await post(c.telegramProofChatId, c.telegramProofTopicId || null); // the public payment-proof channel
|
||||
await post(c.telegramChatId, c.telegramTopicId || null); // the team feed
|
||||
if (c.telegramEchoChatId || c.telegramEchoTopicId) await post(c.telegramEchoChatId || c.telegramChatId, c.telegramEchoTopicId || null); // the shared payments topic
|
||||
st.posted[n] = { at: Date.now(), total }; writeJson(MILESTONES_FILE, st);
|
||||
console.log('member milestone posted:', n, 'total', total, havePhoto ? 'with photo' : 'text only');
|
||||
}
|
||||
} catch (e) { console.error('milestone check', e.message); } finally { milestoneBusy = false; }
|
||||
}
|
||||
setTimeout(() => milestoneCheck().catch(() => {}), 90000).unref(); // once after boot, when the index is warm
|
||||
function sendTelegram(text, topicId, replyMarkup, dedupeKey) {
|
||||
const c = getConfig();
|
||||
return sendTelegramTo(c.telegramChatId, text, topicId != null ? topicId : c.telegramTopicId, replyMarkup, undefined, dedupeKey);
|
||||
@@ -748,6 +793,7 @@ async function handleApi(req,res,pathname){
|
||||
const limit=Number(q.get('limit')||40);
|
||||
return json(res,200,chain.getPayoutsPublic(offset,limit),{'Cache-Control':'public, max-age=20'});
|
||||
}
|
||||
if(req.method==='GET'&&pathname==='/api/admin/milestone/preview'){ if(!requireAdmin(req,res))return; const c=getConfig(); const d=chain.getOrgShare(Number(c.orgRootId||21)); const n=Number(new URL(req.url,'http://x').searchParams.get('n')||1000); let st={}; try{st=readJson(MILESTONES_FILE);}catch(e){} return json(res,200,{total:d&&d.totalMembers,n,caption:milestoneCaption(n,(d&&d.totalMembers)||n),photo:fs.existsSync(path.join(ROOT,'public','celebrate-'+n+'.jpg')),posted:(st.posted||{})[n]||null,targets:{proof:!!c.telegramProofChatId,team:!!c.telegramChatId,echo:!!(c.telegramEchoChatId||c.telegramEchoTopicId)}}); }
|
||||
if(req.method==='GET'&&pathname==='/api/public/org-stats'){
|
||||
const root=Number(getConfig().orgRootId||21);
|
||||
const d=chain.getOrgShare(Number.isInteger(root)&&root>0?root:21);
|
||||
@@ -1881,6 +1927,7 @@ chain.startIndexer(evt=>{
|
||||
// the next fetch instead of up to 2 minutes later.
|
||||
try{memberCache.clear();}catch(e){}
|
||||
try{tgbot.notifyEvent(evt);}catch(e){}
|
||||
if(evt.type==='registered'){ setTimeout(()=>milestoneCheck().catch(()=>{}), 5000); } // after the indexer wires the new member
|
||||
try{
|
||||
const c=getConfig();
|
||||
const EK=tgEventKey(evt); // one id for this on-chain event, so no feed can post it twice
|
||||
|
||||
Reference in New Issue
Block a user