Files
signalk-plugin/plugin/telegram/telegram.core.js
2026-03-11 15:25:03 +01:00

270 lines
9.4 KiB
JavaScript

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<TelegramBot>} 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
};