Aggiunto collegamento al server
This commit is contained in:
269
plugin/telegram/telegram.core.js
Normal file
269
plugin/telegram/telegram.core.js
Normal file
@@ -0,0 +1,269 @@
|
||||
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
|
||||
};
|
||||
Reference in New Issue
Block a user