6ac1bcab55
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.
166 lines
8.6 KiB
JavaScript
166 lines
8.6 KiB
JavaScript
// 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 };
|