Files
finanzas/scripts/bot.js

221 lines
7.3 KiB
JavaScript

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();