'use strict'; const fetch = require('node-fetch'); const fs = require('fs'); const path = require('path'); // ── Environment ─────────────────────────────────────────────────────────────── const BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN; const GATEWAY_TOKEN = process.env.GATEWAY_TOKEN; const LLAMA_URL = 'http://127.0.0.1:8080'; const PROXY_BASE = (process.env.CLOUDFLARE_TELEGRAM_PROXY_URL || 'https://api.telegram.org').replace(/\/$/, ''); // ── Persistence ─────────────────────────────────────────────────────────────── const DATA_DIR = '/app/data'; const CONV_FILE = path.join(DATA_DIR, 'conversations.json'); const SAVE_INTERVAL = 60 * 1000; // ── Deduplication tracking ──────────────────────────────────────────────────── const processedUpdates = new Set(); // ── Token Scrubber ──────────────────────────────────────────────────────────── const TOKEN_PATTERN = /bot[0-9]+:[A-Za-z0-9_-]+/g; function scrub(str) { return String(str).replace(TOKEN_PATTERN, 'bot[REDACTED]'); } // ── Startup Validation ──────────────────────────────────────────────────────── if (!BOT_TOKEN) { console.error('❌ CRITICAL: TELEGRAM_BOT_TOKEN secret is not set!'); } if (!GATEWAY_TOKEN || GATEWAY_TOKEN === 'changeme') { console.warn('⚠️ WARNING: GATEWAY_TOKEN is not set or using default!'); } // ── In-memory conversation history ─────────────────────────────────────────── let conversations = {}; let lastActive = {}; const CONV_TTL_MS = 60 * 60 * 1000; const MAX_HIST = parseInt(process.env.MAX_HISTORY_TURNS || '8'); // ── Shared state for dashboard (consumed by health-server.js) ───────────────── // Attach to global so health-server can read without circular imports global.botState = { lastMessageAt: null, totalMessages: 0, activeChats: 0, }; // ── Boot: restore conversations from disk ──────────────────────────────────── try { if (fs.existsSync(CONV_FILE)) { const saved = JSON.parse(fs.readFileSync(CONV_FILE, 'utf8')); conversations = saved.conversations || {}; lastActive = saved.lastActive || {}; console.log(`📂 Restored ${Object.keys(conversations).length} conversations from disk`); } } catch (e) { console.warn('⚠️ Could not restore conversations from disk:', e.message); } // ── Persist conversations to disk ──────────────────────────────────────────── function saveConversations() { try { fs.mkdirSync(DATA_DIR, { recursive: true }); fs.writeFileSync(CONV_FILE, JSON.stringify({ conversations, lastActive }, null, 2)); } catch (e) { console.error('❌ Failed to save conversations:', e.message); } } setInterval(saveConversations, SAVE_INTERVAL); // ── Stale conversation cleanup ──────────────────────────────────────────────── setInterval(() => { const now = Date.now(); let cleaned = 0; for (const chatId of Object.keys(lastActive)) { if (now - lastActive[chatId] > CONV_TTL_MS) { delete conversations[chatId]; delete lastActive[chatId]; cleaned++; } } if (cleaned > 0) { console.log(`🧹 Cleaned ${cleaned} stale conversations`); saveConversations(); } global.botState.activeChats = Object.keys(conversations).length; }, 30 * 60 * 1000); // ── Core Telegram API helper ────────────────────────────────────────────────── async function telegramCall(method, body) { if (!BOT_TOKEN) throw new Error('TELEGRAM_BOT_TOKEN is not configured'); const url = `${PROXY_BASE}/bot${BOT_TOKEN}/${method}`; try { const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), timeout: 15000 }); if (!res.ok) { const errText = scrub(await res.text()); throw new Error(`Telegram ${method} failed (${res.status}): ${errText}`); } return await res.json(); } catch (e) { throw new Error(scrub(e.message)); } } async function sendTelegram(chatId, text) { try { const htmlText = markdownToTelegramHtml(text); return await telegramCall('sendMessage', { chat_id: chatId, text: htmlText, parse_mode: 'HTML' }); } catch (_) { return await telegramCall('sendMessage', { chat_id: chatId, text: text.substring(0, 4096) }); } } function markdownToTelegramHtml(text) { return text .replace(/&/g, '&') .replace(//g, '>') .replace(/```[\w]*\n?([\s\S]*?)```/g, (_, code) => `
${code.trim()}`)
.replace(/`([^`\n]+)`/g, '$1')
.replace(/\*\*(.+?)\*\*/g, '$1')
.replace(/(?$1')
.substring(0, 4096);
}
async function sendTyping(chatId) {
return telegramCall('sendChatAction', { chat_id: chatId, action: 'typing' });
}
// ── LLM Call ──────────────────────────────────────────────────────────────────
async function callLLM(messages) {
const res = await fetch(`${LLAMA_URL}/v1/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${GATEWAY_TOKEN || 'changeme'}`
},
body: JSON.stringify({
model: 'local-model',
messages,
// Qwen3 thinking models need enough budget for the