// 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 `); 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 — 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 };