first commit
This commit is contained in:
commit
d0d99063e2
33 files changed
+9595
No files matched your search
+1940
File diff suppressed because it is too large.
Load diff
@@ -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'
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user