const TelegramBot = require('node-telegram-bot-api'); const fs = require('fs'); const path = require('path'); const BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN; let bot = null; let app = null; let pollingRetryCount = 0; const MAX_POLLING_RETRIES = 10; const POLLING_BASE_DELAY_MS = 5000; // Registry per i comandi, callback e query inline in formato { pattern: Regex, execute: Function } let commandsRegistry = []; let callbackHandlers = []; let inlineQueriesRegistry = []; let isMessageListenerRegistered = false; // Inizializzazione del bot. function initBot() { if (!BOT_TOKEN) { console.warn("[Telegram] BOT_TOKEN not set: bot disabled"); return null; } if (global.__meb_telegram_bot) { bot = global.__meb_telegram_bot; console.log("[Telegram] Già avviato. Riavvio del bot."); } else { bot = new TelegramBot(BOT_TOKEN, { polling: true }); // Gestione errori di polling: intercetta EFATAL (DNS/Rete) e riavvia con backoff esponenziale bot.on('polling_error', (error) => { const isNetworkError = error.code === 'EFATAL' || (error.message && (error.message.includes('EAI_AGAIN') || error.message.includes('ECONNREFUSED') || error.message.includes('ETIMEDOUT'))); if (isNetworkError) { if (pollingRetryCount >= MAX_POLLING_RETRIES) { console.error(`[Telegram] Polling fallito dopo ${MAX_POLLING_RETRIES} tentativi. Bot disattivato. Riavviare il plugin per riprovare.`); return; } pollingRetryCount++; const delay = Math.min(POLLING_BASE_DELAY_MS * Math.pow(2, pollingRetryCount - 1), 300000); // max 5 min console.warn(`[Telegram] Errore Polling Critico (${error.code}), tentativo ${pollingRetryCount}/${MAX_POLLING_RETRIES}. Riavvio tra ${delay / 1000}s...`); setTimeout(() => { bot.startPolling({ restart: true }) .then(() => { pollingRetryCount = 0; }) .catch(err => console.error("[Telegram] Errore riavvio polling:", err.message)); }, delay); } else { console.error(`[Telegram] Polling error: ${error.message}`); } }); global.__meb_telegram_bot = bot; console.log("[Telegram] Avvio del bot."); } // Caricamento dei comandi e dei callback. if (!global.__meb_telegram_handlers) { global.__meb_telegram_handlers = true; loadCommands(); loadCallbacks(); loadInlineQueries(); setupMessageListener(); // Registra il listener generale dei messaggi } return bot; } /** * Registra il listener centrale per tutti i messaggi. */ function setupMessageListener() { if (!bot || isMessageListenerRegistered) return; bot.on('message', async (msg) => { if (!msg.text) return; // Cicla i comandi registrati e vedi se il testo corrisponde a un pattern for (const cmd of commandsRegistry) { if (cmd.pattern && cmd.pattern.test(msg.text)) { try { await cmd.execute(bot, msg, { app, getSK }); } catch (error) { console.error(`[Telegram] Error executing command ${msg.text}:`, error); bot.sendMessage(msg.chat.id, "⚠️ Errore interno durante l'esecuzione del comando."); } return; // Trovato ed eseguito } } }); bot.on('callback_query', async (query) => { const chatId = query.message.chat.id; const data = query.data; await bot.answerCallbackQuery(query.id); const context = { bot, app, getSK, chatId, data, msg: query.message }; // Find matching handler const handler = callbackHandlers.find(h => { if (h.id) return h.id === data; if (h.match) return h.match(data); return false; }); if (handler) { try { await handler.execute(context); } catch (err) { const msgErr = err.message || (err.response && err.response.body && err.response.body.description) || String(err); if (msgErr.includes("message is not modified") || msgErr.includes("message to edit not found")) { // Silently ignore unmodified edit or deleted message } else { console.error(`[Telegram] Error executing callback ${data}:`, err); await bot.sendMessage(chatId, `Errore nella chimata dell'api, ${msgErr}.`); } } } else { console.warn(`[Telegram] Unknown callback action: ${data}`); await bot.sendMessage(chatId, `Azione sconosciuta: ${data}`); } }); bot.on('inline_query', async (query) => { const text = query.query; // Cerca una query inline corrispondente for (const handler of inlineQueriesRegistry) { if (handler.pattern && handler.pattern.test(text)) { try { await handler.execute(bot, query, { app, getSK }); } catch (err) { console.error(`[Telegram] Error executing inline query ${text}:`, err); } return; } } }); isMessageListenerRegistered = true; } /** * Ottiene il valore di una chiave dal DataBrowser di SignalK. * @param {*} skPath Nome della chiave (path completo, come ad esempio "navigation.position.latitude"). * @returns Valore della chiave. */ function getSK(skPath) { if (!app) return null; const v = app.getSelfPath(skPath); return v && v.value !== undefined && v.value !== null ? v.value : null; } /** * Carica o ricarica i comandi del bot. Pulisce la cache di module_require per implementare l'hot reload. * @returns {void} */ function loadCommands() { if (!bot) return; const commandsDir = path.join(__dirname, 'commands'); if (fs.existsSync(commandsDir)) { commandsRegistry = []; // Svuota i vecchi comandi const menuCommands = []; // Per il menu di Telegram // Legge solo i file .js dalla cartella /commands. const commandFiles = fs.readdirSync(commandsDir).filter(file => file.endsWith('.js')); // Per ogni file, importa il comando for (const file of commandFiles) { const fullPath = path.resolve(commandsDir, file); //Importa i comandi da module.exports all'interno del file const command = require(fullPath); //Registra il comando nel registry interno. if (command.pattern && command.execute) { commandsRegistry.push(command); // Se ha una descrizione e un nome comando, lo aggiungiamo al menu if (command.command && command.description) { menuCommands.push({ command: command.command.toLowerCase(), description: command.description }); } } } // Invia la lista dei comandi a Telegram per il menu a sinistra if (menuCommands.length > 0) { bot.setMyCommands(menuCommands).catch(err => { console.error("[Telegram] Errore nel setMyCommands:", err); }); } } } /** * Carica o ricarica i callback del bot. * @returns {void} */ function loadCallbacks() { if (!bot) return; const callbacksDir = path.join(__dirname, 'callbacks'); callbackHandlers = []; if (fs.existsSync(callbacksDir)) { // Legge solo i file .js dalla cartella /callbacks. const callbackFiles = fs.readdirSync(callbacksDir).filter(file => file.endsWith('.js')); // Per ogni file, importa i callback e li aggiunge all'array callbackHandlers. for (const file of callbackFiles) { const fullPath = path.resolve(callbacksDir, file); //Importa i callback da module.exports all'interno del file const handlers = require(fullPath); if (Array.isArray(handlers)) { callbackHandlers.push(...handlers); } } } } /** * Carica o ricarica le query inline del bot. * @returns {void} */ function loadInlineQueries() { if (!bot) return; const inlineDir = path.join(__dirname, 'inline'); inlineQueriesRegistry = []; if (fs.existsSync(inlineDir)) { const inlineFiles = fs.readdirSync(inlineDir).filter(file => file.endsWith('.js')); for (const file of inlineFiles) { const fullPath = path.resolve(inlineDir, file); const handler = require(fullPath); if (handler.pattern && handler.execute) { inlineQueriesRegistry.push(handler); } } } } /** * Collega il bot all'app. * @param {*} mebApp L'app di SignalK. * @returns {TelegramBot} Il bot. */ function linkBotToApp(mebApp) { app = mebApp; bot = initBot(); return bot; } /** * Invia un messaggio ad un utente tramite il bot. * @param {*} chatId L'ID della chat. * @param {*} text Il testo del messaggio. * @param {*} options Le opzioni del messaggio. * @returns {Promise} Il bot. */ function send(chatId, text, options = {}) { if (!bot) return Promise.reject("Bot not initialized"); return bot.sendMessage(chatId, text, options); } module.exports = { linkBotToApp, send };