e5eefdfc61
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
270 lines
23 KiB
JavaScript
270 lines
23 KiB
JavaScript
const http = require('http');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const crypto = require('crypto');
|
|
const { URL } = require('url');
|
|
|
|
const PORT = Number(process.env.PORT || 3000);
|
|
const ROOT = __dirname;
|
|
const PUBLIC_DIR = path.join(ROOT, 'public');
|
|
const DATA_DIR = process.env.DATA_DIR || path.join(ROOT, 'data');
|
|
const SEED_DIR = path.join(ROOT, 'seed');
|
|
const SPONSORS_FILE = path.join(DATA_DIR, 'sponsors.json');
|
|
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';
|
|
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';
|
|
const OPENROUTER_KEY_FILE = path.join(DATA_DIR, 'openrouter.key');
|
|
function getOpenRouterKey() {
|
|
if (process.env.OPENROUTER_API_KEY) return process.env.OPENROUTER_API_KEY;
|
|
try { return fs.readFileSync(OPENROUTER_KEY_FILE, 'utf8').trim(); } catch (e) { return ''; }
|
|
}
|
|
const chatHits = new Map();
|
|
function chatRateLimited(ip) {
|
|
const now = Date.now(), rec = chatHits.get(ip);
|
|
if (!rec || now > rec.reset) { chatHits.set(ip, { count: 1, reset: now + 60000 }); return false; }
|
|
rec.count++; return rec.count > 10;
|
|
}
|
|
function chatSystemPrompt() {
|
|
const c = getConfig(), sponsors = getSponsors(), a = activeSponsor(sponsors);
|
|
const waiting = sponsors.filter(s=>s.status==='waiting').length;
|
|
return `You are "Team Help", the assistant on ${c.siteName || 'Crypto Team Build'} (https://rmcircle.saasy.top), the team site for the RM Circle Premium team build — a project of the Crypto Team Build Network.
|
|
|
|
FACTS:
|
|
- Strategy: enter RM Circle dApp at Premium tier (${c.premiumEntryPol || 362} POL on Polygon Mainnet, chain ID 137, POL is the gas token). Each member gets EXACTLY 2 directs, then the position is "qualified" and its referral link is retired. The team then helps those 2 get their 2 (moving-link strategy). Build depth, not width. Never add extra directs to a qualified link.
|
|
- First team goal: 30 properly placed positions (2+4+8+16), then 32, 64, 128 and beyond.
|
|
- 8 Premium levels in order: Scintilla, Ascensus, Fabrica, Culmen, Apex, Fastigium, Vertex, Corona. Everyone starts at Scintilla. Upgrade as quickly as practical, ideally with earned POL; the first two payments at each level help fund the next upgrade. Stay aware of your active downline's levels so you don't fall behind and miss payments.
|
|
- 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.saasy.top/start right before joining.
|
|
- Site pages: https://rmcircle.saasy.top/ (strategy overview + roadmap), https://rmcircle.saasy.top/start (current sponsor + join steps), https://rmcircle.saasy.top/training (4 videos: 1. How the team build works, 2. Create your MetaMask wallet, 3. Funding your wallet, 4. Buying the Premium position).
|
|
- Telegram group for live team help: ${c.telegramUrl || 'https://t.me/cryptoteambuild'}
|
|
|
|
RULES:
|
|
- Keep answers short: 1-4 sentences, plain text, no markdown formatting. Include full URLs when pointing to a page.
|
|
- NEVER promise, estimate, or imply earnings or income. If asked about returns/profit, say results depend on team effort, duplication, upgrades, smart-contract rules and POL's market value, that no income is guaranteed, and to only use funds they can afford to lose.
|
|
- NEVER ask for or discuss handling anyone's Secret Recovery Phrase or private keys except to warn they must never share them with anyone.
|
|
- Only answer questions about this project, the site, wallets/POL as they relate to joining, and the team process. For anything else, or anything you are not sure about, say you're not sure and point them to the Telegram group: ${c.telegramUrl || 'https://t.me/cryptoteambuild'}
|
|
- Never give financial, legal, or tax advice.`;
|
|
}
|
|
const SUBMISSIONS_FILE = path.join(DATA_DIR, 'submissions.json');
|
|
if (!fs.existsSync(SUBMISSIONS_FILE)) fs.writeFileSync(SUBMISSIONS_FILE, '[]');
|
|
const submitHits = new Map();
|
|
function submitRateLimited(ip) {
|
|
const now = Date.now(), rec = submitHits.get(ip);
|
|
if (!rec || now > rec.reset) { submitHits.set(ip, { count: 1, reset: now + 600000 }); return false; }
|
|
rec.count++; return rec.count > 5;
|
|
}
|
|
function sendTelegram(text) {
|
|
const c = getConfig();
|
|
if (!c.telegramBotToken || !c.telegramChatId) return;
|
|
const payload = { chat_id: c.telegramChatId, text };
|
|
if (c.telegramTopicId && /^[0-9]+$/.test(String(c.telegramTopicId))) payload.message_thread_id = Number(c.telegramTopicId);
|
|
fetch(`https://api.telegram.org/bot${c.telegramBotToken}/sendMessage`, {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
}).then(async r=>{ if(!r.ok) console.error('telegram sendMessage status', r.status, (await r.text().catch(()=>'')).slice(0,200)); })
|
|
.catch(e=>console.error('telegram error', e.message));
|
|
}
|
|
function firePostback(clickid, txid, source) {
|
|
const pb = getConfig().bemobPostbackUrl;
|
|
if (!clickid || !pb || !/^https:\/\/[a-z0-9.-]+\/postback/i.test(pb)) return;
|
|
fetch(`${pb}${pb.includes('?')?'&':'?'}cid=${encodeURIComponent(clickid)}&payout=0&txid=${encodeURIComponent(txid)}`)
|
|
.then(r=>{ if(r.ok) recordEvent('postback', source); else console.error('bemob postback status', r.status); })
|
|
.catch(e=>console.error('bemob postback error', e.message));
|
|
}
|
|
async function handleSubmitId(req, res) {
|
|
const ip = String(req.headers['x-forwarded-for']||req.socket.remoteAddress||'').split(',')[0].trim();
|
|
if (submitRateLimited(ip)) return json(res, 429, { error: 'Too many submissions — please wait a few minutes.' });
|
|
const b = await bodyJson(req).catch(()=>null);
|
|
if (!b) return json(res, 400, { error: 'Invalid request.' });
|
|
const newId = String(b.newId||'').trim();
|
|
if (!/^[0-9]{1,10}$/.test(newId)) return json(res, 400, { error: 'Enter your numeric RM Circle ID (numbers only).' });
|
|
const memberName = String(b.memberName||'').replace(/[ |