diff --git a/grep.exe.stackdump b/grep.exe.stackdump new file mode 100644 index 0000000..0083e4f --- /dev/null +++ b/grep.exe.stackdump @@ -0,0 +1,31 @@ +Stack trace: +Frame Function Args +0007FFFFBBB0 0002100603A4 (0000000000B0, 0007FFFFDC10, 0007FFFFC730, 0007FFFFDC10) msys-2.0.dll+0x203A4 +0007FFFFC490 0002100625FE (0007FFFFC730, 000000000000, 000000000180, 000000000000) msys-2.0.dll+0x225FE +0007FFFFC490 0002100C1794 (0007FFFFC6F0, 000000000000, 000000000000, 0007FFFFC700) msys-2.0.dll+0x81794 +000000000006 0002100BD229 (7FFA00000346, 0007FFFFC700, 000000000000, 7FFA46EDAB50) msys-2.0.dll+0x7D229 +0007FFFFC848 0002100BD62A (0007FFFFC858, 000A00000000, 0000000000B5, 0000000000B5) msys-2.0.dll+0x7D62A +0007FFFFC848 000210213B18 (000000000000, 000A00000030, 000000000000, 000000000000) msys-2.0.dll+0x1D3B18 +0007FFFFC848 00010042A1E5 (0002100A7AB3, 000000000000, 000A00000520, 0000000001F0) grep.exe+0x2A1E5 +0007FFFFCB80 000100404B7C (000000000014, 0007FFFFCB80, 000000000000, 000000000000) grep.exe+0x4B7C +0007FFFFCB80 000100429678 (0002100455E0, 000000714950, 000000000148, 000000000000) grep.exe+0x29678 +0007FFFFCD30 000210047F11 (000000000000, 000000000000, 000000000000, 000000000000) msys-2.0.dll+0x7F11 +000000000000 000210045AC3 (000000000000, 000000000000, 000000000000, 000000000000) msys-2.0.dll+0x5AC3 +0007FFFFFFF0 000210045B74 (000000000000, 000000000000, 000000000000, 000000000000) msys-2.0.dll+0x5B74 +End of stack trace +Loaded modules: +000100400000 grep.exe +7FFA46D60000 ntdll.dll +7FFA045B0000 aswhook.dll +7FFA46060000 KERNEL32.DLL +7FFA44190000 KERNELBASE.dll +000430B30000 msys-intl-8.dll +0005603F0000 msys-iconv-2.dll +000210040000 msys-2.0.dll +0004C36D0000 msys-pcre-1.dll +7FFA46C10000 advapi32.dll +7FFA46B50000 msvcrt.dll +7FFA44B50000 sechost.dll +7FFA46180000 RPCRT4.dll +7FFA43520000 CRYPTBASE.DLL +7FFA449D0000 bcryptPrimitives.dll diff --git a/server.js b/server.js index 067abf7..3ec2e8a 100644 --- a/server.js +++ b/server.js @@ -20,6 +20,7 @@ messages.init({ dataDir: DATA_DIR, chain, isProd: IS_PROD }); const suiteMeter = require('./suite-meter'); suiteMeter.init({ dataDir: DATA_DIR }); const suiteAI = require('./suite-ai'); suiteAI.init({ dataDir: DATA_DIR }); const suitePages = require('./suite-pages'); suitePages.init({ dataDir: DATA_DIR }); +const suiteTools = require('./suite-tools'); const tgbot = require('./tgbot'); tgbot.init({ dataDir: DATA_DIR, chain, getConfig, messages, baseUrl: 'https://rmcircle.team' }); const SESSION_TTL = 8 * 60 * 60 * 1000; @@ -161,7 +162,9 @@ function recruitMsg(evt) { return `šŸ”„ The team just grew!\n\nA new member joined The RM Circle and locked in their position on-chain. The momentum is real.\n\nšŸ‘‰ Claim your spot at ${home} šŸš€\n\n${tags}`; } if (evt.type === 'upgraded') { - return `⚔ Level up!\n\nMember #${evt.id} just upgraded to ${evt.levelName} on The RM Circle — climbing the ranks and opening up bigger pass-ups.\n\nšŸ‘‰ Start building at ${home} šŸš€\n\n${tags}`; + const tools = suiteTools.unlockText(evt.level, evt.levelName, getConfig()); + const toolLine = tools ? `\n\nThat upgrade also unlocked ${tools} — every paid level extends the toolkit that comes with your position.` : ''; + return `⚔ Level up!\n\nMember #${evt.id} just upgraded to ${evt.levelName} on The RM Circle — climbing the ranks and opening up bigger pass-ups.${toolLine}\n\nšŸ‘‰ Start building at ${home} šŸš€\n\n${tags}`; } return ''; } @@ -953,7 +956,7 @@ async function handleApi(req,res,pathname){ const maxOrder=sponsors.reduce((m,s)=>Math.max(m,s.sortOrder||0),0);sponsors.push({id:String(id).trim(),name:String(name).trim(),parentId:String(parentId||'').trim(),directs:0,level,status:sponsors.some(s=>s.status==='active')?'waiting':'active',sortOrder:maxOrder+10,clicks:0,notes:String(notes||'').trim(),email:String(email||'').trim().slice(0,120)});sponsors=normalizeStatuses(sponsors);saveSponsors(sponsors);return json(res,201,{sponsors}); } if(req.method==='PATCH'&&pathname==='/api/admin/config'){ - const b=await bodyJson(req),cur=getConfig(),next={...cur};for(const k of ['siteName','programName','bridgeHeadline','bridgeSubheadline','premiumEntryPol','dappReferralBaseUrl','telegramUrl','supportLabel','showSponsorName','showQueueProgress','bemobPostbackUrl','telegramBotToken','companionBotToken','miniAppShortName','telegramChatId','telegramTopicId','telegramRecruitTopicId','teamRootId','emailFrom','teamAlertEmail','ownerIds','ownerAlertEmail','orgRootId','ctbOfferPostbackUrl','ctbOfferSecret','recruitCtaUrl','walletNotice','tweetEnabled','tweetCtaUrl','tweetHashtags','blotatoTwitterId','dappFallbackPublic','moonpayPublicKey','moonpaySecretKey','publicRotationMode','publicRotationRootId','rotationExcludeIds','suiteAllowlist'])if(Object.prototype.hasOwnProperty.call(b,k))next[k]=b[k];next.premiumEntryPol=Number(next.premiumEntryPol)||362;next.updatedAt=new Date().toISOString();writeJson(CONFIG_FILE,next);return json(res,200,{config:next}); + const b=await bodyJson(req),cur=getConfig(),next={...cur};for(const k of ['siteName','programName','bridgeHeadline','bridgeSubheadline','premiumEntryPol','dappReferralBaseUrl','telegramUrl','supportLabel','showSponsorName','showQueueProgress','bemobPostbackUrl','telegramBotToken','companionBotToken','miniAppShortName','telegramChatId','telegramTopicId','telegramRecruitTopicId','teamRootId','emailFrom','teamAlertEmail','ownerIds','ownerAlertEmail','orgRootId','ctbOfferPostbackUrl','ctbOfferSecret','recruitCtaUrl','walletNotice','tweetEnabled','tweetCtaUrl','tweetHashtags','blotatoTwitterId','dappFallbackPublic','moonpayPublicKey','moonpaySecretKey','publicRotationMode','publicRotationRootId','rotationExcludeIds','suiteAllowlist','suiteToolsInAlerts'])if(Object.prototype.hasOwnProperty.call(b,k))next[k]=b[k];next.premiumEntryPol=Number(next.premiumEntryPol)||362;next.updatedAt=new Date().toISOString();writeJson(CONFIG_FILE,next);return json(res,200,{config:next}); } const m=pathname.match(/^\/api\/admin\/sponsors\/([^/]+)(?:\/(increment|activate|qualify|reset|move))?$/); if(m){const id=decodeURIComponent(m[1]),action=m[2]||null;let sponsors=getSponsors(),idx=sponsors.findIndex(s=>s.id===id);if(idx<0)return json(res,404,{error:'Sponsor not found.'}); @@ -1106,7 +1109,7 @@ chain.startIndexer(evt=>{ const contact=id=>{const s=getSponsors().find(x=>String(x.id)===String(id));return s&&s.email?`\nContact: ${s.name?s.name+' — ':''}${s.email}`:''}; let text; if(evt.type==='registered')text=`šŸ“ˆ TEAM BUILD: new position!\n#${evt.id} registered under #${evt.referrerId} (${evt.tierName}).`; - else if(evt.type==='upgraded')text=`šŸš€ TEAM BUILD: #${evt.id} upgraded to ${evt.levelName}.`; + else if(evt.type==='upgraded'){const tl=suiteTools.unlockText(evt.level,evt.levelName,getConfig());text=`šŸš€ TEAM BUILD: #${evt.id} upgraded to ${evt.levelName}.${tl?`\n🧰 Unlocked: ${tl}.`:''}`;} else text=`šŸ’ø TEAM BUILD: #${evt.toId} just got PAID ${evt.pol.toFixed(2)} POL${evt.kind==='upline'?` (${evt.gen?`Gen ${evt.gen} `:''}upgrade pass-up from #${evt.fromId})`:` (referral reward from #${evt.fromId})`}.${contact(evt.toId)}`; if(evt.tx)text+=`\nhttps://polygonscan.com/tx/${evt.tx}`; sendTelegram(text); diff --git a/suite-tools.js b/suite-tools.js new file mode 100644 index 0000000..5031361 --- /dev/null +++ b/suite-tools.js @@ -0,0 +1,66 @@ +// One source of truth for "what does this level unlock in the Circle Suite", +// shared by the Telegram feeds, the X/Twitter payout posts, and anywhere else +// that congratulates an upgrade. +// +// Two honesty rules baked in: +// 1. `live:false` tiers are NEVER announced. We do not congratulate someone +// for unlocking software that does not exist yet. +// 2. The whole feature is gated on config.suiteToolsInAlerts, so nothing goes +// public while the Suite is still in team beta. Flip it on at launch. +'use strict'; + +const LEVEL_TOOLS = { + 1: { name: 'Scintilla', live: true, short: 'the Launch Kit — promo center, printable handouts with your QR, the Circle Method course, your dashboard and the AI coach' }, + 2: { name: 'Ascensus', live: true, short: 'the Copy Engine and Page Builder — an AI copywriter for your posts and DMs, plus your own hosted invitation page' }, + 3: { name: 'Fabrica', live: false, short: 'the Email Engine and Video Maker' }, + 4: { name: 'Culmen', live: false, short: 'your own Voice Profile and multi-page funnels' }, + 5: { name: 'Apex', live: false, short: 'the Traffic Desk — syndicated network display advertising' }, + 6: { name: 'Fastigium', live: false, short: 'the Funnel Factory and Replay Funnels' }, + 7: { name: 'Vertex', live: false, short: 'Leader Ops — team radar and coaching digests' }, + 8: { name: 'Corona', live: false, short: 'the Founder Desk — your own AI operator' } +}; + +// Very short form for character-limited surfaces (X/Twitter). +const TERSE = { + 1: 'the Launch Kit', + 2: 'the Copy Engine + Page Builder', + 3: 'the Email Engine + Video Maker', + 4: 'Voice Profile + funnels', + 5: 'the Traffic Desk', + 6: 'the Funnel Factory', + 7: 'Leader Ops', + 8: 'the Founder Desk' +}; + +function levelNumber(level, levelName) { + const n = Number(level); + if (n >= 1 && n <= 8) return n; + if (levelName) { + for (const k of Object.keys(LEVEL_TOOLS)) { + if (LEVEL_TOOLS[k].name.toLowerCase() === String(levelName).toLowerCase()) return Number(k); + } + } + return 0; +} + +function enabled(cfg) { return !!(cfg && cfg.suiteToolsInAlerts); } + +// Full sentence fragment, or '' when we shouldn't say anything. +function unlockText(level, levelName, cfg) { + if (!enabled(cfg)) return ''; + const n = levelNumber(level, levelName); + const t = LEVEL_TOOLS[n]; + if (!t || !t.live) return ''; + return t.short; +} + +// Terse form for tweets, or '' when we shouldn't say anything. +function unlockTerse(level, levelName, cfg) { + if (!enabled(cfg)) return ''; + const n = levelNumber(level, levelName); + const t = LEVEL_TOOLS[n]; + if (!t || !t.live) return ''; + return TERSE[n] || ''; +} + +module.exports = { LEVEL_TOOLS, unlockText, unlockTerse, levelNumber, enabled }; diff --git a/tgbot.js b/tgbot.js index 6b0ce02..4e33a10 100644 --- a/tgbot.js +++ b/tgbot.js @@ -8,6 +8,7 @@ const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); +const suiteTools = require('./suite-tools'); let DATA_DIR = '', chain = null, getConfig = null, messages = null; const FILE = () => path.join(DATA_DIR, 'tg-links.json'); @@ -101,6 +102,14 @@ async function notifyEvent(evt) { if (!chat) return; const kind = evt.kind === 'upline' ? `${evt.gen ? `Gen ${evt.gen} ` : ''}upgrade pass-up from #${evt.fromId}` : `referral reward from #${evt.fromId}'s entry`; await dm(chat, `šŸ’° You just caught ${evt.pol.toFixed(2)} POL — ${kind}.\nVerify: https://polygonscan.com/tx/${evt.tx}\n\nYour pipeline: https://rmcircle.team/my/${evt.toId}`); + } else if (evt.type === 'upgraded') { + // Congratulate the member who upgraded, and tell them what it opened up. + const chat = memberChat(evt.id); + if (!chat) return; + const tools = suiteTools.unlockText(evt.level, evt.levelName, getConfig()); + await dm(chat, `⚔ You're now ${evt.levelName}!\n\nYour reach just extended one generation deeper — payments from that layer can now land on you.` + + (tools ? `\n\n🧰 This level also unlocked ${tools}.\nOpen it: https://rmcircle.team/suite` : '') + + `\n\nNow teach your two to climb: https://rmcircle.team/training#lesson-8`); } else if (evt.type === 'registered' && evt.referrerId) { const chat = memberChat(evt.referrerId); if (!chat) return; diff --git a/tweet.js b/tweet.js index 1ad2144..955d29f 100644 --- a/tweet.js +++ b/tweet.js @@ -9,6 +9,7 @@ // - Never logs the API key. const fs = require('fs'); const path = require('path'); +const suiteTools = require('./suite-tools'); const BLOTATO_URL = 'https://backend.blotato.com/v2/posts'; const MIN_GAP_MS = 2600; // ~23/min ceiling, under Blotato's 30/min @@ -55,6 +56,21 @@ function render(evt, cfg) { const u = evt.upgrade; const lvl = (u && (u.levelName)) || evt.levelName; const up = (u && u.id && lvl) ? `Member #${u.id} upgraded to ${lvl} and ` : ''; + // What that upgrade unlocked in the Circle Suite — only when the tools are + // live AND config.suiteToolsInAlerts is on (see suite-tools.js). + const tool = suiteTools.unlockTerse((u && u.level), lvl, cfg); + if (tool) { + body = pick([ + `šŸš€ Level up! ${up}${pol} POL landed on Member #${evt.toId}'s position — and that upgrade unlocked ${tool}. šŸ’Ž`, + `⚔ ${up}Member #${evt.toId} received ${pol} POL straight from the contract — the upgrade also opened up ${tool}. šŸ”—`, + `šŸ”„ ${up}${pol} POL paid up to Member #${evt.toId} on Polygon — plus ${tool} unlocked with the level. šŸš€`, + ]); + const headT = `${body}\n\nSee how it works šŸ‘‰ ${cta}`; + let outT = `${headT}\n\n${tags}`; + if (outT.length > 280) outT = headT; + if (outT.length > 280) outT = headT.slice(0, 279) + '…'; + return outT; + } body = pick([ `šŸš€ Level up! ${up}${pol} POL just landed on Member #${evt.toId}'s position — instant, on-chain, unstoppable. šŸ’Ž`, `⚔ Another upgrade paid! ${up}Member #${evt.toId} received ${pol} POL straight from the smart contract — no middleman. šŸ”—`,