270 lines
9.4 KiB
JavaScript
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
|
|
};
|