const TelegramBot = require('node-telegram-bot-api'); const fs = require('fs'); const path = require('path'); require('dotenv').config({ path: path.join(__dirname, '..', '.env') }); // 1. Load Settings const SETTINGS_FILE = path.join(__dirname, '..', 'server-settings.json'); const DB_FILE = path.join(__dirname, '..', 'data', 'db.json'); console.log(`📂 DB Path resolved to: ${DB_FILE}`); // --- Database Helpers --- function getDatabase() { if (!fs.existsSync(DB_FILE)) return null; try { return JSON.parse(fs.readFileSync(DB_FILE, 'utf8')); } catch (err) { console.error("❌ Error reading DB:", err); return null; } } function saveDatabase(data) { try { fs.writeFileSync(DB_FILE, JSON.stringify(data, null, 2)); console.log("💾 Database updated successfully."); return true; } catch (err) { console.error("❌ Error saving DB:", err); return false; } } // --- Action Handlers --- function handleAddIncome(amount, description, category = 'other') { const db = getDatabase(); if (!db) return "Error: Database not found."; // Ensure incomes array exists if (!db.incomes) db.incomes = []; const newIncome = { id: Date.now().toString(), // Simple ID amount: parseFloat(amount), description: description, category: category, date: new Date().toISOString() }; db.incomes.push(newIncome); if (saveDatabase(db)) { return `✅ Ingreso agregado: $${newIncome.amount.toLocaleString()} (${newIncome.description})`; } else { return "❌ Error interno guardando el ingreso."; } } function getSettings() { let settings = {}; // Try to load from file if (fs.existsSync(SETTINGS_FILE)) { try { settings = JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8')); } catch (err) { console.error("❌ Error reading settings:", err); } } // Override with Env Vars if (!settings.telegram) settings.telegram = {}; if (process.env.TELEGRAM_BOT_TOKEN) settings.telegram.botToken = process.env.TELEGRAM_BOT_TOKEN; if (process.env.TELEGRAM_CHAT_ID) settings.telegram.chatId = process.env.TELEGRAM_CHAT_ID; return settings; } async function startBot() { console.log("🚀 Starting Finance Bot..."); const settings = getSettings(); if (!settings || !settings.telegram?.botToken) { console.error("❌ config or Bot Token missing."); return; } const { botToken, chatId } = settings.telegram; const bot = new TelegramBot(botToken, { polling: true }); console.log(`✅ Bot started! Waiting for messages...`); bot.on('message', async (msg) => { const incomingChatId = msg.chat.id.toString(); const text = msg.text; // Security check: only reply to the configured owner/group if (chatId && incomingChatId !== chatId) { console.warn(`⚠️ Ignoring message from unauthorized chat: ${incomingChatId}`); return; } if (!text) return; console.log(`📩 Received: "${text}" from ${msg.from.first_name}`); // Send typing action bot.sendChatAction(incomingChatId, 'typing'); // Refresh settings to get latest AI config const currentSettings = getSettings(); const aiProvider = currentSettings?.aiProviders?.[0]; if (!aiProvider || !aiProvider.endpoint || !aiProvider.token) { bot.sendMessage(incomingChatId, "⚠️ No AI provider configured. Please set one up in the app."); return; } try { // Prepare AI Request let targetUrl = aiProvider.endpoint; if (!targetUrl.endsWith('/messages') && !targetUrl.endsWith('/chat/completions')) { targetUrl = targetUrl.endsWith('/') ? `${targetUrl}v1/messages` : `${targetUrl}/v1/messages`; } const model = aiProvider.model || "gpt-3.5-turbo"; console.log(`🤖 Asking ${aiProvider.name} (Model: ${model})...`); const dbData = getDatabase(); const dbString = dbData ? JSON.stringify(dbData) : "No data available."; const systemPrompt = `You are a helpful financial assistant called "Finanzas Bot". You have access to the user's financial data (JSON). Current Date: ${new Date().toISOString()} CAPABILITIES: 1. ANSWER questions about the data. 2. ADD INCOMES if the user asks. *** IMPORTANT: TO ADD AN INCOME *** If the user wants to add an income, DO NOT reply with text. Instead, reply with a JSON OBJECT strictly in this format: { "action": "add_income", "amount": 150000, "description": "Freelance payment", "category": "freelance" } (Categories: salary, freelance, business, gift, other) DATA: ${dbString} `; const response = await fetch(targetUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${aiProvider.token}`, 'x-api-key': aiProvider.token, 'anthropic-version': '2023-06-01' }, body: JSON.stringify({ model: model, messages: [ { role: "system", content: systemPrompt }, { role: "user", content: text } ], max_tokens: 1000 }) }); if (!response.ok) { const errText = await response.text(); throw new Error(`API Error ${response.status}: ${errText}`); } const data = await response.json(); // Handle different response formats (Anthropic vs OpenAI-compatible) let reply = "🤖 No response content."; if (data.content && Array.isArray(data.content)) { reply = data.content.find(c => c.type === 'text')?.text || reply; } else if (data.choices && Array.isArray(data.choices)) { reply = data.choices[0]?.message?.content || reply; } // Check for JSON Action try { // Try to find JSON block if mixed with text const jsonMatch = reply.match(/\{[\s\S]*\}/); const potentialJson = jsonMatch ? jsonMatch[0] : reply; const actionData = JSON.parse(potentialJson); if (actionData && actionData.action === 'add_income') { console.log("⚡ Executing Action: add_income"); const resultMsg = handleAddIncome(actionData.amount, actionData.description, actionData.category); bot.sendMessage(incomingChatId, resultMsg); return; // Stop here, don't send the raw JSON reply } } catch (e) { // Not a JSON action, standard text reply } bot.sendMessage(incomingChatId, reply, { parse_mode: 'Markdown' }); } catch (error) { console.error("❌ AI Error:", error.message); bot.sendMessage(incomingChatId, `❌ Error calling AI: ${error.message}`); } }); bot.on('polling_error', (error) => { console.error("⚠️ Polling Error:", error.code); // E.g., EFATAL if token is wrong }); } startBot();