diff --git a/.env.example b/.env.example index b7245e1..6267a6a 100644 --- a/.env.example +++ b/.env.example @@ -17,3 +17,20 @@ TOPSAPP_GUARAI_PASS=sua_senha_guarai # Configurações TopSapp - Instância Pedro Afonso TOPSAPP_PEDRO_AFONSO_USER=seu_usuario_pedro_afonso TOPSAPP_PEDRO_AFONSO_PASS=sua_senha_pedro_afonso + +# ========================================================== +# Configurações GenieACS (TR-069 ACS) +# Porta padrão NBI: 7557 +# ========================================================== + +# Instância Guarai - Clicknet +GENIEACS_GUARAI_URL=http://g.clicknetto.com.br +GENIEACS_GUARAI_NBI_PORT=7557 +GENIEACS_GUARAI_USER=api +GENIEACS_GUARAI_PASS=sua_senha_genieacs_guarai + +# Exemplo de nova instância futura: +# GENIEACS_PEDRO_AFONSO_URL=http://genieacs.exemplo.com.br +# GENIEACS_PEDRO_AFONSO_NBI_PORT=7557 +# GENIEACS_PEDRO_AFONSO_USER=api +# GENIEACS_PEDRO_AFONSO_PASS=sua_senha diff --git a/.env.production b/.env.production index 4b6a8fa..e750d18 100644 --- a/.env.production +++ b/.env.production @@ -19,6 +19,14 @@ NODE_ENV=production # Para descobrir seu ID, envie /start para @userinfobot ALLOWED_USERS=593272271 +# ========================================================== +# Configurações GenieACS (TR-069 ACS) +# ========================================================== +GENIEACS_GUARAI_URL=http://g.clicknetto.com.br +GENIEACS_GUARAI_NBI_PORT=7557 +GENIEACS_GUARAI_USER=api +GENIEACS_GUARAI_PASS=34641747 + # ================================= # INSTRUÇÕES DE USO: # ================================= diff --git a/src/bot/handler.js b/src/bot/handler.js index 85a881d..76ce900 100644 --- a/src/bot/handler.js +++ b/src/bot/handler.js @@ -3,6 +3,7 @@ const SSHManager = require('../ssh/manager'); const { TopSappManager } = require('../topsapp'); const FiberhomeManager = require('../fiberhome/manager'); const FiberhomeTL1Manager = require('../fiberhome/manager_tl1'); +const { GenieACSManager } = require('../genieacs'); const config = require('../config/config'); const logger = require('../utils/logger'); const DateProcessor = require('../utils/dateProcessor'); @@ -15,6 +16,7 @@ class BotHandler { this.topsapp = new TopSappManager(); this.fiberhome = new FiberhomeManager(); this.fiberhomeTL1 = new FiberhomeTL1Manager(); + this.genieacs = new GenieACSManager(); this.dateProcessor = new DateProcessor(); this.userSessions = new Map(); // Armazena sessões de usuário this.suporteSessions = new Map(); // Armazena sessões de suporte em andamento @@ -252,6 +254,11 @@ class BotHandler { await this.handleFiberhomeStatus(chatId); break; + case 'ver_roteador': + console.log(`📡 Executando handleVerRoteador com target: ${target}`); + await this.handleVerRoteador(chatId, target); + break; + case 'abrir_suporte': case 'adicionar_suporte': case 'criar_suporte': @@ -6890,6 +6897,73 @@ Digite /help para ver todos os comandos disponíveis! sessaoAtiva.etapa = 'selecionar_categoria'; await this.processarEtapaSelecionarCategoria(sessaoAtiva.id); } + + /** + * Exibe informações do roteador/CPE via GenieACS. + * Uso: "ver roteador " ou "ver roteador " + */ + async handleVerRoteador(chatId, termo) { + if (!termo) { + await this.bot.sendMessage(chatId, + '📡 *Ver Roteador - GenieACS*\n\n' + + '❓ Informe o serial ou a TAG (login TopSapp) do dispositivo.\n' + + 'Exemplos:\n' + + '• `ver roteador ABCD12345678`\n' + + '• `ver roteador joao.silva`', + { parse_mode: 'Markdown' } + ); + return; + } + + await this.bot.sendMessage(chatId, `🔍 Consultando dispositivo \`${termo}\` no GenieACS...`, { parse_mode: 'Markdown' }); + + try { + const resultado = await this.genieacs.buscarRoteador(termo); + + if (!resultado.success) { + await this.bot.sendMessage(chatId, + `❌ *Dispositivo não encontrado*\n\n` + + `🔍 Busca: \`${termo}\`\n` + + `⚠️ ${resultado.error}`, + { parse_mode: 'Markdown' } + ); + return; + } + + const d = resultado.device; + const na = (v) => (v !== null && v !== undefined && v !== '') ? String(v) : 'N/A'; + + const tagLabel = d.tags && d.tags.length > 0 ? d.tags.join(', ') : 'N/A'; + const buscadoPor = resultado.searchedBy === 'tag' ? 'TAG' : 'Serial'; + + const msg = + `📡 *Informações do Roteador*\n` + + `📌 Instância: ${resultado.instance}\n` + + `🔎 Encontrado por: ${buscadoPor}\n\n` + + `🖥️ *Dispositivo*\n` + + `├ Nome: ${na(d.productClass)}\n` + + `├ Fabricante: ${na(d.manufacturer)}\n` + + `├ OUI: ${na(d.oui)}\n` + + `├ Serial: \`${na(d.serial)}\`\n` + + `├ TAG (TopSapp): \`${tagLabel}\`\n` + + `├ HW: ${na(d.hardwareVersion)}\n` + + `└ SW: ${na(d.softwareVersion)}\n\n` + + `📶 *Wi-Fi 2.4 GHz*\n` + + `├ SSID: ${na(d.ssid24)}\n` + + `├ Canal: ${na(d.channel24)}\n` + + `└ Auto-Canal: ${d.autoChannel24}\n\n` + + `📶 *Wi-Fi 5 GHz*\n` + + `├ SSID: ${na(d.ssid5)}\n` + + `├ Canal: ${na(d.channel5)}\n` + + `└ Auto-Canal: ${d.autoChannel5}`; + + await this.bot.sendMessage(chatId, msg, { parse_mode: 'Markdown' }); + + } catch (error) { + logger.error('Erro em handleVerRoteador:', error); + await this.bot.sendMessage(chatId, `❌ Erro ao consultar o GenieACS: ${error.message}`); + } + } } module.exports = BotHandler; diff --git a/src/genieacs/api.js b/src/genieacs/api.js new file mode 100644 index 0000000..f517ccc --- /dev/null +++ b/src/genieacs/api.js @@ -0,0 +1,165 @@ +const axios = require('axios'); +const GenieACSConfig = require('./config'); +const logger = require('../utils/logger'); + +/** + * Parâmetros TR-069 que serão buscados para o comando "ver roteador". + * A projeção é enviada como query string para a NBI do GenieACS. + */ +const DEVICE_PROJECTION = [ + '_tags', + 'DeviceID.SerialNumber', + 'DeviceID.ProductClass', + 'DeviceID.Manufacturer', + 'DeviceID.OUI', + 'InternetGatewayDevice.DeviceInfo.SoftwareVersion', + 'InternetGatewayDevice.DeviceInfo.HardwareVersion', + 'InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.SSID', + 'InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.AutoChannelEnable', + 'InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.Channel', + 'InternetGatewayDevice.LANDevice.1.WLANConfiguration.2.SSID', + 'InternetGatewayDevice.LANDevice.1.WLANConfiguration.2.AutoChannelEnable', + 'InternetGatewayDevice.LANDevice.1.WLANConfiguration.2.Channel' +].join(','); + +class GenieACSApi { + constructor() { + this.config = new GenieACSConfig(); + } + + /** + * Monta um cliente axios com Basic Auth para uma instância. + * @param {Object} instance - objeto de instância do GenieACSConfig + */ + _buildClient(instance) { + return axios.create({ + baseURL: this.config.getNBIUrl(instance), + timeout: 15000, + auth: { + username: instance.user, + password: instance.password + } + }); + } + + /** + * Navega por um objeto aninhado usando um caminho separado por ponto + * e retorna o campo _value do nó folha, conforme o formato de resposta + * da NBI do GenieACS. + * + * Exemplo: getNestedValue(device, 'DeviceID.SerialNumber') retorna o valor + * de device.DeviceID.SerialNumber._value + * + * @param {Object} obj - Objeto de dados do dispositivo + * @param {string} path - Caminho com pontos (ex.: 'DeviceID.SerialNumber') + * @returns {*} - O valor encontrado ou null + */ + _getNestedValue(obj, path) { + if (!obj) return null; + const parts = path.split('.'); + let current = obj; + for (const part of parts) { + if (current === null || current === undefined) return null; + current = current[part]; + } + // O GenieACS armazena o valor em _value no nó folha + if (current && typeof current === 'object' && '_value' in current) { + return current._value; + } + return current ?? null; + } + + /** + * Executa a busca HTTP na NBI com um query object MongoDB-like. + * Método interno reutilizável. + * + * @param {Object} queryObj - Objeto de filtro no formato GenieACS + * @param {Object} instance - Instância de GenieACSConfig + * @returns {Promise} - { success, device, rawDevice, searchedBy, error } + */ + async _buscar(queryObj, instance, searchedBy) { + const client = this._buildClient(instance); + + try { + const response = await client.get('/devices', { + params: { + query: JSON.stringify(queryObj), + projection: DEVICE_PROJECTION + } + }); + + const devices = response.data; + if (!Array.isArray(devices) || devices.length === 0) { + return { success: false }; + } + + const raw = devices[0]; + const device = this._parseDevice(raw); + return { success: true, device, rawDevice: raw, searchedBy }; + + } catch (error) { + const status = error.response?.status; + const msg = status + ? `Erro HTTP ${status} ao consultar GenieACS (${instance.name})` + : `Falha de conexão com GenieACS (${instance.name}): ${error.message}`; + + logger.error(`[GenieACS] ${msg}`); + return { success: false, error: msg }; + } + } + + /** + * Busca um dispositivo por serial number. + * @param {string} serial - Serial number + * @param {Object} instance - Instância de GenieACSConfig + */ + async buscarDispositivo(serial, instance) { + logger.info(`[GenieACS] Buscando serial "${serial}" em ${instance.name}`); + return this._buscar({ 'DeviceID.SerialNumber': serial }, instance, 'serial'); + } + + /** + * Busca um dispositivo pela TAG (nome de usuário TopSapp). + * No GenieACS, as tags são armazenadas no campo _tags (array de strings). + * @param {string} tag - Valor da tag (ex.: login PPPoE do cliente) + * @param {Object} instance - Instância de GenieACSConfig + */ + async buscarPorTag(tag, instance) { + logger.info(`[GenieACS] Buscando por TAG "${tag}" em ${instance.name}`); + return this._buscar({ '_tags': tag }, instance, 'tag'); + } + + /** + * Extrai os campos relevantes de um dispositivo bruto retornado pela NBI. + * @param {Object} raw - Objeto bruto do GenieACS + * @returns {Object} - Objeto formatado com os campos de interesse + */ + _parseDevice(raw) { + const get = (path) => this._getNestedValue(raw, path); + + const autoChannel24 = get('InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.AutoChannelEnable'); + const autoChannel5 = get('InternetGatewayDevice.LANDevice.1.WLANConfiguration.2.AutoChannelEnable'); + + // _tags é um array direto na raiz do objeto (não segue o padrão _value) + const tags = Array.isArray(raw._tags) ? raw._tags : []; + + return { + deviceId : raw._id || null, + tags : tags, + serial : get('DeviceID.SerialNumber'), + productClass : get('DeviceID.ProductClass'), + manufacturer : get('DeviceID.Manufacturer'), + oui : get('DeviceID.OUI'), + softwareVersion : get('InternetGatewayDevice.DeviceInfo.SoftwareVersion'), + hardwareVersion : get('InternetGatewayDevice.DeviceInfo.HardwareVersion'), + ssid24 : get('InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.SSID'), + autoChannel24 : autoChannel24 === true || autoChannel24 === 'true' || autoChannel24 === '1' ? 'Sim' : 'Não', + channel24 : get('InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.Channel'), + ssid5 : get('InternetGatewayDevice.LANDevice.1.WLANConfiguration.2.SSID'), + autoChannel5 : autoChannel5 === true || autoChannel5 === 'true' || autoChannel5 === '1' ? 'Sim' : 'Não', + channel5 : get('InternetGatewayDevice.LANDevice.1.WLANConfiguration.2.Channel') + }; + } +} + +module.exports = GenieACSApi; diff --git a/src/genieacs/config.js b/src/genieacs/config.js new file mode 100644 index 0000000..2b0f573 --- /dev/null +++ b/src/genieacs/config.js @@ -0,0 +1,78 @@ +/** + * Configurações das instâncias GenieACS + * Suporta múltiplas instâncias para expansão futura. + * + * Variáveis de ambiente esperadas (prefix GENIEACS__*): + * GENIEACS_GUARAI_URL, GENIEACS_GUARAI_NBI_PORT, + * GENIEACS_GUARAI_USER, GENIEACS_GUARAI_PASS + */ +class GenieACSConfig { + constructor() { + // Instâncias conhecidas – novas podem ser adicionadas aqui + // ou via variáveis de ambiente com o mesmo padrão de chave. + this.instances = { + guarai: { + name: 'Guarai - Clicknet', + baseURL: process.env.GENIEACS_GUARAI_URL || 'http://g.clicknetto.com.br', + nbiPort: parseInt(process.env.GENIEACS_GUARAI_NBI_PORT, 10) || 7557, + user: process.env.GENIEACS_GUARAI_USER || '', + password: process.env.GENIEACS_GUARAI_PASS || '' + } + /* + * Exemplo de nova instância futura: + * pedro_afonso: { + * name: 'Pedro Afonso - Clicknet', + * baseURL: process.env.GENIEACS_PEDRO_AFONSO_URL || '', + * nbiPort: parseInt(process.env.GENIEACS_PEDRO_AFONSO_NBI_PORT, 10) || 7557, + * user: process.env.GENIEACS_PEDRO_AFONSO_USER || '', + * password: process.env.GENIEACS_PEDRO_AFONSO_PASS || '' + * } + */ + }; + } + + /** + * Retorna a instância padrão (guarai). + */ + getDefaultInstance() { + return this.instances.guarai; + } + + /** + * Retorna uma instância pelo nome da chave. + * Se a chave não existir, retorna a instância padrão. + */ + getInstanceByKey(key) { + return this.instances[key] || this.getDefaultInstance(); + } + + /** + * Seleciona a instância correta com base em um texto de localização livre. + * Útil quando o bot suportar múltiplas instâncias. + */ + getInstanceByLocation(location = '') { + const loc = location.toLowerCase(); + if (loc.includes('pedro') || loc.includes('afonso')) { + return this.instances.pedro_afonso || this.getDefaultInstance(); + } + return this.getDefaultInstance(); + } + + /** + * Verifica se ao menos uma instância está configurada (user + password). + */ + isReady() { + return Object.values(this.instances).some( + (inst) => inst.user && inst.password + ); + } + + /** + * Retorna a URL NBI completa de uma instância. + */ + getNBIUrl(instance) { + return `${instance.baseURL}:${instance.nbiPort}`; + } +} + +module.exports = GenieACSConfig; diff --git a/src/genieacs/index.js b/src/genieacs/index.js new file mode 100644 index 0000000..5b253ec --- /dev/null +++ b/src/genieacs/index.js @@ -0,0 +1,5 @@ +const GenieACSApi = require('./api'); +const GenieACSConfig = require('./config'); +const GenieACSManager = require('./manager'); + +module.exports = { GenieACSApi, GenieACSConfig, GenieACSManager }; diff --git a/src/genieacs/manager.js b/src/genieacs/manager.js new file mode 100644 index 0000000..0a55be5 --- /dev/null +++ b/src/genieacs/manager.js @@ -0,0 +1,62 @@ +const GenieACSApi = require('./api'); +const GenieACSConfig = require('./config'); +const logger = require('../utils/logger'); + +class GenieACSManager { + constructor() { + this.api = new GenieACSApi(); + this.config = new GenieACSConfig(); + logger.info('GenieACSManager inicializado'); + console.log('📡 GenieACSManager inicializado'); + } + + /** + * Verifica se ao menos uma instância está configurada. + */ + isReady() { + return this.config.isReady(); + } + + /** + * Busca informações de um roteador/CPE pelo serial number ou pela TAG (username TopSapp). + * Tenta por serial primeiro; se não encontrar, tenta por TAG. + * + * @param {string} termo - Serial ou TAG do dispositivo + * @param {string} location - Texto de localização (opcional) + * @returns {Promise} - { success, device, instance, searchedBy, error } + */ + async buscarRoteador(termo, location = '') { + if (!this.isReady()) { + return { + success: false, + error: 'GenieACS não está configurado. Verifique as variáveis GENIEACS_* no arquivo .env' + }; + } + + const instance = this.config.getInstanceByLocation(location); + const termoLimpo = termo.trim(); + + // 1ª tentativa: busca por serial + logger.info(`[GenieACS] Tentando como serial: "${termoLimpo}"`); + const porSerial = await this.api.buscarDispositivo(termoLimpo, instance); + if (porSerial.success) { + return { ...porSerial, instance: instance.name }; + } + + // 2ª tentativa: busca por TAG (usuario/login TopSapp) + logger.info(`[GenieACS] Não encontrado como serial. Tentando como TAG: "${termoLimpo}"`); + const porTag = await this.api.buscarPorTag(termoLimpo, instance); + if (porTag.success) { + return { ...porTag, instance: instance.name }; + } + + // Nenhum resultado + const errConexao = porSerial.error || porTag.error; + return { + success: false, + error: errConexao || `Nenhum dispositivo encontrado com serial ou TAG "${termoLimpo}"` + }; + } +} + +module.exports = GenieACSManager; diff --git a/src/nlp/processor.js b/src/nlp/processor.js index 205d1b8..b37dd85 100644 --- a/src/nlp/processor.js +++ b/src/nlp/processor.js @@ -33,11 +33,15 @@ class NLPProcessor { 'listar_servicos': ['listar serviços', 'servicos cliente', 'planos cliente', 'contratos cliente', 'ver servicos', 'plano cliente', 'assinaturas', 'planos', 'serviços ativos', 'ver plano'], 'abrir_suporte': ['abrir suporte', 'criar suporte', 'adicionar suporte', 'novo suporte', 'suporte', 'chamado', 'ticket', 'atendimento', 'suporte tecnico', 'atendimento tecnico', 'abrir chamado', 'registrar chamado', 'criar chamado', 'visita tecnica', 'agendar visita', 'reclamação', 'reclamacao'], 'consultar_usuario': ['consultar usuario', 'consultar user', 'ver usuario', 'status usuario', 'status user', 'info usuario', 'info user', 'checar usuario', 'verificar usuario', 'pesquisar usuario', 'localizar usuario', 'buscar usuario', 'usuario conectado', 'user conectado', 'usuario online', 'user online', 'usuario ativo', 'user ativo'], - 'diagnosticar': ['diagnosticar', 'diagnostico', 'diagnose', 'diagnosticar cliente', 'diagnostico cliente', 'checar cliente completo', 'verificar cliente completo', 'relatorio cliente', 'relatorio completo', 'status completo cliente', 'analisar cliente', 'inspecionar cliente', 'situacao do cliente', 'situação do cliente'] + 'diagnosticar': ['diagnosticar', 'diagnostico', 'diagnose', 'diagnosticar cliente', 'diagnostico cliente', 'checar cliente completo', 'verificar cliente completo', 'relatorio cliente', 'relatorio completo', 'status completo cliente', 'analisar cliente', 'inspecionar cliente', 'situacao do cliente', 'situação do cliente'], + 'ver_roteador': ['ver roteador', 'info roteador', 'status roteador', 'roteador', 'cpe', 'ver cpe', 'info cpe', 'dados roteador', 'informações roteador', 'informacoes roteador', 'genieacs', 'ver device', 'info device', 'dispositivo cpe'] }; // Padrões de comandos this.commandPatterns = [ + // Padrões específicos para GenieACS – ver roteador + /^(ver|info|status|dados|informações|informacoes)\s+(roteador|cpe|device)\s+([A-Z0-9_-]+)$/i, + /^(roteador|cpe)\s+([A-Z0-9_-]+)$/i, // Padrões específicos para TopSapp /^(buscar|procurar|encontrar)\s+(cliente|user)\s+(.+)$/i, /^(ver|mostrar)\s+(faturas?|boletos?|contas?)\s+(.+)$/i, @@ -161,6 +165,18 @@ class NLPProcessor { const fullMatch = match[0]; const action = match[1]; + // Detectar comando de ver roteador / CPE via GenieACS + if (/roteador|cpe|device/i.test(fullMatch) && !/sinal|signal|onu|ont/i.test(fullMatch)) { + // Padrão "ver roteador SERIAL" ou "roteador SERIAL" + const serial = match[match.length - 1] || match[2]; + return { + action: 'ver_roteador', + target: serial ? serial.trim().toUpperCase() : null, + confidence: 0.95, + type: 'genieacs_pattern' + }; + } + // 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 @@ -736,6 +752,19 @@ class NLPProcessor { } } + // Detectar "ver roteador SERIAL" / "roteador SERIAL" via GenieACS + if (/roteador|cpe/i.test(text) && !/onu|ont|sinal|signal/i.test(text)) { + const rrMatch = text.match(/(?:ver|info|status|dados)?\s*(?:roteador|cpe)\s+([A-Z0-9_-]+)/i); + if (rrMatch && rrMatch[1]) { + return { + action: 'ver_roteador', + target: rrMatch[1].trim().toUpperCase(), + confidence: 0.9, + type: 'keyword_roteador' + }; + } + } + // Procurar por ações conhecidas for (const [action, synonyms] of Object.entries(this.actionSynonyms)) { for (const synonym of synonyms) {