Telegram companion bot, phase 1

New tgbot.js (raw Bot API, zero deps, private-chats only so the
group feeds are untouched): wallet-verified linking via dashboard
deep-link codes, personal payout + joined-on-your-link DMs, a
message bridge that delivers site messages natively in Telegram
with reply-to-answer routing (matrix-line permissions enforced by
the existing messages.js rules), and links/msg/help commands.
Webhook self-registers; secret derived from the token. Config key
companionBotToken overrides the feed bot token when Marty creates
a dedicated bot. Dashboard Messages panel gains Connect Telegram;
chatbot + AI prompt synced.
This commit is contained in:
martbost
2026-08-22 19:39:55 -05:00
parent 89a7e60fcb
commit 6ac1bcab55
4 changed files with 202 additions and 0 deletions
+2
View File
@@ -35,6 +35,8 @@
a:()=>`Great question — and we checked it on-chain, not just in theory. The contract needs <strong>no one</strong> to keep it running: joins, upgrades, placement and every payout are fully automatic — no button anyone has to press, no expiry. If the creators walked away, lost their keys, or vanished, member payments keep flowing exactly as coded. We also verified that the founder, development and fee wallets are <strong>ordinary wallets, not programs</strong> — an ordinary wallet always accepts an incoming payment even if its key is lost forever, so a dead admin wallet can't jam a single member payment (at worst the project's <em>own</em> fee sits there uncollected). And the contract holds no stored balance — every payment is delivered in the same transaction. Full write-up in section 6 of <a href="/contract">rmcircle.team/contract</a>.`},
{k:['pyramid','ponzi','pyramid scheme','ponzi scheme','mlm','recruiting scheme','is this a scheme'],
a:()=>`A pyramid or Ponzi scheme funnels everyone's money to a central company and pays earlier joiners out of later joiners' deposits — and you can't verify any of it. This is the opposite: <strong>no company holds the money</strong>. A public smart contract on Polygon sends each payment person-to-person in the same transaction it arrives, and you can read the code and every payout yourself on-chain — nothing pooled, nothing hidden, rules that can't be changed. It <em>is</em> a team build, so it takes real effort and carries real crypto risk — not a passive investment. But you don't have to trust anyone; verify it at <a href="/contract">rmcircle.team/contract</a>. No income is guaranteed.`},
{k:['telegram bot','connect telegram','payout ping','telegram notification','message my downline','contact my downline','reach my downline'],
a:()=>`Link your position to our Telegram companion bot: open your <a href="/my">dashboard</a> → Messages → sign in with your wallet → tap "Connect Telegram". Once linked you get an instant DM whenever your position catches a payment, team messages reach you natively in Telegram (reply right there to answer), you're pinged when someone joins on your link, and "links" gives you all your invite links. Messaging still follows your matrix lines only — same rules as the site.`},
{k:['cash out','cashout','spend my crypto','withdraw','off ramp','off-ramp','gift card','giftcard','get my money out','turn into cash','convert to dollars'],
a:()=>`Three good paths, easiest first: (1) <strong>E-gift cards</strong> — send POL to CWallet (cwallet.com), swap to a US-dollar token there (their internal swaps are virtually free), and buy gift cards for brands you already use — groceries, gas, Amazon. eGifter (egifter.com) also takes crypto directly. (2) <strong>Straight cash-out</strong> via a regulated exchange in your country (Coinbase, Kraken…): send, sell, withdraw to your bank. (3) <strong>Keep it working</strong> — many members leave catches in the wallet to fund their next level. Full guide: <a href="/training#spending">Spending what you earn</a>. Honest notes: those are independent custodial services — only move what you're about to spend; taxes may apply where you live; not financial advice.`},
{k:['circle method','recruiting course','how do i recruit','recruiting training','get my two','get your two','how to invite','module 1','lessons'],
+17
View File
@@ -295,6 +295,23 @@
st.textContent='Sent ✓';setTimeout(function(){loadMsgUI(d);},700);
}catch(e){st.textContent='Send failed — try again.';sb.disabled=false;}
});
// Telegram companion link — payout pings + team messages in Telegram
try{
const foot=document.createElement('div');
foot.style.cssText='margin-top:12px;padding-top:10px;border-top:1px solid var(--line, #24425d)';
foot.innerHTML='<button id="tgLinkBtn" class="btn btn-secondary btn-sm">✈️ Connect Telegram — payout pings + team chat</button><span class="micro" id="tgLinkNote" style="margin-left:8px"></span>';
el.appendChild(foot);
const tb=document.getElementById('tgLinkBtn');
tb.addEventListener('click',async function(){
tb.disabled=true;
try{
const r=await(await fetch('/api/public/tg-link',{method:'POST'})).json();
if(r.url){window.open(r.url,'_blank','noopener');document.getElementById('tgLinkNote').textContent='Tap START in Telegram to finish linking.';}
else document.getElementById('tgLinkNote').textContent=r.error||'Try again shortly.';
}catch(e){document.getElementById('tgLinkNote').textContent='Network hiccup — try again.';}
tb.disabled=false;
});
}catch(e){}
}
async function msgAuth(d){
const err=document.getElementById('msgAuthErr');
+18
View File
@@ -17,6 +17,8 @@ const CONFIG_FILE = path.join(DATA_DIR, 'config.json');
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'changeme';
const IS_PROD = process.env.NODE_ENV === 'production';
messages.init({ dataDir: DATA_DIR, chain, isProd: IS_PROD });
const tgbot = require('./tgbot');
tgbot.init({ dataDir: DATA_DIR, chain, getConfig, messages, baseUrl: 'https://rmcircle.team' });
const SESSION_TTL = 8 * 60 * 60 * 1000;
const LEVELS = ['Scintilla','Ascensus','Fabrica','Culmen','Apex','Fastigium','Vertex','Corona'];
const OPENROUTER_MODEL = process.env.OPENROUTER_MODEL || 'deepseek/deepseek-v4-flash:nitro';
@@ -48,6 +50,7 @@ FACTS:
- Current team sponsor: ${a ? `ID ${a.id}${c.showSponsorName && a.name ? ` (${a.name})` : ''}, ${a.directs}/2 directs` : 'shown on the start page'}. ${waiting} placement(s) waiting. Placements rotate as positions qualify — always verify on https://rmcircle.team/start right before joining.
- Site pages: https://rmcircle.team/ (strategy overview + roadmap + live team stats), https://rmcircle.team/start (current sponsor + join steps), https://rmcircle.team/training (THE CIRCLE METHOD — the team's free 10-lesson course in 3 modules. M1 Get Your Two: L1 mindset, L2 warm list, L3 the conversation, L4 objections. M2 Help Your Two: L5 dashboard-as-coaching-desk, L6 first 48 hours, L7 stalled people & pass-ups, L8 timing upgrades to catches. M3 Teach the Teachers: L9 run the same play, L10 the 20-minute weekly rhythm. ROUTING RULE — answer with the lesson: how do I find people→L2 (/training#lesson-2); what do I say→L3; pyramid objection→L4; new member just joined→L6; someone stalled→L7; should I upgrade→L8; overwhelmed→L10. Deep links: /training#lesson-N — plus 7 how-to videos — team overview, wallet setup, funding, the new connect-wallet join flow on the site, the dApp backup method, how payments work, and a full 14-min Member Dashboard walkthrough — + spillover article), https://rmcircle.team/how-pay-works (the two income streams shown as a pay-flow diagram + Premium/Standard tier comparison), https://rmcircle.team/contract (plain-language security review of the verified smart contract — code can't change, no pooled funds, locked rules, honest list of operator powers), https://rmcircle.team/weekly-rhythm (printable 20-minute Weekly Rhythm routine + 4-week habit tracker from Method Lesson 10, personalized like the Fast Start sheet), https://rmcircle.team/fast-start (printable 48-Hour Fast Start checklist — personalized with the member's invite link and a scannable QR code when opened from their dashboard; prints clean black-on-white, and prints in whatever language the member selected with the 🌐 button), https://rmcircle.team/my (member dashboard — its "Your team" panel opens with an organization bar: total members in your org, generations deep, qualified count below you, POL earned below you, and its approximate USD value at an hourly-cached POL price; the matrix under it drills leg by leg), https://rmcircle.team/tools (for existing team members who want to promote — share-ready promo videos (including the “Pocket Change” curiosity hook video — 25 ways people flush pocket change weekly with nothing to show for it, then the side-hustle flip; it deliberately shows no URL so the poster's invite link in the caption/description carries the credit, and matching pocket-change post copy sits in the Social posts section), copy-paste social posts, short/long email swipes, a downloadable banner kit in every standard size, and an Official RM Circle Media library (13 vertical social videos + 15 graphics from the creators — pair them with your own invite link in the caption; each curiosity video also has a MATCHED invite link (adds ?v=<hook> to the member's /join link) that makes the landing page continue that video's hook — recommend it when members ask which link to use with a video); open it from the gold Promo Tools button on your dashboard and every post/swipe arrives pre-personalized with YOUR invite link; to write promos in their own voice, mybrandedvoice.com), https://rmcircle.team/disclaimer (affiliate/earnings/risk disclosures).
- UPGRADING FROM THE DASHBOARD: a qualified member can upgrade their level directly on their dashboard (rmcircle.team/my/<id>) — an "Upgrade" card appears with the exact next-level cost read live from the contract; they connect the wallet that OWNS the position, confirm one transaction, done. The site never touches the funds (wallet pays the contract directly). If the wallet doesn't cover the cost, the card offers the MoonPay card-buy option. On phones, open the page inside the wallet app's browser.
- TELEGRAM COMPANION BOT: members can link their position (dashboard → Messages → "Connect Telegram", wallet-verified) to get instant payout DMs, native Telegram delivery of team messages (reply in Telegram to answer — matrix-line rules still apply), joined-on-your-link pings, and their invite/angle links via the "links" command. This finally lets members reach their downline as real people instead of just IDs — while handles stay private (the bot relays).
- MESSAGES (on-site, wallet-verified): every member dashboard has a Messages panel — sign in once with the wallet that owns your position (a free signature, cannot move funds), then message your upline or anyone in your own team, or broadcast to your whole team. Spam-proof by design: messaging only works along your own matrix lines, so strangers can't message you. Unread messages show as a bell on your dashboard. Members are told the team admin can review messages for abuse. No email address needed.
- BUYING POL WITH A CARD (for people brand new to crypto): the site links to MoonPay (moonpay.com/buy/pol) on the training page, the start page, and automatically on the join page when a connected wallet's balance is short. Guidance to give: choose POL on the POLYGON network, send it to YOUR OWN wallet address, buy about entry + gas (~385 POL). When explaining gas, use the car analogy: just like a car needs fuel to get anywhere, every blockchain transaction burns a tiny bit of POL to move — keep a little in the tank beyond the entry, because a wallet with an empty tank cannot make the trip. MoonPay is an independent company (merchant of record) — it handles ID verification and charges its own card fee (~4.5%); this site never touches or holds anyone's money. First purchases can take a few minutes to arrive.
- LANGUAGE: always reply in the language the member writes in — translate program terms naturally and keep level names (Scintilla, Ascensus, ...) as-is. Site pages have a floating 🌐 Translate button (bottom-left) that machine-translates any page and remembers the choice.
@@ -616,11 +619,25 @@ async function handleApi(req,res,pathname){
if(!s)return json(res,401,{error:'Not signed in.'});
return json(res,200,messages.inbox(s));
}
if(req.method==='POST'&&pathname.startsWith('/api/tg-hook/')){
if(pathname.slice('/api/tg-hook/'.length)!==tgbot.webhookSecret())return json(res,404,{error:'Not found'});
const b=await bodyJson(req).catch(()=>null);
if(b)tgbot.handleUpdate(b);
return json(res,200,{ok:true});
}
if(req.method==='POST'&&pathname==='/api/public/tg-link'){
const s2=messages.authFromCookie(req);
if(!s2)return json(res,401,{error:'Not signed in.'});
const r=tgbot.makeLinkCode(s2.id);
if(!r.url)return json(res,200,{error:'The Telegram bot is warming up — try again in a minute.'});
return json(res,200,{url:r.url,linked:!!tgbot.memberChat(s2.id)});
}
if(req.method==='POST'&&pathname==='/api/public/msg-send'){
const s=messages.authFromCookie(req);
if(!s)return json(res,401,{error:'Not signed in.'});
const b=await bodyJson(req);
const r=messages.send(s,b||{});
if(!r.error&&b&&!b.org&&b.toId)try{tgbot.notifyMessage(s.id,Number(b.toId),String(b.body||''))}catch(e){}
return json(res,r.error?400:200,r);
}
if(req.method==='POST'&&pathname==='/api/public/msg-read'){
@@ -886,6 +903,7 @@ chain.startIndexer(evt=>{
// just drop the whole member cache so dashboards reflect joins/upgrades on
// the next fetch instead of up to 2 minutes later.
try{memberCache.clear();}catch(e){}
try{tgbot.notifyEvent(evt);}catch(e){}
try{
const c=getConfig();
// teamRootId accepts a comma list ("21,136") — alerts fire for ANY listed org
+165
View File
@@ -0,0 +1,165 @@
// Telegram companion bot: identity linking, downline message bridge, personal
// payout pings. Deliberately additive — private-chat updates only (the same
// bot keeps posting group feeds untouched), no wallet actions ever, all state
// in one JSON file on the volume. Uses the raw Bot API via fetch; token comes
// from config (companionBotToken overrides telegramBotToken when set).
'use strict';
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
let DATA_DIR = '', chain = null, getConfig = null, messages = null;
const FILE = () => path.join(DATA_DIR, 'tg-links.json');
function load() { try { return JSON.parse(fs.readFileSync(FILE(), 'utf8')); } catch (e) { return { u: '', members: {}, chats: {}, codes: {}, rmap: {} }; } }
function save(d) { try { fs.writeFileSync(FILE(), JSON.stringify(d)); } catch (e) { console.error('tgbot save', e.message); } }
function token() { const c = getConfig(); return String(c.companionBotToken || c.telegramBotToken || '').trim(); }
function webhookSecret() { const t = token(); return t ? crypto.createHash('sha1').update('tg-hook:' + t).digest('hex').slice(0, 24) : ''; }
async function api(method, payload) {
const t = token(); if (!t) return null;
try {
const r = await fetch(`https://api.telegram.org/bot${t}/${method}`, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload || {})
});
return await r.json();
} catch (e) { console.error('tgbot api', method, e.message); return null; }
}
async function dm(chatId, text, extra) {
return api('sendMessage', Object.assign({ chat_id: chatId, text, disable_web_page_preview: true }, extra || {}));
}
// --- setup: learn our username + point the webhook at ourselves -------------
async function ensureWebhook(baseUrl) {
const d = load();
const me = await api('getMe', {});
if (me && me.ok) { d.u = me.result.username; save(d); }
const secret = webhookSecret();
if (!secret) return;
const url = `${baseUrl}/api/tg-hook/${secret}`;
const info = await api('getWebhookInfo', {});
if (info && info.ok && info.result.url === url) return;
const r = await api('setWebhook', { url, allowed_updates: ['message'] });
console.log('tgbot webhook:', r && r.ok ? url : JSON.stringify(r).slice(0, 120));
}
function init(opts) {
DATA_DIR = opts.dataDir; chain = opts.chain; getConfig = opts.getConfig; messages = opts.messages;
ensureWebhook(opts.baseUrl || 'https://rmcircle.team').catch(e => console.error('tgbot init', e.message));
}
// --- linking ----------------------------------------------------------------
function makeLinkCode(memberId) {
const d = load();
// one pending code per member; 10-minute expiry
for (const [c, rec] of Object.entries(d.codes)) if (rec.id === memberId || rec.exp < Date.now()) delete d.codes[c];
const code = crypto.randomBytes(8).toString('hex');
d.codes[code] = { id: memberId, exp: Date.now() + 10 * 60 * 1000 };
save(d);
return { code, url: d.u ? `https://t.me/${d.u}?start=${code}` : null };
}
function memberChat(memberId) { const d = load(); return d.members[String(memberId)] || null; }
function chatMember(chatId) { const d = load(); return d.chats[String(chatId)] || null; }
// --- outbound notifications -------------------------------------------------
async function notifyEvent(evt) {
try {
if (evt.type === 'payout') {
const chat = memberChat(evt.toId);
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 === 'registered' && evt.referrerId) {
const chat = memberChat(evt.referrerId);
if (!chat) return;
await dm(chat, `🎉 #${evt.id} just joined on your link!\nThe first 48 hours decide everything — run the play: https://rmcircle.team/training#lesson-6\nSay hello now: reply here with:\nmsg ${evt.id} Welcome to the team!`);
}
} catch (e) { console.error('tgbot notifyEvent', e.message); }
}
async function notifyMessage(fromId, toId, body) {
try {
const chat = memberChat(toId);
if (!chat) return;
const r = await dm(chat, `📨 Message from #${fromId} (your team line):\n\n${body}\n\n↩️ Reply to THIS message to answer — it goes straight back to them.`);
if (r && r.ok) {
const d = load();
const key = String(chat);
d.rmap[key] = d.rmap[key] || {};
d.rmap[key][String(r.result.message_id)] = fromId;
const ids = Object.keys(d.rmap[key]); if (ids.length > 50) for (const old of ids.slice(0, ids.length - 50)) delete d.rmap[key][old];
save(d);
}
} catch (e) { console.error('tgbot notifyMessage', e.message); }
}
// --- member commands --------------------------------------------------------
function linksText(id) {
const b = 'https://rmcircle.team';
return `🔗 Your links, #${id} (each one credits YOU):\n\n` +
`Moving invite link (routes to whoever needs help next in your leg):\n${b}/join/${id}\n\n` +
`Angle-matched links — pick the hook that fits your person:\n` +
`💸 Pocket Change: ${b}/join/${id}?v=pocket\n` +
`📱 Your Phone: ${b}/join/${id}?v=phone\n` +
`🙋 I Don't Know Anyone: ${b}/join/${id}?v=two\n` +
`🪦 Side Hustle Graveyard: ${b}/join/${id}?v=graveyard\n\n` +
`Videos + ready-made posts: ${b}/tools?id=${id}`;
}
async function sendAsMember(memberId, toId, text, chatId) {
const r = messages.send({ id: memberId }, { toId, body: text });
if (r.error) { await dm(chatId, `⚠️ ${r.error}`); return; }
await dm(chatId, `✅ Sent to #${toId}. If they've linked Telegram they got it instantly; otherwise it's waiting on their dashboard.`);
notifyMessage(memberId, toId, text);
}
async function handleUpdate(update) {
try {
const msg = update && update.message;
if (!msg || !msg.chat || msg.chat.type !== 'private' || !msg.text) return;
const chatId = msg.chat.id;
const text = msg.text.trim();
const linked = chatMember(chatId);
if (/^\/start(\s|$)/.test(text)) {
const code = text.split(/\s+/)[1];
if (code) {
const d = load();
const rec = d.codes[code];
if (rec && rec.exp > Date.now()) {
delete d.codes[code];
d.members[String(rec.id)] = chatId;
d.chats[String(chatId)] = rec.id;
save(d);
await dm(chatId, `✅ Linked to position #${rec.id}!\n\nFrom now on:\n💰 You get a DM the moment your position catches a payment\n📨 Team messages reach you here — reply to answer\n🎉 You're pinged when someone joins on your link\n\nTry: links — or: msg <id> <your message>`);
return;
}
await dm(chatId, `That link code is expired or already used. Get a fresh one from the Messages panel on your dashboard: https://rmcircle.team/my`);
return;
}
await dm(chatId, `👋 This is the RM Circle companion bot.\n\nTo link your position: open your dashboard (https://rmcircle.team/my), sign in to Messages with your wallet, and tap "Connect Telegram". That proves the position is yours — no passwords.\n\nOnce linked you get payout pings, team messages, and your share links right here.`);
return;
}
if (!linked) { await dm(chatId, `You're not linked yet. Open https://rmcircle.team/my → Messages → "Connect Telegram".`); return; }
if (/^\/?links$/i.test(text)) { await dm(chatId, linksText(linked)); return; }
if (/^\/?help$/i.test(text)) { await dm(chatId, `Commands:\nlinks — your invite + angle links\nmsg <id> <text> — message someone on your team line\nReply to any 📨 message to answer it.\nEverything else lives on your dashboard: https://rmcircle.team/my/${linked}`); return; }
const m = text.match(/^\/?msg\s+#?(\d+)\s+([\s\S]+)/i);
if (m) { await sendAsMember(linked, Number(m[1]), m[2].trim(), chatId); return; }
// replying to a bridged message routes back to its sender
if (msg.reply_to_message) {
const d = load();
const target = (d.rmap[String(chatId)] || {})[String(msg.reply_to_message.message_id)];
if (target) { await sendAsMember(linked, target, text, chatId); return; }
}
await dm(chatId, `Not sure what you mean — try "help". (Questions about the program? The chat bubble on rmcircle.team answers in any language.)`);
} catch (e) { console.error('tgbot handleUpdate', e.message); }
}
module.exports = { init, handleUpdate, notifyEvent, notifyMessage, makeLinkCode, webhookSecret, memberChat };