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