first commit

This commit is contained in:
rodrigo committed 2025-08-08 10:22:27 -03:00
commit d0d99063e2
33 files changed
+9595

No files matched your search

+1940
View File
File diff suppressed because it is too large. Load diff
+24
View File
@@ -0,0 +1,24 @@
module.exports = {
telegram: {
token: process.env.TELEGRAM_BOT_TOKEN || ''
},
ssh: {
defaultHost: process.env.DEFAULT_SSH_HOST || '',
defaultUser: process.env.DEFAULT_SSH_USER || '',
defaultPassword: process.env.DEFAULT_SSH_PASSWORD || '',
timeout: 30000
},
bot: {
debug: process.env.BOT_DEBUG === 'true',
allowedUsers: process.env.ALLOWED_USERS ?
process.env.ALLOWED_USERS.split(',').map(id => parseInt(id.trim())) :
[]
},
commands: {
start: '/start',
help: '/help',
ssh: '/ssh',
status: '/status',
equipamentos: '/equipamentos'
}
};
+46
View File
@@ -0,0 +1,46 @@
require('dotenv').config();
const TelegramBot = require('node-telegram-bot-api');
const BotHandler = require('./bot/handler');
const config = require('./config/config');
// Criar instância do bot
const bot = new TelegramBot(config.telegram.token, { polling: true });
// Criar handler do bot
const botHandler = new BotHandler(bot);
// Log de inicialização
console.log('🤖 Bot de Ajuda iniciado!');
console.log(`📱 Token: ${config.telegram.token ? '✅ Configurado' : '❌ Não configurado'}`);
console.log(`🔐 Debug: ${config.bot.debug ? 'Ativado' : 'Desativado'}`);
// Event listeners
bot.on('message', (msg) => {
botHandler.handleMessage(msg);
});
bot.on('callback_query', (callbackQuery) => {
botHandler.handleCallbackQuery(callbackQuery);
});
// Error handling
bot.on('error', (error) => {
console.error('❌ Erro no bot:', error);
});
bot.on('polling_error', (error) => {
console.error('❌ Erro de polling:', error);
});
// Graceful shutdown
process.on('SIGINT', () => {
console.log('\n🛑 Encerrando bot...');
bot.stopPolling();
process.exit(0);
});
process.on('SIGTERM', () => {
console.log('\n🛑 Encerrando bot...');
bot.stopPolling();
process.exit(0);
});
+476
View File
@@ -0,0 +1,476 @@
class NLPProcessor {
constructor() {
// Dicionário de sinônimos para ações
this.actionSynonyms = {
'desconectar': ['desconectar', 'desligar', 'parar', 'stop', 'disconnect', 'kick', 'expulsar'],
'desconectar_usuario': ['desconectar cliente', 'desconectar usuário', 'desconectar user', 'kick cliente', 'kick usuário', 'expulsar cliente', 'remover cliente'],
'conectar': ['conectar', 'ligar', 'iniciar', 'start', 'connect'],
'reiniciar': ['reiniciar', 'restart', 'reboot', 'reset'],
'status': ['status', 'estado', 'verificar', 'check', 'info'],
'help': ['ajuda', 'help', 'socorro', 'comandos'],
'ssh': ['ssh', 'acessar', 'entrar', 'login'],
'lista': ['lista', 'listar', 'equipamentos', 'devices', 'list'],
'listar_usuarios': ['listar usuários', 'listar clientes', 'usuarios conectados', 'clientes conectados', 'show users'],
'ver_sinal': ['ver sinal', 'verificar sinal', 'sinal', 'check signal', 'signal'],
'remover_onu': ['remover onu', 'deletar onu', 'apagar onu', 'remove onu', 'delete onu'],
'adicionar_onu': ['adicionar onu', 'add onu', 'criar onu', 'cadastrar onu', 'registrar onu'],
'ping': ['ping', 'testar', 'test', 'conectividade']
};
// Padrões de comandos
this.commandPatterns = [
// Padrões específicos para ver sinal
/^(ver|verificar|check)\s+(sinal|signal)\s+([A-Z0-9]+)$/i,
/^(sinal|signal)\s+([A-Z0-9]+)$/i,
// Padrões específicos para adicionar ONU
/^(adicionar|add|criar|cadastrar|registrar)\s+(onu|ont)\s+([A-Z0-9]+)$/i,
/^(adicionar|add|criar|cadastrar|registrar)\s+([A-Z0-9]+)$/i,
// Padrões específicos para remover ONU
/^(remover|deletar|apagar|remove|delete)\s+(onu|ont)\s+([A-Z0-9]+)$/i,
/^(remover|deletar|apagar|remove|delete)\s+([A-Z0-9]+)$/i,
// Padrões específicos para desconectar usuário (devem vir primeiro)
/^(desconectar|kick|expulsar|remover)\s+(cliente|usuário|usuario|user)\s+(.+)$/i,
/^(desconectar|kick|expulsar|remover)\s+(usuario|user)\s+(.+)$/i,
/^(desconectar|kick|expulsar|remover)\s+(.+)\s+(do|no)?\s*(concentrador|juniper)?$/i,
// Padrão genérico para desconectar (detectar se é nome de usuário)
/^(desconectar|kick|expulsar|remover)\s+([a-zA-Z0-9_.-]+)$/i,
// Padrões para listar usuários
/^(listar|mostrar)\s+(usuários|usuarios|clientes|users)(\s+conectados)?$/i,
// Padrões gerais (devem vir por último)
/^(conectar|ligar|iniciar)\s+(.+)$/i,
/^(reiniciar|restart)\s+(.+)$/i,
/^(verificar|check)\s+(status\s+)?(.+)$/i,
/^ssh\s+(.+)$/i,
/^ping\s+(.+)$/i
];
}
/**
* Processa uma mensagem de texto e extrai a intenção
* @param {string} text - Texto da mensagem
* @returns {Object} - Objeto com ação, equipamento e confiança
*/
processMessage(text) {
if (!text || typeof text !== 'string') {
return { action: null, target: null, confidence: 0 };
}
const normalizedText = text.toLowerCase().trim();
// Verificar se é um comando direto (/comando)
if (normalizedText.startsWith('/')) {
return this.processSlashCommand(normalizedText);
}
// Verificar padrões de comandos
for (const pattern of this.commandPatterns) {
const match = normalizedText.match(pattern);
if (match) {
return this.extractFromPattern(match);
}
}
// Verificar palavras-chave individuais
return this.extractFromKeywords(normalizedText);
}
/**
* Processa comandos que começam com /
*/
processSlashCommand(text) {
const parts = text.split(' ');
const command = parts[0].substring(1); // Remove o /
const target = parts.slice(1).join(' ');
return {
action: command,
target: target || null,
confidence: 1.0,
type: 'slash_command'
};
}
/**
* Extrai ação e alvo de padrões regex
*/
extractFromPattern(match) {
const fullMatch = match[0];
const action = match[1];
// Detectar comando de ver sinal
if (/ver|verificar|check|sinal|signal/i.test(action) && /sinal|signal/i.test(fullMatch)) {
let serialNumber = match[match.length - 1]; // Último grupo capturado
// Se não encontrou serial, tentar extrair de forma diferente
if (!serialNumber || serialNumber.trim() === '') {
const serialMatch = fullMatch.match(/([A-Z0-9]+)$/i);
if (serialMatch) {
serialNumber = serialMatch[1];
}
}
return {
action: 'ver_sinal',
target: serialNumber ? serialNumber.trim().toUpperCase() : null,
confidence: 0.95,
type: 'signal_check_pattern'
};
}
// Padrão específico para "sinal SERIAL"
if (/sinal|signal/i.test(action)) {
let serialNumber = match[2]; // Segundo grupo capturado
return {
action: 'ver_sinal',
target: serialNumber ? serialNumber.trim().toUpperCase() : null,
confidence: 0.9,
type: 'signal_simple_pattern'
};
}
// Padrões específicos para adicionar ONU
if (/adicionar|add|criar|cadastrar|registrar/i.test(action)) {
// Padrão 1: "adicionar onu SERIAL" ou "add ont SERIAL"
if (/onu|ont/i.test(fullMatch)) {
let serialNumber = match[3]; // Terceiro grupo capturado (serial após "onu/ont")
return {
action: 'adicionar_onu',
target: serialNumber ? serialNumber.trim().toUpperCase() : null,
confidence: 0.95,
type: 'add_onu_pattern'
};
}
// Padrão 2: "adicionar SERIAL" - assumir que é ONU se parece com serial
let potentialSerial = match[2];
if (potentialSerial && this.looksLikeOnuSerial(potentialSerial)) {
return {
action: 'adicionar_onu',
target: potentialSerial.trim().toUpperCase(),
confidence: 0.85,
type: 'add_onu_inferred'
};
}
}
// Padrões específicos para remover ONU
if (/remover|deletar|apagar|remove|delete/i.test(action)) {
// Padrão 1: "remover onu SERIAL" ou "remover ont SERIAL"
if (/onu|ont/i.test(fullMatch)) {
let serialNumber = match[3]; // Terceiro grupo capturado (serial após "onu/ont")
return {
action: 'remover_onu',
target: serialNumber ? serialNumber.trim().toUpperCase() : null,
confidence: 0.95,
type: 'remove_onu_pattern'
};
}
// Padrão 2: "remover SERIAL" - assumir que é ONU se parece com serial
let potentialSerial = match[2];
if (potentialSerial && this.looksLikeOnuSerial(potentialSerial)) {
return {
action: 'remover_onu',
target: potentialSerial.trim().toUpperCase(),
confidence: 0.85,
type: 'remove_onu_inferred'
};
}
}
// Detectar se é comando de desconectar usuário - verificar padrões específicos
if (/desconectar|kick|expulsar|remover/i.test(action)) {
// Padrão 1: "desconectar cliente/usuario/user NOME"
if (/cliente|usuário|usuario|user/i.test(fullMatch)) {
let username = match[3] || match[2]; // Pegar o grupo com o nome do usuário
// Se não encontrou o nome, tentar extrair de forma mais robusta
if (!username || username.trim() === '') {
const parts = fullMatch.toLowerCase().split(/cliente|usuário|usuario|user/);
if (parts.length > 1) {
username = parts[1].trim().split(/\s+/)[0]; // Primeira palavra após cliente/usuário
}
}
return {
action: 'desconectar_usuario',
target: username ? username.trim() : null,
confidence: 0.95,
type: 'user_disconnect_pattern'
};
}
// Padrão 2: "desconectar NOME do concentrador" ou similar
if (/concentrador|juniper/i.test(fullMatch)) {
let username = match[2]; // Nome do usuário deve estar no segundo grupo
return {
action: 'desconectar_usuario',
target: username ? username.trim() : null,
confidence: 0.9,
type: 'user_disconnect_pattern'
};
}
// Padrão 3: "desconectar NOME" - detectar se NOME parece ser um usuário
if (match[2] && this.looksLikeUsername(match[2])) {
return {
action: 'desconectar_usuario',
target: match[2].trim(),
confidence: 0.85,
type: 'user_disconnect_inferred'
};
}
}
// Detectar comando de listar usuários
if (/listar|mostrar/i.test(action) && /usuários|usuarios|clientes|users/i.test(fullMatch)) {
return {
action: 'listar_usuarios',
target: 'concentrador', // Target padrão
confidence: 0.9,
type: 'list_users_pattern'
};
}
// Padrões gerais
const normalizedAction = this.normalizeAction(action);
const target = match[match.length - 1]; // Último grupo capturado
return {
action: normalizedAction,
target: target ? target.trim() : null,
confidence: 0.9,
type: 'pattern_match'
};
}
/**
* Extrai informações baseado em palavras-chave
*/
extractFromKeywords(text) {
let bestMatch = { action: null, target: null, confidence: 0 };
// Verificar primeiro se é um comando de desconectar usuário
if (/desconectar|kick|expulsar|remover/i.test(text)) {
// Padrão explícito: "desconectar usuario/cliente NOME"
if (/usuario|user|cliente/i.test(text)) {
const match = text.match(/(?:desconectar|kick|expulsar|remover)\s+(?:usuario|user|cliente)\s+(\w+)/i);
if (match) {
return {
action: 'desconectar_usuario',
target: match[1],
confidence: 0.9,
type: 'keyword_user_disconnect'
};
}
// Tentar padrão mais simples: "desconectar NOME" onde o texto contém "usuario"
const simpleMatch = text.match(/(?:desconectar|kick|expulsar|remover)\s+(\w+)/i);
if (simpleMatch) {
return {
action: 'desconectar_usuario',
target: simpleMatch[1],
confidence: 0.8,
type: 'keyword_user_disconnect_simple'
};
}
}
// Padrão implícito: "desconectar NOME" onde NOME parece ser username
const implicitMatch = text.match(/(?:desconectar|kick|expulsar|remover)\s+([a-zA-Z0-9._-]+)/i);
if (implicitMatch && this.looksLikeUsername(implicitMatch[1])) {
return {
action: 'desconectar_usuario',
target: implicitMatch[1],
confidence: 0.75,
type: 'keyword_user_disconnect_inferred'
};
}
}
// Procurar por ações conhecidas
for (const [action, synonyms] of Object.entries(this.actionSynonyms)) {
for (const synonym of synonyms) {
if (text.includes(synonym)) {
const confidence = synonym.length / text.length;
if (confidence > bestMatch.confidence) {
bestMatch = {
action: action,
target: this.extractTarget(text, synonym),
confidence: Math.min(confidence * 2, 0.8), // Max 0.8 para keywords
type: 'keyword_match'
};
}
}
}
}
return bestMatch;
}
/**
* Extrai o alvo removendo a palavra de ação
*/
extractTarget(text, actionWord) {
const parts = text.split(actionWord);
if (parts.length > 1) {
return parts[1].trim();
}
return parts[0].trim();
}
/**
* Normaliza a ação para um formato padrão
*/
normalizeAction(action) {
const normalized = action.toLowerCase();
for (const [standardAction, synonyms] of Object.entries(this.actionSynonyms)) {
if (synonyms.includes(normalized)) {
return standardAction;
}
}
return normalized;
}
/**
* Verifica se o texto parece ser um endereço IP
*/
isIPAddress(text) {
const ipPattern = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
return ipPattern.test(text);
}
/**
* Verifica se o texto parece ser um hostname
*/
isHostname(text) {
const hostnamePattern = /^[a-zA-Z0-9.-]+$/;
return hostnamePattern.test(text) && text.includes('.');
}
/**
* Verifica se o texto parece ser um nome de usuário
*/
looksLikeUsername(text) {
if (!text || typeof text !== 'string') return false;
const trimmedText = text.trim();
// Lista de palavras que indicam equipamentos/sistemas, não usuários
const equipmentWords = [
'router', 'switch', 'juniper', 'concentrador', 'equipamento',
'servidor', 'firewall', 'modem', 'access', 'point', 'ap',
'gateway', 'bridge', 'hub', 'repeater', 'todas', 'all',
'conexões', 'conexoes', 'sistema'
];
// Se contém palavras de equipamento, provavelmente não é username
if (equipmentWords.some(word => trimmedText.toLowerCase().includes(word))) {
return false;
}
// Padrões típicos de username:
// - Apenas letras, números, pontos, hífens, underscores
// - Não muito longo (geralmente < 50 caracteres)
// - Não contém espaços (exceto se for nome completo como "ana lucia")
const usernamePattern = /^[a-zA-Z0-9._-]+$/;
const namePattern = /^[a-zA-Z][a-zA-Z0-9._\s-]*[a-zA-Z0-9]$/;
return (
trimmedText.length > 2 &&
trimmedText.length < 50 &&
(usernamePattern.test(trimmedText) || namePattern.test(trimmedText))
);
}
/**
* Verifica se o texto parece ser um serial de ONU
*/
looksLikeOnuSerial(text) {
if (!text || typeof text !== 'string') return false;
const trimmedText = text.trim().toUpperCase();
// Padrões típicos de serial de ONU:
// - Combinação de letras e números
// - Geralmente entre 8-16 caracteres
// - Pode conter caracteres alfanuméricos
// - Não contém espaços ou caracteres especiais (exceto _ ou -)
// Verificar se tem tamanho apropriado
if (trimmedText.length < 6 || trimmedText.length > 20) {
return false;
}
// Verificar se contém apenas caracteres válidos para serial
const serialPattern = /^[A-Z0-9_-]+$/;
if (!serialPattern.test(trimmedText)) {
return false;
}
// Verificar se tem pelo menos algumas letras e números (serial típico)
const hasLetters = /[A-Z]/.test(trimmedText);
const hasNumbers = /[0-9]/.test(trimmedText);
return hasLetters && hasNumbers;
}
/**
* Sugere correções para comandos não reconhecidos
*/
suggestCorrection(text) {
const suggestions = [];
const normalized = text.toLowerCase();
// Verificar similaridade com ações conhecidas
for (const [action, synonyms] of Object.entries(this.actionSynonyms)) {
for (const synonym of synonyms) {
if (this.levenshteinDistance(normalized, synonym) <= 2) {
suggestions.push(synonym);
}
}
}
return suggestions;
}
/**
* Calcula distância de Levenshtein para sugestões
*/
levenshteinDistance(str1, str2) {
const matrix = [];
for (let i = 0; i <= str2.length; i++) {
matrix[i] = [i];
}
for (let j = 0; j <= str1.length; j++) {
matrix[0][j] = j;
}
for (let i = 1; i <= str2.length; i++) {
for (let j = 1; j <= str1.length; j++) {
if (str2.charAt(i - 1) === str1.charAt(j - 1)) {
matrix[i][j] = matrix[i - 1][j - 1];
} else {
matrix[i][j] = Math.min(
matrix[i - 1][j - 1] + 1,
matrix[i][j - 1] + 1,
matrix[i - 1][j] + 1
);
}
}
}
return matrix[str2.length][str1.length];
}
}
module.exports = NLPProcessor;
+1685
View File
File diff suppressed because it is too large. Load diff