/** * GoldRate Engine — Telegram Bot for API Key Management * * Private admin bot to create, list, revoke, and check API keys. * Only authorized admin Telegram IDs can use this bot. * * Commands: * /start — Welcome message * /create_sln — Create SLN (Chennai) API key * /create_ibja — Create IBJA (India) API key * /list — List all active keys * /revoke — Revoke a key * /check — Check key status * /help — Show all commands * * Setup: * 1. Create a bot via @BotFather on Telegram * 2. Add TELEGRAM_BOT_TOKEN to .env * 3. Add your Telegram user ID to TELEGRAM_ADMIN_IDS in .env * 4. Start: node src/telegramBot.js (or auto-starts with server) */ require('dotenv').config(); const TelegramBotModule = require('node-telegram-bot-api'); const TelegramBot = TelegramBotModule.default || TelegramBotModule; const crypto = require('crypto'); const { createClient } = require('@supabase/supabase-js'); // ── Config ─────────────────────────────────────────── const BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN; const ADMIN_IDS = (process.env.TELEGRAM_ADMIN_IDS || '') .split(',') .map(id => id.trim()) .filter(Boolean); let supabase = null; let bot = null; // ── Initialize ─────────────────────────────────────── function initTelegramBot() { if (!BOT_TOKEN) { console.log('ℹ️ TELEGRAM_BOT_TOKEN not set — Telegram bot disabled'); return null; } const supabaseUrl = process.env.SUPABASE_URL; const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY; if (!supabaseUrl || !supabaseKey) { console.log('⚠️ Supabase not configured — Telegram bot disabled'); return null; } supabase = createClient(supabaseUrl, supabaseKey); bot = new TelegramBot(BOT_TOKEN, { polling: true }); // Global error handler — prevent crashes bot.on('polling_error', (err) => { console.warn('⚠️ Telegram polling error:', err.message); }); bot.on('error', (err) => { console.warn('⚠️ Telegram error:', err.message); }); // Register handlers bot.onText(/\/start/, safeHandler(handleStart)); bot.onText(/\/help/, safeHandler(handleHelp)); bot.onText(/\/create_sln(.*)/, safeHandler(handleCreateSLN)); bot.onText(/\/create_ibja(.*)/, safeHandler(handleCreateIBJA)); bot.onText(/\/list/, safeHandler(handleList)); bot.onText(/\/revoke(.*)/, safeHandler(handleRevoke)); bot.onText(/\/check(.*)/, safeHandler(handleCheck)); console.log('🤖 Telegram Bot started! Waiting for commands...'); if (ADMIN_IDS.length > 0) { console.log(`🔒 Authorized admins: ${ADMIN_IDS.join(', ')}`); } else { console.log('⚠️ No TELEGRAM_ADMIN_IDS set — bot will tell you your ID on /start'); } return bot; } // ── Safe handler wrapper (prevents crashes) ────────── function safeHandler(fn) { return async (msg, match) => { try { await fn(msg, match); } catch (err) { console.error('⚠️ Telegram handler error:', err.message); try { await bot.sendMessage(msg.chat.id, `❌ Error: ${err.message}`); } catch (e) { console.error('⚠️ Could not send error message:', e.message); } } }; } // ── Auth Check ─────────────────────────────────────── function isAdmin(chatId) { if (ADMIN_IDS.length === 0) return true; return ADMIN_IDS.includes(chatId.toString()); } function generateApiKey(source) { const prefix = source.toLowerCase(); const random = crypto.randomBytes(16).toString('hex'); return `${prefix}_${random}`; } // ── HTML helper ────────────────────────────────────── function esc(text) { return String(text).replace(/&/g, '&').replace(//g, '>'); } // ── /start ─────────────────────────────────────────── async function handleStart(msg) { const chatId = msg.chat.id; const name = esc(msg.from.first_name || 'Admin'); const adminStatus = isAdmin(chatId) ? '✅ You are authorized as admin' : '❌ Not authorized. Add your ID to TELEGRAM_ADMIN_IDS in .env'; const text = `🪙 GoldRate Engine — API Key Bot Welcome ${name}! 👋 Your Telegram ID: ${chatId} ${adminStatus} Available Commands: /create_sln Owner Name — Chennai (SLN) key /create_ibja Owner Name — India (IBJA) key /list — List all API keys /check api_key — Check key details /revoke api_key — Deactivate a key /help — Show this message`; await bot.sendMessage(chatId, text, { parse_mode: 'HTML' }); } // ── /help ──────────────────────────────────────────── async function handleHelp(msg) { return handleStart(msg); } // ── /create_sln ────────────────────────────────────── async function handleCreateSLN(msg, match) { const chatId = msg.chat.id; if (!isAdmin(chatId)) { return bot.sendMessage(chatId, '❌ Unauthorized. Contact admin.'); } const args = (match[1] || '').trim(); if (!args) { return bot.sendMessage(chatId, '⚠️ Usage: /create_sln Owner Name [Days]\n\nExample: /create_sln Gold Shop ABC 90', { parse_mode: 'HTML' }); } // Parse name and optional days (e.g. "Shop Name 90" -> "Shop Name" and 90 days) const parts = args.split(/\s+/); let days = 30; let ownerName = args; if (parts.length > 1) { const lastPart = parts[parts.length - 1]; if (/^\d+$/.test(lastPart)) { days = parseInt(lastPart); ownerName = parts.slice(0, -1).join(' '); } } await createKey(chatId, ownerName, 'SLN', days); } // ── /create_ibja ───────────────────────────────────── async function handleCreateIBJA(msg, match) { const chatId = msg.chat.id; if (!isAdmin(chatId)) { return bot.sendMessage(chatId, '❌ Unauthorized. Contact admin.'); } const args = (match[1] || '').trim(); if (!args) { return bot.sendMessage(chatId, '⚠️ Usage: /create_ibja Owner Name [Days]\n\nExample: /create_ibja Jeweller XYZ 120', { parse_mode: 'HTML' }); } // Parse name and optional days const parts = args.split(/\s+/); let days = 30; let ownerName = args; if (parts.length > 1) { const lastPart = parts[parts.length - 1]; if (/^\d+$/.test(lastPart)) { days = parseInt(lastPart); ownerName = parts.slice(0, -1).join(' '); } } await createKey(chatId, ownerName, 'IBJA', days); } // ── Create Key (shared) ───────────────────────────── async function createKey(chatId, ownerName, source, days = 30) { const apiKey = generateApiKey(source); const expiresAt = new Date(); expiresAt.setDate(expiresAt.getDate() + days); const { data, error } = await supabase .from('api_keys') .insert({ api_key: apiKey, owner_name: ownerName, source: source, expires_at: expiresAt.toISOString(), is_active: true, notes: `Created via Telegram by ${chatId}` }) .select() .single(); if (error) { return bot.sendMessage(chatId, `❌ Failed to create key: ${esc(error.message)}\n\n💡 Did you create the api_keys table in Supabase?`, { parse_mode: 'HTML' }); } const expiryStr = expiresAt.toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' }); const sourceName = source === 'SLN' ? 'SLN Bullion Chennai' : 'IBJA India'; const text = `✅ API Key Created! 🔑 Key: ${apiKey} 👤 Owner: ${esc(ownerName)} 📍 Source: ${sourceName} 📅 Valid Until: ${expiryStr} ⏱ Duration: ${days} days Copy the key above and share with the user.`; await bot.sendMessage(chatId, text, { parse_mode: 'HTML' }); } // ── /list ──────────────────────────────────────────── async function handleList(msg) { const chatId = msg.chat.id; if (!isAdmin(chatId)) { return bot.sendMessage(chatId, '❌ Unauthorized.'); } const { data, error } = await supabase .from('api_keys') .select('*') .order('created_at', { ascending: false }) .limit(20); if (error) { return bot.sendMessage(chatId, `❌ Error: ${esc(error.message)}`); } if (!data || data.length === 0) { return bot.sendMessage(chatId, '📋 No API keys found.\n\nCreate one with /create_sln or /create_ibja'); } const now = new Date(); let text = `📋 API Keys (${data.length})\n\n`; for (const key of data) { const expired = new Date(key.expires_at) < now; const status = !key.is_active ? '❌ Revoked' : expired ? '⏰ Expired' : '✅ Active'; const daysLeft = Math.max(0, Math.ceil((new Date(key.expires_at) - now) / (1000 * 60 * 60 * 24))); const expiryStr = new Date(key.expires_at).toLocaleDateString('en-IN', { day: '2-digit', month: 'short' }); text += `${status} ${esc(key.owner_name)} (${key.source})\n`; text += `${key.api_key.substring(0, 20)}...\n`; text += `Expires: ${expiryStr}${!expired && key.is_active ? ` (${daysLeft}d left)` : ''}\n\n`; } text += 'Use /check KEY to see full details'; await bot.sendMessage(chatId, text, { parse_mode: 'HTML' }); } // ── /revoke ────────────────────────────────────────── async function handleRevoke(msg, match) { const chatId = msg.chat.id; if (!isAdmin(chatId)) { return bot.sendMessage(chatId, '❌ Unauthorized.'); } const apiKey = (match[1] || '').trim(); if (!apiKey) { return bot.sendMessage(chatId, '⚠️ Usage: /revoke api_key_here', { parse_mode: 'HTML' }); } const { data, error } = await supabase .from('api_keys') .update({ is_active: false }) .eq('api_key', apiKey) .select() .single(); if (error || !data) { return bot.sendMessage(chatId, `❌ Key not found: ${esc(apiKey)}`, { parse_mode: 'HTML' }); } await bot.sendMessage(chatId, `✅ Key Revoked\n\n👤 Owner: ${esc(data.owner_name)}\n📍 Source: ${data.source}\n🔑 Key: ${esc(apiKey)}`, { parse_mode: 'HTML' }); } // ── /check ─────────────────────────────────────────── async function handleCheck(msg, match) { const chatId = msg.chat.id; if (!isAdmin(chatId)) { return bot.sendMessage(chatId, '❌ Unauthorized.'); } const apiKey = (match[1] || '').trim(); if (!apiKey) { return bot.sendMessage(chatId, '⚠️ Usage: /check api_key_here', { parse_mode: 'HTML' }); } const { data, error } = await supabase .from('api_keys') .select('*') .eq('api_key', apiKey) .single(); if (error || !data) { return bot.sendMessage(chatId, `❌ Key not found: ${esc(apiKey)}`, { parse_mode: 'HTML' }); } const now = new Date(); const expired = new Date(data.expires_at) < now; const daysLeft = Math.max(0, Math.ceil((new Date(data.expires_at) - now) / (1000 * 60 * 60 * 24))); const status = !data.is_active ? '❌ Revoked' : expired ? '⏰ Expired' : '✅ Active'; const createdStr = new Date(data.created_at).toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' }); const expiryStr = new Date(data.expires_at).toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' }); const sourceName = data.source === 'SLN' ? '📍 SLN Bullion Chennai' : '🇮🇳 IBJA India'; const daysInfo = (!expired && data.is_active) ? `\nDays Left: ${daysLeft}` : ''; const text = `🔑 API Key Details Key: ${data.api_key} Owner: ${esc(data.owner_name)} Phone: ${data.owner_phone || 'N/A'} Email: ${data.owner_email || 'N/A'} Source: ${sourceName} Status: ${status} Created: ${createdStr} Expires: ${expiryStr}${daysInfo}`; await bot.sendMessage(chatId, text, { parse_mode: 'HTML' }); } // ── Export ──────────────────────────────────────────── module.exports = { initTelegramBot }; // ── Run standalone ─────────────────────────────────── if (require.main === module) { console.log('🤖 Starting Telegram Bot standalone...'); const botInstance = initTelegramBot(); if (!botInstance) { console.error('❌ Bot could not start. Check TELEGRAM_BOT_TOKEN in .env'); process.exit(1); } }