diff --git a/public/assets/admin.js b/public/assets/admin.js index b1cf68a..bb69203 100644 --- a/public/assets/admin.js +++ b/public/assets/admin.js @@ -485,7 +485,7 @@ })); // site settings: key / value rows; booleans as checkboxes, numbers stay numbers - const SITE_META = { siteName: 'Site name', tagline: 'Tagline', rehearsal: 'Testnet rehearsal banner', defaultSponsorId: 'Fallback sponsor (member #)', walletConnectProjectId: 'WalletConnect project id', moonpayPublicKey: 'MoonPay public key', telegramBotToken: 'Telegram proof feed: bot token', telegramChatId: 'Telegram proof feed: chat id', telegramTopicId: 'Telegram proof feed: topic id (optional)', telegramEvents: 'Telegram proof feed: events (payouts | payouts+purchases | all)', telegramCtaUrl: 'Telegram proof feed: join link under each post', pnlFixedMonthlyUsd: 'P&L: fixed monthly cost (USD)' }; + const SITE_META = { siteName: 'Site name', tagline: 'Tagline', rehearsal: 'Testnet rehearsal banner', defaultSponsorId: 'Fallback sponsor (member #)', walletConnectProjectId: 'WalletConnect project id', moonpayPublicKey: 'MoonPay public key', telegramBotToken: 'Telegram proof feed: bot token', telegramChatId: 'Telegram proof feed: chat id', telegramTopicId: 'Telegram proof feed: topic id (optional)', telegramEvents: 'Telegram proof feed: events (payouts | payouts+purchases | all)', telegramCtaUrl: 'Telegram proof feed: join link under each post', telegramEchoChatId: 'Telegram echo (shared payments topic): chat id', telegramEchoTopicId: 'Telegram echo: topic id', telegramEchoEvents: 'Telegram echo: events (payouts | payouts+purchases | all)', pnlFixedMonthlyUsd: 'P&L: fixed monthly cost (USD)' }; function drawSite() { const wrap = $('siteForm'); wrap.innerHTML = Object.entries(siteObj).map(([k, v]) => '
' + esc(SITE_META[k] || humanize(k)) + '' diff --git a/server.js b/server.js index a24f9a1..81105cc 100644 --- a/server.js +++ b/server.js @@ -339,6 +339,7 @@ function siteConfig() { rehearsal: true, // shows the testnet banner; flipped off at mainnet launch // payment-proof Telegram feed (blank = off) and the P&L pane's fixed monthly cost telegramBotToken: '', telegramChatId: '', telegramTopicId: '', telegramEvents: 'payouts', telegramCtaUrl: 'https://instantadpay.com/', + telegramEchoChatId: '', telegramEchoTopicId: '', telegramEchoEvents: 'payouts', // shared cross-program payments topic telegramAdminChatId: '', // private chat for admin alerts (sign-up guard bursts); falls back to ADMIN_EMAIL launchAt: '', // public launch moment, ISO 8601 with offset (e.g. 2026-09-18T19:00:00-05:00): countdown on /launch + dashboard mark geoTier1: '', // comma-separated ISO country codes; empty = built-in default (US, CA, GB, AU, NZ, IE, DE, FR, NL, SE, NO, DK, FI, CH, AT, BE) @@ -526,22 +527,29 @@ async function emailOnEvent(ev) { // payment-proof Telegram feed (same pattern as the RM Circle proof channel): one compact // line per event, admin-configured under Settings > Site (telegramBotToken, telegramChatId, // optional telegramTopicId, telegramEvents = payouts | payouts+purchases | all, telegramCtaUrl) +// A second target, telegramEchoChatId + telegramEchoTopicId (+ telegramEchoEvents), echoes +// the same lines into a shared cross-program payments topic that RM Circle also posts to, +// so every line there carries the program name. async function telegramOnEvent(ev) { const sc = siteConfig(); - if (!sc.telegramBotToken || !sc.telegramChatId || !ev) return; - const mode = String(sc.telegramEvents || 'payouts'); + if (!sc.telegramBotToken || !ev) return; + if (!sc.telegramChatId && !sc.telegramEchoChatId) return; const names = await accounts.namesForMembers([ev.recipientId, ev.buyerId, ev.sponsorId, ev.newBuyerId, ev.id].filter(Boolean)).catch(() => ({})); const who = id => '#' + id + (names[id] ? ' @' + names[id] : ''); const cc = chain.getConfig(); const tx = (cc.explorer ? cc.explorer.replace(/\/+$/, '') : 'https://polygonscan.com') + '/tx/' + ev.tx; + const build = (mode) => { let line = null; if (ev.type === 'TierPaid') line = '\u{1F4B8} Level ' + ev.tier + ' payout: ' + weiToPol(ev.amountWei) + ' POL \u2192 ' + who(ev.recipientId); else if (ev.type === 'BuyerCounted' && mode !== 'payouts') line = '\u2B50 ' + who(ev.sponsorId) + ' now has ' + ev.newCount + ' qualifying buyer' + (ev.newCount === 1 ? '' : 's') + (ev.newCount === 2 ? ' \u00b7 level 2 open' : ev.newCount === 5 ? ' \u00b7 level 3 open' : ''); else if (ev.type === 'Purchase' && mode !== 'payouts') line = '\u{1F9FE} ' + who(ev.buyerId) + ' bought a $' + Math.round(ev.priceCents / 100) + ' package'; + else if (ev.type === 'AwardPaid') line = '\u{1F4B8} Award payout: ' + weiToPol(ev.amountWei) + ' POL \u2192 ' + who(ev.toId); else if (ev.type === 'MemberActivated' && mode === 'all') line = '\u{1F91D} ' + who(ev.id) + ' switched on payouts'; - if (!line) return; - const text = line + ' \u00b7 verify' + (sc.telegramCtaUrl ? '\nJoin free' : ''); - await telegramSend(sc.telegramChatId, text, sc.telegramTopicId); + if (!line) return null; + return line + ' \u00b7 verify' + (sc.telegramCtaUrl ? '\nJoin free' : ''); + }; + if (sc.telegramChatId) { const t = build(String(sc.telegramEvents || 'payouts')); if (t) await telegramSend(sc.telegramChatId, t, sc.telegramTopicId); } + if (sc.telegramEchoChatId) { const t = build(String(sc.telegramEchoEvents || 'payouts')); if (t) await telegramSend(sc.telegramEchoChatId, '\u{1F7E0} InstantAdPay \u00b7 ' + t, sc.telegramEchoTopicId); } } // one sendMessage call; never throws, never logs the token async function telegramSend(chatId, text, threadId) {