topsapp: mostra Circuit-ID do cliente
Signed-off-by: Rodrigo Martins <rodrigo@martinsevieira.adv.br>
This commit is contained in:
1 parent
d8badf1fde
commit
01f1315f9e
34 files changed
+70789
-70768
No files matched your search
+8130
-8115
File diff suppressed because it is too large.
Load diff
+24
-24
@@ -1,24 +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'
|
||||
}
|
||||
};
|
||||
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'
|
||||
}
|
||||
};
|
||||
+1076
-1076
File diff suppressed because it is too large.
Load diff
+501
-501
File diff suppressed because it is too large.
Load diff
+616
-616
File diff suppressed because it is too large.
Load diff
+889
-889
File diff suppressed because it is too large.
Load diff
+328
-328
@@ -1,328 +1,328 @@
|
||||
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',
|
||||
'Tags',
|
||||
'DeviceID.SerialNumber',
|
||||
'DeviceID.ProductClass',
|
||||
'DeviceID.Manufacturer',
|
||||
'DeviceID.OUI',
|
||||
'InternetGatewayDevice.DeviceInfo.SoftwareVersion',
|
||||
'InternetGatewayDevice.DeviceInfo.HardwareVersion',
|
||||
// Wi-Fi
|
||||
'InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.SSID',
|
||||
'InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.AutoChannelEnable',
|
||||
'InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.Channel',
|
||||
'InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.TotalAssociations',
|
||||
'InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.PreSharedKey.1.KeyPassphrase',
|
||||
'InternetGatewayDevice.LANDevice.1.WLANConfiguration.2.SSID',
|
||||
'InternetGatewayDevice.LANDevice.1.WLANConfiguration.2.AutoChannelEnable',
|
||||
'InternetGatewayDevice.LANDevice.1.WLANConfiguration.2.Channel',
|
||||
'InternetGatewayDevice.LANDevice.1.WLANConfiguration.2.TotalAssociations',
|
||||
'InternetGatewayDevice.LANDevice.1.WLANConfiguration.2.PreSharedKey.1.KeyPassphrase',
|
||||
// PPPoE WAN
|
||||
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.Username',
|
||||
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.ConnectionStatus',
|
||||
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.ExternalIPAddress',
|
||||
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.LastConnectionError',
|
||||
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.DNSServers',
|
||||
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.DefaultGateway',
|
||||
// Link físico e tráfego
|
||||
'InternetGatewayDevice.WANDevice.1.WANCommonInterfaceConfig.PhysicalLinkStatus',
|
||||
'VirtualParameters.pppoe_uptime',
|
||||
'VirtualParameters.trafego_recebido',
|
||||
'VirtualParameters.trafego_enviado',
|
||||
'VirtualParameters.taxa_download',
|
||||
'VirtualParameters.taxa_upload',
|
||||
// DHCP LAN
|
||||
'InternetGatewayDevice.LANDevice.1.LANHostConfigManagement.MinAddress',
|
||||
'InternetGatewayDevice.LANDevice.1.LANHostConfigManagement.MaxAddress',
|
||||
'InternetGatewayDevice.LANDevice.1.LANHostConfigManagement.DNSServers',
|
||||
// IP LAN
|
||||
'InternetGatewayDevice.LANDevice.1.LANHostConfigManagement.IPInterface.1.IPInterfaceIPAddress',
|
||||
'InternetGatewayDevice.LANDevice.1.LANHostConfigManagement.IPInterface.1.IPInterfaceSubnetMask'
|
||||
].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: 30000,
|
||||
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) {
|
||||
const v = current._value;
|
||||
// Tratar valores vazios/nulos como ausentes (retorna null para acionar fallback)
|
||||
return (v !== null && v !== undefined && v !== '') ? v : null;
|
||||
}
|
||||
// Valor direto (string, número, booleano)
|
||||
if (current !== null && current !== undefined && typeof current !== 'object') {
|
||||
return String(current).trim() !== '' ? current : null;
|
||||
}
|
||||
return 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<Object>} - { 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.
|
||||
* Tenta exato primeiro; se não encontrar, tenta uppercase (usuário pode ter digitado minúsculo).
|
||||
* @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}`);
|
||||
// Tentativa 1: exato como foi informado
|
||||
const r1 = await this._buscar({ 'DeviceID.SerialNumber': serial }, instance, 'serial');
|
||||
if (r1.success) return r1;
|
||||
// Tentativa 2: uppercase (seriais costumam ser armazenados em maiúsculas)
|
||||
const upper = serial.toUpperCase();
|
||||
if (upper !== serial) {
|
||||
logger.info(`[GenieACS] Tentando serial em uppercase: "${upper}"`);
|
||||
return this._buscar({ 'DeviceID.SerialNumber': upper }, instance, 'serial');
|
||||
}
|
||||
return r1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Busca um dispositivo pela TAG (nome de usuário TopSapp).
|
||||
* Usa regex case-insensitive para não depender de maiúsculas/minúsculas.
|
||||
* @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}`);
|
||||
// Escapa caracteres especiais de regex no valor da tag
|
||||
const tagEscaped = tag.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
|
||||
// Tentativa 1: campo _tags com regex case-insensitive (formato array)
|
||||
const r1 = await this._buscar(
|
||||
{ '_tags': { '$regex': `^${tagEscaped}$`, '$options': 'i' } },
|
||||
instance, 'tag'
|
||||
);
|
||||
if (r1.success) return r1;
|
||||
|
||||
// Tentativa 2: campo Tags.<nome> = true (formato objeto, como exibido na UI)
|
||||
logger.info(`[GenieACS] Tentando campo Tags.${tag} em ${instance.name}`);
|
||||
return this._buscar({ [`Tags.${tag.toLowerCase()}`]: true }, instance, 'tag');
|
||||
}
|
||||
|
||||
/**
|
||||
* Cria uma task setParameterValues no dispositivo via NBI.
|
||||
*
|
||||
* O parâmetro `connection_request` na URL instrui o GenieACS a enviar um
|
||||
* TR-069 Connection Request ao CPE imediatamente, triggering um inform.
|
||||
* Sem ele, o CPE só executaria a task no próximo inform agendado.
|
||||
* O `timeout` (ms) define por quanto tempo o GenieACS aguarda o CPE conectar
|
||||
* antes de retornar 202 (task enfileirada mas ainda não executada).
|
||||
*
|
||||
* @param {string} deviceId - _id do dispositivo
|
||||
* @param {Array} parameterValues - Array de [paramName, value, xsdType]
|
||||
* @param {Object} instance - Instância de GenieACSConfig
|
||||
* @returns {Promise<Object>} - { success, queued, error }
|
||||
*/
|
||||
async setParameterValues(deviceId, parameterValues, instance) {
|
||||
const client = this._buildClient(instance);
|
||||
const encodedId = encodeURIComponent(deviceId);
|
||||
|
||||
try {
|
||||
const response = await client.post(
|
||||
`/devices/${encodedId}/tasks?connection_request`,
|
||||
{ name: 'setParameterValues', parameterValues },
|
||||
{ params: { timeout: 15000 } }
|
||||
);
|
||||
const queued = response.status === 202;
|
||||
logger.info(`[GenieACS] setParameterValues em ${deviceId} (${queued ? 'enfileirado' : 'aplicado'}): ${JSON.stringify(parameterValues)}`);
|
||||
return { success: true, queued };
|
||||
} catch (error) {
|
||||
const status = error.response?.status;
|
||||
const msg = status
|
||||
? `Erro HTTP ${status} ao gravar parâmetros em ${deviceId} (${instance.name})`
|
||||
: `Falha ao gravar parâmetros: ${error.message}`;
|
||||
logger.error(`[GenieACS] ${msg}`);
|
||||
return { success: false, error: msg };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adiciona uma TAG a um dispositivo na NBI do GenieACS.
|
||||
* Endpoint: POST /devices/{id}/tags/{tag}
|
||||
*
|
||||
* @param {string} deviceId - _id do dispositivo (ex.: "00E0FC-Huawei-VMXQU22716608217")
|
||||
* @param {string} tag - Tag a adicionar (ex.: "clickcolmeia2025")
|
||||
* @param {Object} instance - Instância de GenieACSConfig
|
||||
* @returns {Promise<Object>} - { success, error }
|
||||
*/
|
||||
async adicionarTag(deviceId, tag, instance) {
|
||||
const client = this._buildClient(instance);
|
||||
const encodedId = encodeURIComponent(deviceId);
|
||||
const encodedTag = encodeURIComponent(tag);
|
||||
|
||||
try {
|
||||
await client.post(`/devices/${encodedId}/tags/${encodedTag}`);
|
||||
logger.info(`[GenieACS] TAG "${tag}" adicionada ao device ${deviceId}`);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
const status = error.response?.status;
|
||||
const msg = status
|
||||
? `Erro HTTP ${status} ao adicionar TAG "${tag}" (${instance.name})`
|
||||
: `Falha ao adicionar TAG: ${error.message}`;
|
||||
logger.error(`[GenieACS] ${msg}`);
|
||||
return { success: false, error: msg };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
// O _id do GenieACS segue o formato "OUI-ProductClass-SerialNumber".
|
||||
// Usado como fallback quando os campos DeviceID.* não chegam via projeção
|
||||
// ou estão em formato diferente do esperado.
|
||||
const idParts = typeof raw._id === 'string' ? raw._id.split('-') : [];
|
||||
const idOUI = idParts[0] || null;
|
||||
const idProductClass = idParts[1] || null;
|
||||
// Serial é tudo a partir da 3ª parte (pode conter hifens)
|
||||
const idSerial = idParts.length >= 3 ? idParts.slice(2).join('-') : null;
|
||||
|
||||
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)
|
||||
// Tags também pode vir como objeto { clickcolmeia2025: { _value: true } } (formato UI do GenieACS)
|
||||
let tags = [];
|
||||
if (Array.isArray(raw._tags)) {
|
||||
tags = raw._tags;
|
||||
} else if (raw.Tags && typeof raw.Tags === 'object') {
|
||||
// Extrai as chaves do objeto Tags onde o valor é true
|
||||
tags = Object.entries(raw.Tags)
|
||||
.filter(([k, v]) => k !== '_timestamp' && (v === true || (v && v._value === true)))
|
||||
.map(([k]) => k);
|
||||
}
|
||||
|
||||
return {
|
||||
deviceId : raw._id || null,
|
||||
tags : tags,
|
||||
serial : get('DeviceID.SerialNumber') ?? idSerial,
|
||||
productClass : get('DeviceID.ProductClass') ?? idProductClass,
|
||||
manufacturer : get('DeviceID.Manufacturer') ?? idProductClass,
|
||||
oui : get('DeviceID.OUI') ?? idOUI,
|
||||
softwareVersion : get('InternetGatewayDevice.DeviceInfo.SoftwareVersion'),
|
||||
hardwareVersion : get('InternetGatewayDevice.DeviceInfo.HardwareVersion'),
|
||||
// Wi-Fi
|
||||
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'),
|
||||
clients24 : get('InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.TotalAssociations'),
|
||||
senha24 : get('InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.PreSharedKey.1.KeyPassphrase'),
|
||||
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'),
|
||||
clients5 : get('InternetGatewayDevice.LANDevice.1.WLANConfiguration.2.TotalAssociations'),
|
||||
senha5 : get('InternetGatewayDevice.LANDevice.1.WLANConfiguration.2.PreSharedKey.1.KeyPassphrase'),
|
||||
// PPPoE WAN
|
||||
pppoeUsername : get('InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.Username'),
|
||||
pppoeStatus : get('InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.ConnectionStatus'),
|
||||
pppoeIP : get('InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.ExternalIPAddress'),
|
||||
pppoeUptime : get('VirtualParameters.pppoe_uptime'),
|
||||
pppoeLastError : get('InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.LastConnectionError'),
|
||||
dnsWAN : get('InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.DNSServers'),
|
||||
gatewayWAN : get('InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.DefaultGateway'),
|
||||
// Link físico e tráfego
|
||||
linkFisicoWAN : get('InternetGatewayDevice.WANDevice.1.WANCommonInterfaceConfig.PhysicalLinkStatus'),
|
||||
trafegoRecebido : get('VirtualParameters.trafego_recebido'),
|
||||
trafegoEnviado : get('VirtualParameters.trafego_enviado'),
|
||||
taxaDownload : get('VirtualParameters.taxa_download'),
|
||||
taxaUpload : get('VirtualParameters.taxa_upload'),
|
||||
// DHCP LAN
|
||||
dhcpInicio : get('InternetGatewayDevice.LANDevice.1.LANHostConfigManagement.MinAddress'),
|
||||
dhcpFim : get('InternetGatewayDevice.LANDevice.1.LANHostConfigManagement.MaxAddress'),
|
||||
dnsLAN : get('InternetGatewayDevice.LANDevice.1.LANHostConfigManagement.DNSServers'),
|
||||
ipLAN : get('InternetGatewayDevice.LANDevice.1.LANHostConfigManagement.IPInterface.1.IPInterfaceIPAddress'),
|
||||
mascaraLAN : get('InternetGatewayDevice.LANDevice.1.LANHostConfigManagement.IPInterface.1.IPInterfaceSubnetMask')
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GenieACSApi;
|
||||
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',
|
||||
'Tags',
|
||||
'DeviceID.SerialNumber',
|
||||
'DeviceID.ProductClass',
|
||||
'DeviceID.Manufacturer',
|
||||
'DeviceID.OUI',
|
||||
'InternetGatewayDevice.DeviceInfo.SoftwareVersion',
|
||||
'InternetGatewayDevice.DeviceInfo.HardwareVersion',
|
||||
// Wi-Fi
|
||||
'InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.SSID',
|
||||
'InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.AutoChannelEnable',
|
||||
'InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.Channel',
|
||||
'InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.TotalAssociations',
|
||||
'InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.PreSharedKey.1.KeyPassphrase',
|
||||
'InternetGatewayDevice.LANDevice.1.WLANConfiguration.2.SSID',
|
||||
'InternetGatewayDevice.LANDevice.1.WLANConfiguration.2.AutoChannelEnable',
|
||||
'InternetGatewayDevice.LANDevice.1.WLANConfiguration.2.Channel',
|
||||
'InternetGatewayDevice.LANDevice.1.WLANConfiguration.2.TotalAssociations',
|
||||
'InternetGatewayDevice.LANDevice.1.WLANConfiguration.2.PreSharedKey.1.KeyPassphrase',
|
||||
// PPPoE WAN
|
||||
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.Username',
|
||||
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.ConnectionStatus',
|
||||
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.ExternalIPAddress',
|
||||
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.LastConnectionError',
|
||||
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.DNSServers',
|
||||
'InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.DefaultGateway',
|
||||
// Link físico e tráfego
|
||||
'InternetGatewayDevice.WANDevice.1.WANCommonInterfaceConfig.PhysicalLinkStatus',
|
||||
'VirtualParameters.pppoe_uptime',
|
||||
'VirtualParameters.trafego_recebido',
|
||||
'VirtualParameters.trafego_enviado',
|
||||
'VirtualParameters.taxa_download',
|
||||
'VirtualParameters.taxa_upload',
|
||||
// DHCP LAN
|
||||
'InternetGatewayDevice.LANDevice.1.LANHostConfigManagement.MinAddress',
|
||||
'InternetGatewayDevice.LANDevice.1.LANHostConfigManagement.MaxAddress',
|
||||
'InternetGatewayDevice.LANDevice.1.LANHostConfigManagement.DNSServers',
|
||||
// IP LAN
|
||||
'InternetGatewayDevice.LANDevice.1.LANHostConfigManagement.IPInterface.1.IPInterfaceIPAddress',
|
||||
'InternetGatewayDevice.LANDevice.1.LANHostConfigManagement.IPInterface.1.IPInterfaceSubnetMask'
|
||||
].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: 30000,
|
||||
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) {
|
||||
const v = current._value;
|
||||
// Tratar valores vazios/nulos como ausentes (retorna null para acionar fallback)
|
||||
return (v !== null && v !== undefined && v !== '') ? v : null;
|
||||
}
|
||||
// Valor direto (string, número, booleano)
|
||||
if (current !== null && current !== undefined && typeof current !== 'object') {
|
||||
return String(current).trim() !== '' ? current : null;
|
||||
}
|
||||
return 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<Object>} - { 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.
|
||||
* Tenta exato primeiro; se não encontrar, tenta uppercase (usuário pode ter digitado minúsculo).
|
||||
* @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}`);
|
||||
// Tentativa 1: exato como foi informado
|
||||
const r1 = await this._buscar({ 'DeviceID.SerialNumber': serial }, instance, 'serial');
|
||||
if (r1.success) return r1;
|
||||
// Tentativa 2: uppercase (seriais costumam ser armazenados em maiúsculas)
|
||||
const upper = serial.toUpperCase();
|
||||
if (upper !== serial) {
|
||||
logger.info(`[GenieACS] Tentando serial em uppercase: "${upper}"`);
|
||||
return this._buscar({ 'DeviceID.SerialNumber': upper }, instance, 'serial');
|
||||
}
|
||||
return r1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Busca um dispositivo pela TAG (nome de usuário TopSapp).
|
||||
* Usa regex case-insensitive para não depender de maiúsculas/minúsculas.
|
||||
* @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}`);
|
||||
// Escapa caracteres especiais de regex no valor da tag
|
||||
const tagEscaped = tag.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
|
||||
// Tentativa 1: campo _tags com regex case-insensitive (formato array)
|
||||
const r1 = await this._buscar(
|
||||
{ '_tags': { '$regex': `^${tagEscaped}$`, '$options': 'i' } },
|
||||
instance, 'tag'
|
||||
);
|
||||
if (r1.success) return r1;
|
||||
|
||||
// Tentativa 2: campo Tags.<nome> = true (formato objeto, como exibido na UI)
|
||||
logger.info(`[GenieACS] Tentando campo Tags.${tag} em ${instance.name}`);
|
||||
return this._buscar({ [`Tags.${tag.toLowerCase()}`]: true }, instance, 'tag');
|
||||
}
|
||||
|
||||
/**
|
||||
* Cria uma task setParameterValues no dispositivo via NBI.
|
||||
*
|
||||
* O parâmetro `connection_request` na URL instrui o GenieACS a enviar um
|
||||
* TR-069 Connection Request ao CPE imediatamente, triggering um inform.
|
||||
* Sem ele, o CPE só executaria a task no próximo inform agendado.
|
||||
* O `timeout` (ms) define por quanto tempo o GenieACS aguarda o CPE conectar
|
||||
* antes de retornar 202 (task enfileirada mas ainda não executada).
|
||||
*
|
||||
* @param {string} deviceId - _id do dispositivo
|
||||
* @param {Array} parameterValues - Array de [paramName, value, xsdType]
|
||||
* @param {Object} instance - Instância de GenieACSConfig
|
||||
* @returns {Promise<Object>} - { success, queued, error }
|
||||
*/
|
||||
async setParameterValues(deviceId, parameterValues, instance) {
|
||||
const client = this._buildClient(instance);
|
||||
const encodedId = encodeURIComponent(deviceId);
|
||||
|
||||
try {
|
||||
const response = await client.post(
|
||||
`/devices/${encodedId}/tasks?connection_request`,
|
||||
{ name: 'setParameterValues', parameterValues },
|
||||
{ params: { timeout: 15000 } }
|
||||
);
|
||||
const queued = response.status === 202;
|
||||
logger.info(`[GenieACS] setParameterValues em ${deviceId} (${queued ? 'enfileirado' : 'aplicado'}): ${JSON.stringify(parameterValues)}`);
|
||||
return { success: true, queued };
|
||||
} catch (error) {
|
||||
const status = error.response?.status;
|
||||
const msg = status
|
||||
? `Erro HTTP ${status} ao gravar parâmetros em ${deviceId} (${instance.name})`
|
||||
: `Falha ao gravar parâmetros: ${error.message}`;
|
||||
logger.error(`[GenieACS] ${msg}`);
|
||||
return { success: false, error: msg };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adiciona uma TAG a um dispositivo na NBI do GenieACS.
|
||||
* Endpoint: POST /devices/{id}/tags/{tag}
|
||||
*
|
||||
* @param {string} deviceId - _id do dispositivo (ex.: "00E0FC-Huawei-VMXQU22716608217")
|
||||
* @param {string} tag - Tag a adicionar (ex.: "clickcolmeia2025")
|
||||
* @param {Object} instance - Instância de GenieACSConfig
|
||||
* @returns {Promise<Object>} - { success, error }
|
||||
*/
|
||||
async adicionarTag(deviceId, tag, instance) {
|
||||
const client = this._buildClient(instance);
|
||||
const encodedId = encodeURIComponent(deviceId);
|
||||
const encodedTag = encodeURIComponent(tag);
|
||||
|
||||
try {
|
||||
await client.post(`/devices/${encodedId}/tags/${encodedTag}`);
|
||||
logger.info(`[GenieACS] TAG "${tag}" adicionada ao device ${deviceId}`);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
const status = error.response?.status;
|
||||
const msg = status
|
||||
? `Erro HTTP ${status} ao adicionar TAG "${tag}" (${instance.name})`
|
||||
: `Falha ao adicionar TAG: ${error.message}`;
|
||||
logger.error(`[GenieACS] ${msg}`);
|
||||
return { success: false, error: msg };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
// O _id do GenieACS segue o formato "OUI-ProductClass-SerialNumber".
|
||||
// Usado como fallback quando os campos DeviceID.* não chegam via projeção
|
||||
// ou estão em formato diferente do esperado.
|
||||
const idParts = typeof raw._id === 'string' ? raw._id.split('-') : [];
|
||||
const idOUI = idParts[0] || null;
|
||||
const idProductClass = idParts[1] || null;
|
||||
// Serial é tudo a partir da 3ª parte (pode conter hifens)
|
||||
const idSerial = idParts.length >= 3 ? idParts.slice(2).join('-') : null;
|
||||
|
||||
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)
|
||||
// Tags também pode vir como objeto { clickcolmeia2025: { _value: true } } (formato UI do GenieACS)
|
||||
let tags = [];
|
||||
if (Array.isArray(raw._tags)) {
|
||||
tags = raw._tags;
|
||||
} else if (raw.Tags && typeof raw.Tags === 'object') {
|
||||
// Extrai as chaves do objeto Tags onde o valor é true
|
||||
tags = Object.entries(raw.Tags)
|
||||
.filter(([k, v]) => k !== '_timestamp' && (v === true || (v && v._value === true)))
|
||||
.map(([k]) => k);
|
||||
}
|
||||
|
||||
return {
|
||||
deviceId : raw._id || null,
|
||||
tags : tags,
|
||||
serial : get('DeviceID.SerialNumber') ?? idSerial,
|
||||
productClass : get('DeviceID.ProductClass') ?? idProductClass,
|
||||
manufacturer : get('DeviceID.Manufacturer') ?? idProductClass,
|
||||
oui : get('DeviceID.OUI') ?? idOUI,
|
||||
softwareVersion : get('InternetGatewayDevice.DeviceInfo.SoftwareVersion'),
|
||||
hardwareVersion : get('InternetGatewayDevice.DeviceInfo.HardwareVersion'),
|
||||
// Wi-Fi
|
||||
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'),
|
||||
clients24 : get('InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.TotalAssociations'),
|
||||
senha24 : get('InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.PreSharedKey.1.KeyPassphrase'),
|
||||
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'),
|
||||
clients5 : get('InternetGatewayDevice.LANDevice.1.WLANConfiguration.2.TotalAssociations'),
|
||||
senha5 : get('InternetGatewayDevice.LANDevice.1.WLANConfiguration.2.PreSharedKey.1.KeyPassphrase'),
|
||||
// PPPoE WAN
|
||||
pppoeUsername : get('InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.Username'),
|
||||
pppoeStatus : get('InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.ConnectionStatus'),
|
||||
pppoeIP : get('InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.ExternalIPAddress'),
|
||||
pppoeUptime : get('VirtualParameters.pppoe_uptime'),
|
||||
pppoeLastError : get('InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.LastConnectionError'),
|
||||
dnsWAN : get('InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.DNSServers'),
|
||||
gatewayWAN : get('InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.DefaultGateway'),
|
||||
// Link físico e tráfego
|
||||
linkFisicoWAN : get('InternetGatewayDevice.WANDevice.1.WANCommonInterfaceConfig.PhysicalLinkStatus'),
|
||||
trafegoRecebido : get('VirtualParameters.trafego_recebido'),
|
||||
trafegoEnviado : get('VirtualParameters.trafego_enviado'),
|
||||
taxaDownload : get('VirtualParameters.taxa_download'),
|
||||
taxaUpload : get('VirtualParameters.taxa_upload'),
|
||||
// DHCP LAN
|
||||
dhcpInicio : get('InternetGatewayDevice.LANDevice.1.LANHostConfigManagement.MinAddress'),
|
||||
dhcpFim : get('InternetGatewayDevice.LANDevice.1.LANHostConfigManagement.MaxAddress'),
|
||||
dnsLAN : get('InternetGatewayDevice.LANDevice.1.LANHostConfigManagement.DNSServers'),
|
||||
ipLAN : get('InternetGatewayDevice.LANDevice.1.LANHostConfigManagement.IPInterface.1.IPInterfaceIPAddress'),
|
||||
mascaraLAN : get('InternetGatewayDevice.LANDevice.1.LANHostConfigManagement.IPInterface.1.IPInterfaceSubnetMask')
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GenieACSApi;
|
||||
+78
-78
@@ -1,78 +1,78 @@
|
||||
/**
|
||||
* Configurações das instâncias GenieACS
|
||||
* Suporta múltiplas instâncias para expansão futura.
|
||||
*
|
||||
* Variáveis de ambiente esperadas (prefix GENIEACS_<CHAVE>_*):
|
||||
* 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;
|
||||
/**
|
||||
* Configurações das instâncias GenieACS
|
||||
* Suporta múltiplas instâncias para expansão futura.
|
||||
*
|
||||
* Variáveis de ambiente esperadas (prefix GENIEACS_<CHAVE>_*):
|
||||
* 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;
|
||||
@@ -1,5 +1,5 @@
|
||||
const GenieACSApi = require('./api');
|
||||
const GenieACSConfig = require('./config');
|
||||
const GenieACSManager = require('./manager');
|
||||
|
||||
module.exports = { GenieACSApi, GenieACSConfig, GenieACSManager };
|
||||
const GenieACSApi = require('./api');
|
||||
const GenieACSConfig = require('./config');
|
||||
const GenieACSManager = require('./manager');
|
||||
|
||||
module.exports = { GenieACSApi, GenieACSConfig, GenieACSManager };
|
||||
+152
-152
@@ -1,152 +1,152 @@
|
||||
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.
|
||||
* Se o termo contém "@", também tenta a parte antes do @ como TAG.
|
||||
*
|
||||
* @param {string} termo - Serial ou TAG do dispositivo
|
||||
* @param {string} location - Texto de localização (opcional)
|
||||
* @returns {Promise<Object>} - { 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 (exato e uppercase)
|
||||
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 (termo completo)
|
||||
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 };
|
||||
}
|
||||
|
||||
// 3ª tentativa: se o termo tem "@", tentar só a parte antes do @ como TAG
|
||||
// (ex.: "clickcolmeia2025@isp" -> tenta "clickcolmeia2025")
|
||||
if (termoLimpo.includes('@')) {
|
||||
const tagBase = termoLimpo.split('@')[0].toLowerCase();
|
||||
logger.info(`[GenieACS] Tentando TAG sem domínio: "${tagBase}"`);
|
||||
const porTagBase = await this.api.buscarPorTag(tagBase, instance);
|
||||
if (porTagBase.success) {
|
||||
return { ...porTagBase, instance: instance.name };
|
||||
}
|
||||
}
|
||||
|
||||
// Nenhum resultado
|
||||
const errConexao = porSerial.error || porTag.error;
|
||||
return {
|
||||
success: false,
|
||||
error: errConexao || `Nenhum dispositivo encontrado com serial ou TAG "${termoLimpo}"`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sincroniza o login PPPoE do TopSapp como TAG no GenieACS.
|
||||
*
|
||||
* Fluxo:
|
||||
* 1. Busca o dispositivo pelo serial (vindo do TopSapp)
|
||||
* 2. Deriva a TAG do login PPPoE (parte antes do "@", em minúsculo)
|
||||
* 3. Se a TAG ainda não existir no dispositivo, adiciona via NBI
|
||||
*
|
||||
* @param {string} serial - Serial do dispositivo (do cadastro TopSapp)
|
||||
* @param {string} pppoeLogin - Login PPPoE do cliente (ex.: "clickcolmeia2025" ou "user@isp")
|
||||
* @param {string} location - Texto de localização (opcional)
|
||||
* @returns {Promise<Object>} - { success, tag, deviceId, already, error }
|
||||
*/
|
||||
async sincronizarTagPPPoE(serial, pppoeLogin, location = '') {
|
||||
if (!this.isReady() || !serial || !pppoeLogin) {
|
||||
return { success: false, skipped: true };
|
||||
}
|
||||
|
||||
const instance = this.config.getInstanceByLocation(location);
|
||||
|
||||
// A TAG usa apenas a parte antes do "@", em minúsculo,
|
||||
// igualando ao padrão que o GenieACS já exibe na UI (ex.: "clickcolmeia2025")
|
||||
const tag = pppoeLogin.split('@')[0].toLowerCase();
|
||||
|
||||
// Localizar dispositivo pelo serial
|
||||
const result = await this.api.buscarDispositivo(serial.trim(), instance);
|
||||
if (!result.success || !result.device?.deviceId) {
|
||||
logger.warn(`[GenieACS] Sync TAG: dispositivo serial "${serial}" não encontrado`);
|
||||
return { success: false, error: 'Dispositivo não encontrado pelo serial' };
|
||||
}
|
||||
|
||||
const deviceId = result.device.deviceId;
|
||||
const tagsAtuais = result.device.tags || [];
|
||||
|
||||
// Verificar se a tag já está presente (case-insensitive)
|
||||
if (tagsAtuais.some(t => t.toLowerCase() === tag)) {
|
||||
logger.info(`[GenieACS] Sync TAG: "${tag}" já existe em ${deviceId}`);
|
||||
return { success: true, already: true, tag, deviceId };
|
||||
}
|
||||
|
||||
// Adicionar a TAG
|
||||
logger.info(`[GenieACS] Sync TAG: adicionando "${tag}" em ${deviceId}`);
|
||||
const addResult = await this.api.adicionarTag(deviceId, tag, instance);
|
||||
return { ...addResult, tag, deviceId };
|
||||
}
|
||||
/**
|
||||
* Aplica um conjunto de setParameterValues a um dispositivo já conhecido pelo deviceId.
|
||||
*
|
||||
* @param {string} deviceId - _id do dispositivo no GenieACS
|
||||
* @param {Array} paramValues - Array de [paramName, value, xsdType]
|
||||
* @param {string} location - Texto de localização (opcional)
|
||||
* @returns {Promise<Object>} - { success, error }
|
||||
*/
|
||||
async configurarRoteador(deviceId, paramValues, location = '') {
|
||||
if (!this.isReady() || !deviceId) {
|
||||
return { success: false, error: 'GenieACS não configurado ou deviceId ausente' };
|
||||
}
|
||||
const instance = this.config.getInstanceByLocation(location);
|
||||
return this.api.setParameterValues(deviceId, paramValues, instance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Relê um dispositivo pelo seu _id para obter dados atualizados.
|
||||
*
|
||||
* @param {string} deviceId - _id do dispositivo
|
||||
* @param {string} location - Texto de localização (opcional)
|
||||
* @returns {Promise<Object>} - { success, device, instance, searchedBy }
|
||||
*/
|
||||
async buscarRoteadorPorId(deviceId, location = '') {
|
||||
if (!this.isReady() || !deviceId) {
|
||||
return { success: false, error: 'GenieACS não configurado ou deviceId ausente' };
|
||||
}
|
||||
const instance = this.config.getInstanceByLocation(location);
|
||||
const result = await this.api._buscar({ '_id': deviceId }, instance, 'id');
|
||||
if (result.success) return { ...result, instance: instance.name };
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GenieACSManager;
|
||||
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.
|
||||
* Se o termo contém "@", também tenta a parte antes do @ como TAG.
|
||||
*
|
||||
* @param {string} termo - Serial ou TAG do dispositivo
|
||||
* @param {string} location - Texto de localização (opcional)
|
||||
* @returns {Promise<Object>} - { 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 (exato e uppercase)
|
||||
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 (termo completo)
|
||||
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 };
|
||||
}
|
||||
|
||||
// 3ª tentativa: se o termo tem "@", tentar só a parte antes do @ como TAG
|
||||
// (ex.: "clickcolmeia2025@isp" -> tenta "clickcolmeia2025")
|
||||
if (termoLimpo.includes('@')) {
|
||||
const tagBase = termoLimpo.split('@')[0].toLowerCase();
|
||||
logger.info(`[GenieACS] Tentando TAG sem domínio: "${tagBase}"`);
|
||||
const porTagBase = await this.api.buscarPorTag(tagBase, instance);
|
||||
if (porTagBase.success) {
|
||||
return { ...porTagBase, instance: instance.name };
|
||||
}
|
||||
}
|
||||
|
||||
// Nenhum resultado
|
||||
const errConexao = porSerial.error || porTag.error;
|
||||
return {
|
||||
success: false,
|
||||
error: errConexao || `Nenhum dispositivo encontrado com serial ou TAG "${termoLimpo}"`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sincroniza o login PPPoE do TopSapp como TAG no GenieACS.
|
||||
*
|
||||
* Fluxo:
|
||||
* 1. Busca o dispositivo pelo serial (vindo do TopSapp)
|
||||
* 2. Deriva a TAG do login PPPoE (parte antes do "@", em minúsculo)
|
||||
* 3. Se a TAG ainda não existir no dispositivo, adiciona via NBI
|
||||
*
|
||||
* @param {string} serial - Serial do dispositivo (do cadastro TopSapp)
|
||||
* @param {string} pppoeLogin - Login PPPoE do cliente (ex.: "clickcolmeia2025" ou "user@isp")
|
||||
* @param {string} location - Texto de localização (opcional)
|
||||
* @returns {Promise<Object>} - { success, tag, deviceId, already, error }
|
||||
*/
|
||||
async sincronizarTagPPPoE(serial, pppoeLogin, location = '') {
|
||||
if (!this.isReady() || !serial || !pppoeLogin) {
|
||||
return { success: false, skipped: true };
|
||||
}
|
||||
|
||||
const instance = this.config.getInstanceByLocation(location);
|
||||
|
||||
// A TAG usa apenas a parte antes do "@", em minúsculo,
|
||||
// igualando ao padrão que o GenieACS já exibe na UI (ex.: "clickcolmeia2025")
|
||||
const tag = pppoeLogin.split('@')[0].toLowerCase();
|
||||
|
||||
// Localizar dispositivo pelo serial
|
||||
const result = await this.api.buscarDispositivo(serial.trim(), instance);
|
||||
if (!result.success || !result.device?.deviceId) {
|
||||
logger.warn(`[GenieACS] Sync TAG: dispositivo serial "${serial}" não encontrado`);
|
||||
return { success: false, error: 'Dispositivo não encontrado pelo serial' };
|
||||
}
|
||||
|
||||
const deviceId = result.device.deviceId;
|
||||
const tagsAtuais = result.device.tags || [];
|
||||
|
||||
// Verificar se a tag já está presente (case-insensitive)
|
||||
if (tagsAtuais.some(t => t.toLowerCase() === tag)) {
|
||||
logger.info(`[GenieACS] Sync TAG: "${tag}" já existe em ${deviceId}`);
|
||||
return { success: true, already: true, tag, deviceId };
|
||||
}
|
||||
|
||||
// Adicionar a TAG
|
||||
logger.info(`[GenieACS] Sync TAG: adicionando "${tag}" em ${deviceId}`);
|
||||
const addResult = await this.api.adicionarTag(deviceId, tag, instance);
|
||||
return { ...addResult, tag, deviceId };
|
||||
}
|
||||
/**
|
||||
* Aplica um conjunto de setParameterValues a um dispositivo já conhecido pelo deviceId.
|
||||
*
|
||||
* @param {string} deviceId - _id do dispositivo no GenieACS
|
||||
* @param {Array} paramValues - Array de [paramName, value, xsdType]
|
||||
* @param {string} location - Texto de localização (opcional)
|
||||
* @returns {Promise<Object>} - { success, error }
|
||||
*/
|
||||
async configurarRoteador(deviceId, paramValues, location = '') {
|
||||
if (!this.isReady() || !deviceId) {
|
||||
return { success: false, error: 'GenieACS não configurado ou deviceId ausente' };
|
||||
}
|
||||
const instance = this.config.getInstanceByLocation(location);
|
||||
return this.api.setParameterValues(deviceId, paramValues, instance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Relê um dispositivo pelo seu _id para obter dados atualizados.
|
||||
*
|
||||
* @param {string} deviceId - _id do dispositivo
|
||||
* @param {string} location - Texto de localização (opcional)
|
||||
* @returns {Promise<Object>} - { success, device, instance, searchedBy }
|
||||
*/
|
||||
async buscarRoteadorPorId(deviceId, location = '') {
|
||||
if (!this.isReady() || !deviceId) {
|
||||
return { success: false, error: 'GenieACS não configurado ou deviceId ausente' };
|
||||
}
|
||||
const instance = this.config.getInstanceByLocation(location);
|
||||
const result = await this.api._buscar({ '_id': deviceId }, instance, 'id');
|
||||
if (result.success) return { ...result, instance: instance.name };
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GenieACSManager;
|
||||
+46
-46
@@ -1,46 +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);
|
||||
});
|
||||
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);
|
||||
});
|
||||
+999
-999
File diff suppressed because it is too large.
Load diff
+1837
-1837
File diff suppressed because it is too large.
Load diff
+1045
-1045
File diff suppressed because it is too large.
Load diff
+90
-90
@@ -1,90 +1,90 @@
|
||||
/**
|
||||
* Configurações do TopSapp
|
||||
*/
|
||||
class TopSappConfig {
|
||||
constructor() {
|
||||
this.credentials = {
|
||||
guarai_colmeia: {
|
||||
usuario: process.env.TOPSAPP_GUARAI_USER || '',
|
||||
senha: process.env.TOPSAPP_GUARAI_PASS || '',
|
||||
identificador: process.env.TOPSAPP_GUARAI_ID || 'BotTelegram',
|
||||
idUsuario: process.env.TOPSAPP_GUARAI_USERID || '1'
|
||||
},
|
||||
pedro_afonso: {
|
||||
usuario: process.env.TOPSAPP_PEDRO_AFONSO_USER || '',
|
||||
senha: process.env.TOPSAPP_PEDRO_AFONSO_PASS || '',
|
||||
identificador: process.env.TOPSAPP_PEDRO_AFONSO_ID || 'BotTelegram',
|
||||
idUsuario: process.env.TOPSAPP_PEDRO_AFONSO_USERID || '1'
|
||||
}
|
||||
};
|
||||
|
||||
this.isConfigured = this.validateConfiguration();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validar se as configurações estão completas
|
||||
*/
|
||||
validateConfiguration() {
|
||||
const required = ['usuario', 'senha'];
|
||||
|
||||
for (const instance in this.credentials) {
|
||||
const creds = this.credentials[instance];
|
||||
for (const field of required) {
|
||||
if (!creds[field]) {
|
||||
console.warn(`⚠️ TopSapp: Campo ${field} não configurado para ${instance}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✅ TopSapp: Configurações validadas');
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obter credenciais para uma instância
|
||||
*/
|
||||
getCredentials(instanceKey) {
|
||||
return this.credentials[instanceKey] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obter credenciais por localização
|
||||
*/
|
||||
getCredentialsByLocation(location = '') {
|
||||
const loc = location.toLowerCase();
|
||||
|
||||
if (loc.includes('pedro') || loc.includes('afonso')) {
|
||||
return this.credentials.pedro_afonso;
|
||||
}
|
||||
|
||||
return this.credentials.guarai_colmeia;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar se o TopSapp está configurado
|
||||
*/
|
||||
isReady() {
|
||||
return this.isConfigured;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obter status da configuração
|
||||
*/
|
||||
getStatus() {
|
||||
const status = {};
|
||||
|
||||
for (const instance in this.credentials) {
|
||||
const creds = this.credentials[instance];
|
||||
status[instance] = {
|
||||
configured: !!(creds.usuario && creds.senha),
|
||||
usuario: creds.usuario ? '✅' : '❌',
|
||||
senha: creds.senha ? '✅' : '❌'
|
||||
};
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TopSappConfig;
|
||||
/**
|
||||
* Configurações do TopSapp
|
||||
*/
|
||||
class TopSappConfig {
|
||||
constructor() {
|
||||
this.credentials = {
|
||||
guarai_colmeia: {
|
||||
usuario: process.env.TOPSAPP_GUARAI_USER || '',
|
||||
senha: process.env.TOPSAPP_GUARAI_PASS || '',
|
||||
identificador: process.env.TOPSAPP_GUARAI_ID || 'BotTelegram',
|
||||
idUsuario: process.env.TOPSAPP_GUARAI_USERID || '1'
|
||||
},
|
||||
pedro_afonso: {
|
||||
usuario: process.env.TOPSAPP_PEDRO_AFONSO_USER || '',
|
||||
senha: process.env.TOPSAPP_PEDRO_AFONSO_PASS || '',
|
||||
identificador: process.env.TOPSAPP_PEDRO_AFONSO_ID || 'BotTelegram',
|
||||
idUsuario: process.env.TOPSAPP_PEDRO_AFONSO_USERID || '1'
|
||||
}
|
||||
};
|
||||
|
||||
this.isConfigured = this.validateConfiguration();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validar se as configurações estão completas
|
||||
*/
|
||||
validateConfiguration() {
|
||||
const required = ['usuario', 'senha'];
|
||||
|
||||
for (const instance in this.credentials) {
|
||||
const creds = this.credentials[instance];
|
||||
for (const field of required) {
|
||||
if (!creds[field]) {
|
||||
console.warn(`⚠️ TopSapp: Campo ${field} não configurado para ${instance}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✅ TopSapp: Configurações validadas');
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obter credenciais para uma instância
|
||||
*/
|
||||
getCredentials(instanceKey) {
|
||||
return this.credentials[instanceKey] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obter credenciais por localização
|
||||
*/
|
||||
getCredentialsByLocation(location = '') {
|
||||
const loc = location.toLowerCase();
|
||||
|
||||
if (loc.includes('pedro') || loc.includes('afonso')) {
|
||||
return this.credentials.pedro_afonso;
|
||||
}
|
||||
|
||||
return this.credentials.guarai_colmeia;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verificar se o TopSapp está configurado
|
||||
*/
|
||||
isReady() {
|
||||
return this.isConfigured;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obter status da configuração
|
||||
*/
|
||||
getStatus() {
|
||||
const status = {};
|
||||
|
||||
for (const instance in this.credentials) {
|
||||
const creds = this.credentials[instance];
|
||||
status[instance] = {
|
||||
configured: !!(creds.usuario && creds.senha),
|
||||
usuario: creds.usuario ? '✅' : '❌',
|
||||
senha: creds.senha ? '✅' : '❌'
|
||||
};
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TopSappConfig;
|
||||
@@ -1,9 +1,9 @@
|
||||
const TopSappAPI = require('./api');
|
||||
const TopSappConfig = require('./config');
|
||||
const TopSappManager = require('./manager');
|
||||
|
||||
module.exports = {
|
||||
TopSappAPI,
|
||||
TopSappConfig,
|
||||
TopSappManager
|
||||
};
|
||||
const TopSappAPI = require('./api');
|
||||
const TopSappConfig = require('./config');
|
||||
const TopSappManager = require('./manager');
|
||||
|
||||
module.exports = {
|
||||
TopSappAPI,
|
||||
TopSappConfig,
|
||||
TopSappManager
|
||||
};
|
||||
+1260
-1254
File diff suppressed because it is too large.
Load diff
+433
-433
@@ -1,433 +1,433 @@
|
||||
const logger = require('./logger');
|
||||
|
||||
class DateProcessor {
|
||||
constructor() {
|
||||
// Dicionário de dias da semana
|
||||
this.diasSemana = {
|
||||
'domingo': 0, 'dom': 0,
|
||||
'segunda': 1, 'segunda-feira': 1, 'seg': 1,
|
||||
'terça': 2, 'terca': 2, 'terça-feira': 2, 'terca-feira': 2, 'ter': 2,
|
||||
'quarta': 3, 'quarta-feira': 3, 'qua': 3,
|
||||
'quinta': 4, 'quinta-feira': 4, 'qui': 4,
|
||||
'sexta': 5, 'sexta-feira': 5, 'sex': 5,
|
||||
'sábado': 6, 'sabado': 6, 'sab': 6
|
||||
};
|
||||
|
||||
// Dicionário de meses
|
||||
this.meses = {
|
||||
'janeiro': 0, 'jan': 0,
|
||||
'fevereiro': 1, 'fev': 1,
|
||||
'março': 2, 'mar': 2, 'marco': 2,
|
||||
'abril': 3, 'abr': 3,
|
||||
'maio': 4, 'mai': 4,
|
||||
'junho': 5, 'jun': 5,
|
||||
'julho': 6, 'jul': 6,
|
||||
'agosto': 7, 'ago': 7,
|
||||
'setembro': 8, 'set': 8,
|
||||
'outubro': 9, 'out': 9,
|
||||
'novembro': 10, 'nov': 10,
|
||||
'dezembro': 11, 'dez': 11
|
||||
};
|
||||
|
||||
// Palavras-chave temporais
|
||||
this.palavrasTemporais = {
|
||||
'hoje': 0,
|
||||
'amanhã': 1, 'amanha': 1,
|
||||
'depois de amanhã': 2, 'depois de amanha': 2,
|
||||
'ontem': -1,
|
||||
'anteontem': -2
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Processar texto em linguagem natural para extrair data e hora
|
||||
* @param {string} texto - Texto em linguagem natural
|
||||
* @returns {Object} - { success: boolean, data: Date, originalText: string, parsed: Object }
|
||||
*/
|
||||
processarTexto(texto) {
|
||||
if (!texto || typeof texto !== 'string') {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Texto inválido'
|
||||
};
|
||||
}
|
||||
|
||||
const textoNormalizado = texto.toLowerCase().trim();
|
||||
logger.debug('Processando texto de data/hora:', { texto: textoNormalizado });
|
||||
|
||||
try {
|
||||
// Tentar diferentes padrões de reconhecimento
|
||||
let resultado = null;
|
||||
|
||||
// 1. Padrões com dias da semana
|
||||
resultado = this.processarDiaSemana(textoNormalizado);
|
||||
if (resultado.success) return resultado;
|
||||
|
||||
// 2. Padrões com palavras temporais (hoje, amanhã, etc)
|
||||
resultado = this.processarPalavrasTemporal(textoNormalizado);
|
||||
if (resultado.success) return resultado;
|
||||
|
||||
// 3. Padrões de data específica (DD/MM, DD/MM/YYYY)
|
||||
resultado = this.processarDataEspecifica(textoNormalizado);
|
||||
if (resultado.success) return resultado;
|
||||
|
||||
// 4. Padrões relativos (em X dias, daqui a X horas)
|
||||
resultado = this.processarDataRelativa(textoNormalizado);
|
||||
if (resultado.success) return resultado;
|
||||
|
||||
// Se não conseguiu processar
|
||||
return {
|
||||
success: false,
|
||||
error: 'Não foi possível interpretar a data/hora',
|
||||
originalText: texto
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Erro ao processar data/hora:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
originalText: texto
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processar padrões com dias da semana
|
||||
*/
|
||||
processarDiaSemana(texto) {
|
||||
// Padrões: "sexta às 14h", "na segunda 09:00", "sexta-feira 14:30"
|
||||
const padroes = [
|
||||
/(?:na\s+)?(\w+(?:-\w+)?)\s+(?:às?\s*)?(\d{1,2}):?(\d{0,2})(?:h)?/i,
|
||||
/(?:na\s+)?(\w+(?:-\w+)?)\s+(?:às?\s*)?(\d{1,2})(?:h)?(\d{0,2})?/i
|
||||
];
|
||||
|
||||
for (const padrao of padroes) {
|
||||
const match = texto.match(padrao);
|
||||
if (match) {
|
||||
const diaNome = match[1].toLowerCase();
|
||||
const hora = parseInt(match[2]);
|
||||
const minuto = match[3] ? parseInt(match[3]) : 0;
|
||||
|
||||
if (this.diasSemana.hasOwnProperty(diaNome)) {
|
||||
const diaSemanaTarget = this.diasSemana[diaNome];
|
||||
const proxData = this.obterProximoDiaSemana(diaSemanaTarget);
|
||||
|
||||
if (this.isValidTime(hora, minuto)) {
|
||||
// Obter componentes da data no fuso Brasil
|
||||
const partes = this.extrairPartesBrasil(proxData);
|
||||
const data = this.criarDataBrasil(partes.ano, partes.mes, partes.dia, hora, minuto);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data,
|
||||
originalText: texto,
|
||||
parsed: {
|
||||
type: 'dia_semana',
|
||||
dia: diaNome,
|
||||
hora,
|
||||
minuto
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* Processar palavras temporais (hoje, amanhã, etc)
|
||||
*/
|
||||
processarPalavrasTemporal(texto) {
|
||||
// Padrões: "amanhã às 17h", "hoje 14:30", "depois de amanhã 09h"
|
||||
const padroes = [
|
||||
/(hoje|amanhã|amanha|depois de amanhã|depois de amanha)\s+(?:às?\s*)?(\d{1,2}):?(\d{0,2})(?:h)?/i,
|
||||
/(hoje|amanhã|amanha|depois de amanhã|depois de amanha)\s+(?:às?\s*)?(\d{1,2})(?:h)?(\d{0,2})?/i
|
||||
];
|
||||
|
||||
for (const padrao of padroes) {
|
||||
const match = texto.match(padrao);
|
||||
if (match) {
|
||||
const palavraTemporal = match[1].toLowerCase();
|
||||
const hora = parseInt(match[2]);
|
||||
const minuto = match[3] ? parseInt(match[3]) : 0;
|
||||
|
||||
if (this.palavrasTemporais.hasOwnProperty(palavraTemporal)) {
|
||||
const diasOffset = this.palavrasTemporais[palavraTemporal];
|
||||
|
||||
if (this.isValidTime(hora, minuto)) {
|
||||
// Basear no dia atual no fuso Brasil e aplicar offset
|
||||
const hoje = this.extrairPartesBrasil(new Date());
|
||||
const base = this.criarDataBrasil(hoje.ano, hoje.mes, hoje.dia, 12, 0);
|
||||
base.setDate(base.getDate() + diasOffset);
|
||||
const partes = this.extrairPartesBrasil(base);
|
||||
const data = this.criarDataBrasil(partes.ano, partes.mes, partes.dia, hora, minuto);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data,
|
||||
originalText: texto,
|
||||
parsed: {
|
||||
type: 'palavra_temporal',
|
||||
palavra: palavraTemporal,
|
||||
offset: diasOffset,
|
||||
hora,
|
||||
minuto
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* Processar datas específicas
|
||||
*/
|
||||
processarDataEspecifica(texto) {
|
||||
// Padrões: "15/08 às 14h", "15/08/2024 14:30", "15 de agosto 09h"
|
||||
const padroes = [
|
||||
/(\d{1,2})\/(\d{1,2})(?:\/(\d{2,4}))?\s+(?:às?\s*)?(\d{1,2}):?(\d{0,2})(?:h)?/i,
|
||||
/(\d{1,2})\s+de\s+(\w+)(?:\s+de\s+(\d{2,4}))?\s+(?:às?\s*)?(\d{1,2}):?(\d{0,2})(?:h)?/i
|
||||
];
|
||||
|
||||
for (const padrao of padroes) {
|
||||
const match = texto.match(padrao);
|
||||
if (match) {
|
||||
let dia, mes, ano, hora, minuto;
|
||||
|
||||
if (match[0].includes('/')) {
|
||||
// Formato DD/MM/YYYY
|
||||
dia = parseInt(match[1]);
|
||||
mes = parseInt(match[2]) - 1; // JavaScript usa 0-11 para meses
|
||||
ano = match[3] ? parseInt(match[3]) : new Date().getFullYear();
|
||||
hora = parseInt(match[4]);
|
||||
minuto = match[5] ? parseInt(match[5]) : 0;
|
||||
} else {
|
||||
// Formato "DD de MMMM"
|
||||
dia = parseInt(match[1]);
|
||||
const mesNome = match[2].toLowerCase();
|
||||
mes = this.meses[mesNome];
|
||||
ano = match[3] ? parseInt(match[3]) : new Date().getFullYear();
|
||||
hora = parseInt(match[4]);
|
||||
minuto = match[5] ? parseInt(match[5]) : 0;
|
||||
}
|
||||
|
||||
if (ano < 100) ano += 2000; // Converter anos de 2 dígitos
|
||||
|
||||
if (mes !== undefined && this.isValidDate(dia, mes, ano) && this.isValidTime(hora, minuto)) {
|
||||
const data = this.criarDataBrasil(ano, mes, dia, hora, minuto);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data,
|
||||
originalText: texto,
|
||||
parsed: {
|
||||
type: 'data_especifica',
|
||||
dia,
|
||||
mes: mes + 1, // Para display humano
|
||||
ano,
|
||||
hora,
|
||||
minuto
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* Processar datas relativas
|
||||
*/
|
||||
processarDataRelativa(texto) {
|
||||
// Padrões: "em 3 dias às 14h", "daqui a 2 horas", "em 1 semana"
|
||||
const padroes = [
|
||||
/(?:em|daqui\s+a)\s+(\d+)\s+(dias?|semanas?|meses?)\s+(?:às?\s*)?(\d{1,2}):?(\d{0,2})(?:h)?/i,
|
||||
/(?:em|daqui\s+a)\s+(\d+)\s+(horas?|minutos?)/i
|
||||
];
|
||||
|
||||
for (const padrao of padroes) {
|
||||
const match = texto.match(padrao);
|
||||
if (match) {
|
||||
const quantidade = parseInt(match[1]);
|
||||
const unidade = match[2].toLowerCase();
|
||||
|
||||
let data = new Date();
|
||||
|
||||
if (unidade.includes('dia')) {
|
||||
const base = this.extrairPartesBrasil(new Date());
|
||||
const tmpDate = this.criarDataBrasil(base.ano, base.mes, base.dia, 12, 0);
|
||||
tmpDate.setDate(tmpDate.getDate() + quantidade);
|
||||
if (match[3] && match[4] !== undefined) {
|
||||
const hora = parseInt(match[3]);
|
||||
const minuto = match[4] ? parseInt(match[4]) : 0;
|
||||
if (this.isValidTime(hora, minuto)) {
|
||||
const partes = this.extrairPartesBrasil(tmpDate);
|
||||
data = this.criarDataBrasil(partes.ano, partes.mes, partes.dia, hora, minuto);
|
||||
} else {
|
||||
data = tmpDate;
|
||||
}
|
||||
} else {
|
||||
data = tmpDate;
|
||||
}
|
||||
} else if (unidade.includes('semana')) {
|
||||
const base = this.extrairPartesBrasil(new Date());
|
||||
const tmpDate = this.criarDataBrasil(base.ano, base.mes, base.dia, 12, 0);
|
||||
tmpDate.setDate(tmpDate.getDate() + (quantidade * 7));
|
||||
if (match[3] && match[4] !== undefined) {
|
||||
const hora = parseInt(match[3]);
|
||||
const minuto = match[4] ? parseInt(match[4]) : 0;
|
||||
if (this.isValidTime(hora, minuto)) {
|
||||
const partes = this.extrairPartesBrasil(tmpDate);
|
||||
data = this.criarDataBrasil(partes.ano, partes.mes, partes.dia, hora, minuto);
|
||||
} else {
|
||||
data = tmpDate;
|
||||
}
|
||||
} else {
|
||||
data = tmpDate;
|
||||
}
|
||||
} else if (unidade.includes('mes')) {
|
||||
const base = this.extrairPartesBrasil(new Date());
|
||||
const tmpDate = this.criarDataBrasil(base.ano, base.mes, base.dia, 12, 0);
|
||||
tmpDate.setMonth(tmpDate.getMonth() + quantidade);
|
||||
if (match[3] && match[4] !== undefined) {
|
||||
const hora = parseInt(match[3]);
|
||||
const minuto = match[4] ? parseInt(match[4]) : 0;
|
||||
if (this.isValidTime(hora, minuto)) {
|
||||
const partes = this.extrairPartesBrasil(tmpDate);
|
||||
data = this.criarDataBrasil(partes.ano, partes.mes, partes.dia, hora, minuto);
|
||||
} else {
|
||||
data = tmpDate;
|
||||
}
|
||||
} else {
|
||||
data = tmpDate;
|
||||
}
|
||||
} else if (unidade.includes('hora')) {
|
||||
data.setHours(data.getHours() + quantidade);
|
||||
} else if (unidade.includes('minuto')) {
|
||||
data.setMinutes(data.getMinutes() + quantidade);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data,
|
||||
originalText: texto,
|
||||
parsed: {
|
||||
type: 'data_relativa',
|
||||
quantidade,
|
||||
unidade
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* Obter próximo dia da semana
|
||||
*/
|
||||
obterProximoDiaSemana(diaSemanaTarget) {
|
||||
const hoje = new Date();
|
||||
const diaAtual = hoje.getDay();
|
||||
|
||||
let diasParaAdicionar = diaSemanaTarget - diaAtual;
|
||||
if (diasParaAdicionar <= 0) {
|
||||
diasParaAdicionar += 7; // Próxima semana
|
||||
}
|
||||
|
||||
const proximaData = new Date(hoje);
|
||||
proximaData.setDate(hoje.getDate() + diasParaAdicionar);
|
||||
|
||||
return proximaData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validar se a data é válida
|
||||
*/
|
||||
isValidDate(dia, mes, ano) {
|
||||
if (dia < 1 || dia > 31) return false;
|
||||
if (mes < 0 || mes > 11) return false;
|
||||
if (ano < 1900 || ano > 2100) return false;
|
||||
|
||||
// Verificar dias válidos para o mês
|
||||
const diasNoMes = new Date(ano, mes + 1, 0).getDate();
|
||||
return dia <= diasNoMes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validar se a hora é válida
|
||||
*/
|
||||
isValidTime(hora, minuto) {
|
||||
return hora >= 0 && hora <= 23 && minuto >= 0 && minuto <= 59;
|
||||
}
|
||||
|
||||
/**
|
||||
* Criar um Date com hora no fuso America/Araguaina,
|
||||
* independente do TZ do servidor.
|
||||
*/
|
||||
criarDataBrasil(ano, mes, dia, hora, minuto) {
|
||||
// Constrói uma string ISO no fuso -03:00 para garantir
|
||||
// que a hora digitada pelo usuário seja tratada como BR.
|
||||
const pad = (n) => String(n).padStart(2, '0');
|
||||
const isoStr = `${ano}-${pad(mes + 1)}-${pad(dia)}T${pad(hora)}:${pad(minuto)}:00-03:00`;
|
||||
return new Date(isoStr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrair {ano, mes(0-11), dia} de um Date no fuso Brasil.
|
||||
*/
|
||||
extrairPartesBrasil(data) {
|
||||
const fmt = new Intl.DateTimeFormat('en-CA', {
|
||||
timeZone: 'America/Araguaina',
|
||||
year: 'numeric', month: '2-digit', day: '2-digit'
|
||||
});
|
||||
// en-CA produz YYYY-MM-DD
|
||||
const [ano, mes, dia] = fmt.format(data).split('-').map(Number);
|
||||
return { ano, mes: mes - 1, dia };
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatar data para string legível
|
||||
*/
|
||||
formatarData(data) {
|
||||
if (!(data instanceof Date)) return 'Data inválida';
|
||||
|
||||
const opcoes = {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
timeZone: 'America/Araguaina'
|
||||
};
|
||||
|
||||
return data.toLocaleDateString('pt-BR', opcoes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatar data para o formato do sistema (YYYY-MM-DD HH:mm:ss)
|
||||
* Retorna sempre no horário de Brasília.
|
||||
*/
|
||||
formatarParaSistema(data) {
|
||||
if (!(data instanceof Date)) return null;
|
||||
|
||||
// Formata no fuso de Brasília para não depender do TZ do servidor
|
||||
const fmt = new Intl.DateTimeFormat('sv-SE', {
|
||||
timeZone: 'America/Araguaina',
|
||||
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||
hour: '2-digit', minute: '2-digit', second: '2-digit',
|
||||
hour12: false
|
||||
});
|
||||
// sv-SE produz YYYY-MM-DD HH:mm:ss
|
||||
return fmt.format(data).replace('T', ' ');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DateProcessor;
|
||||
const logger = require('./logger');
|
||||
|
||||
class DateProcessor {
|
||||
constructor() {
|
||||
// Dicionário de dias da semana
|
||||
this.diasSemana = {
|
||||
'domingo': 0, 'dom': 0,
|
||||
'segunda': 1, 'segunda-feira': 1, 'seg': 1,
|
||||
'terça': 2, 'terca': 2, 'terça-feira': 2, 'terca-feira': 2, 'ter': 2,
|
||||
'quarta': 3, 'quarta-feira': 3, 'qua': 3,
|
||||
'quinta': 4, 'quinta-feira': 4, 'qui': 4,
|
||||
'sexta': 5, 'sexta-feira': 5, 'sex': 5,
|
||||
'sábado': 6, 'sabado': 6, 'sab': 6
|
||||
};
|
||||
|
||||
// Dicionário de meses
|
||||
this.meses = {
|
||||
'janeiro': 0, 'jan': 0,
|
||||
'fevereiro': 1, 'fev': 1,
|
||||
'março': 2, 'mar': 2, 'marco': 2,
|
||||
'abril': 3, 'abr': 3,
|
||||
'maio': 4, 'mai': 4,
|
||||
'junho': 5, 'jun': 5,
|
||||
'julho': 6, 'jul': 6,
|
||||
'agosto': 7, 'ago': 7,
|
||||
'setembro': 8, 'set': 8,
|
||||
'outubro': 9, 'out': 9,
|
||||
'novembro': 10, 'nov': 10,
|
||||
'dezembro': 11, 'dez': 11
|
||||
};
|
||||
|
||||
// Palavras-chave temporais
|
||||
this.palavrasTemporais = {
|
||||
'hoje': 0,
|
||||
'amanhã': 1, 'amanha': 1,
|
||||
'depois de amanhã': 2, 'depois de amanha': 2,
|
||||
'ontem': -1,
|
||||
'anteontem': -2
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Processar texto em linguagem natural para extrair data e hora
|
||||
* @param {string} texto - Texto em linguagem natural
|
||||
* @returns {Object} - { success: boolean, data: Date, originalText: string, parsed: Object }
|
||||
*/
|
||||
processarTexto(texto) {
|
||||
if (!texto || typeof texto !== 'string') {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Texto inválido'
|
||||
};
|
||||
}
|
||||
|
||||
const textoNormalizado = texto.toLowerCase().trim();
|
||||
logger.debug('Processando texto de data/hora:', { texto: textoNormalizado });
|
||||
|
||||
try {
|
||||
// Tentar diferentes padrões de reconhecimento
|
||||
let resultado = null;
|
||||
|
||||
// 1. Padrões com dias da semana
|
||||
resultado = this.processarDiaSemana(textoNormalizado);
|
||||
if (resultado.success) return resultado;
|
||||
|
||||
// 2. Padrões com palavras temporais (hoje, amanhã, etc)
|
||||
resultado = this.processarPalavrasTemporal(textoNormalizado);
|
||||
if (resultado.success) return resultado;
|
||||
|
||||
// 3. Padrões de data específica (DD/MM, DD/MM/YYYY)
|
||||
resultado = this.processarDataEspecifica(textoNormalizado);
|
||||
if (resultado.success) return resultado;
|
||||
|
||||
// 4. Padrões relativos (em X dias, daqui a X horas)
|
||||
resultado = this.processarDataRelativa(textoNormalizado);
|
||||
if (resultado.success) return resultado;
|
||||
|
||||
// Se não conseguiu processar
|
||||
return {
|
||||
success: false,
|
||||
error: 'Não foi possível interpretar a data/hora',
|
||||
originalText: texto
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Erro ao processar data/hora:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
originalText: texto
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processar padrões com dias da semana
|
||||
*/
|
||||
processarDiaSemana(texto) {
|
||||
// Padrões: "sexta às 14h", "na segunda 09:00", "sexta-feira 14:30"
|
||||
const padroes = [
|
||||
/(?:na\s+)?(\w+(?:-\w+)?)\s+(?:às?\s*)?(\d{1,2}):?(\d{0,2})(?:h)?/i,
|
||||
/(?:na\s+)?(\w+(?:-\w+)?)\s+(?:às?\s*)?(\d{1,2})(?:h)?(\d{0,2})?/i
|
||||
];
|
||||
|
||||
for (const padrao of padroes) {
|
||||
const match = texto.match(padrao);
|
||||
if (match) {
|
||||
const diaNome = match[1].toLowerCase();
|
||||
const hora = parseInt(match[2]);
|
||||
const minuto = match[3] ? parseInt(match[3]) : 0;
|
||||
|
||||
if (this.diasSemana.hasOwnProperty(diaNome)) {
|
||||
const diaSemanaTarget = this.diasSemana[diaNome];
|
||||
const proxData = this.obterProximoDiaSemana(diaSemanaTarget);
|
||||
|
||||
if (this.isValidTime(hora, minuto)) {
|
||||
// Obter componentes da data no fuso Brasil
|
||||
const partes = this.extrairPartesBrasil(proxData);
|
||||
const data = this.criarDataBrasil(partes.ano, partes.mes, partes.dia, hora, minuto);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data,
|
||||
originalText: texto,
|
||||
parsed: {
|
||||
type: 'dia_semana',
|
||||
dia: diaNome,
|
||||
hora,
|
||||
minuto
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* Processar palavras temporais (hoje, amanhã, etc)
|
||||
*/
|
||||
processarPalavrasTemporal(texto) {
|
||||
// Padrões: "amanhã às 17h", "hoje 14:30", "depois de amanhã 09h"
|
||||
const padroes = [
|
||||
/(hoje|amanhã|amanha|depois de amanhã|depois de amanha)\s+(?:às?\s*)?(\d{1,2}):?(\d{0,2})(?:h)?/i,
|
||||
/(hoje|amanhã|amanha|depois de amanhã|depois de amanha)\s+(?:às?\s*)?(\d{1,2})(?:h)?(\d{0,2})?/i
|
||||
];
|
||||
|
||||
for (const padrao of padroes) {
|
||||
const match = texto.match(padrao);
|
||||
if (match) {
|
||||
const palavraTemporal = match[1].toLowerCase();
|
||||
const hora = parseInt(match[2]);
|
||||
const minuto = match[3] ? parseInt(match[3]) : 0;
|
||||
|
||||
if (this.palavrasTemporais.hasOwnProperty(palavraTemporal)) {
|
||||
const diasOffset = this.palavrasTemporais[palavraTemporal];
|
||||
|
||||
if (this.isValidTime(hora, minuto)) {
|
||||
// Basear no dia atual no fuso Brasil e aplicar offset
|
||||
const hoje = this.extrairPartesBrasil(new Date());
|
||||
const base = this.criarDataBrasil(hoje.ano, hoje.mes, hoje.dia, 12, 0);
|
||||
base.setDate(base.getDate() + diasOffset);
|
||||
const partes = this.extrairPartesBrasil(base);
|
||||
const data = this.criarDataBrasil(partes.ano, partes.mes, partes.dia, hora, minuto);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data,
|
||||
originalText: texto,
|
||||
parsed: {
|
||||
type: 'palavra_temporal',
|
||||
palavra: palavraTemporal,
|
||||
offset: diasOffset,
|
||||
hora,
|
||||
minuto
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* Processar datas específicas
|
||||
*/
|
||||
processarDataEspecifica(texto) {
|
||||
// Padrões: "15/08 às 14h", "15/08/2024 14:30", "15 de agosto 09h"
|
||||
const padroes = [
|
||||
/(\d{1,2})\/(\d{1,2})(?:\/(\d{2,4}))?\s+(?:às?\s*)?(\d{1,2}):?(\d{0,2})(?:h)?/i,
|
||||
/(\d{1,2})\s+de\s+(\w+)(?:\s+de\s+(\d{2,4}))?\s+(?:às?\s*)?(\d{1,2}):?(\d{0,2})(?:h)?/i
|
||||
];
|
||||
|
||||
for (const padrao of padroes) {
|
||||
const match = texto.match(padrao);
|
||||
if (match) {
|
||||
let dia, mes, ano, hora, minuto;
|
||||
|
||||
if (match[0].includes('/')) {
|
||||
// Formato DD/MM/YYYY
|
||||
dia = parseInt(match[1]);
|
||||
mes = parseInt(match[2]) - 1; // JavaScript usa 0-11 para meses
|
||||
ano = match[3] ? parseInt(match[3]) : new Date().getFullYear();
|
||||
hora = parseInt(match[4]);
|
||||
minuto = match[5] ? parseInt(match[5]) : 0;
|
||||
} else {
|
||||
// Formato "DD de MMMM"
|
||||
dia = parseInt(match[1]);
|
||||
const mesNome = match[2].toLowerCase();
|
||||
mes = this.meses[mesNome];
|
||||
ano = match[3] ? parseInt(match[3]) : new Date().getFullYear();
|
||||
hora = parseInt(match[4]);
|
||||
minuto = match[5] ? parseInt(match[5]) : 0;
|
||||
}
|
||||
|
||||
if (ano < 100) ano += 2000; // Converter anos de 2 dígitos
|
||||
|
||||
if (mes !== undefined && this.isValidDate(dia, mes, ano) && this.isValidTime(hora, minuto)) {
|
||||
const data = this.criarDataBrasil(ano, mes, dia, hora, minuto);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data,
|
||||
originalText: texto,
|
||||
parsed: {
|
||||
type: 'data_especifica',
|
||||
dia,
|
||||
mes: mes + 1, // Para display humano
|
||||
ano,
|
||||
hora,
|
||||
minuto
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* Processar datas relativas
|
||||
*/
|
||||
processarDataRelativa(texto) {
|
||||
// Padrões: "em 3 dias às 14h", "daqui a 2 horas", "em 1 semana"
|
||||
const padroes = [
|
||||
/(?:em|daqui\s+a)\s+(\d+)\s+(dias?|semanas?|meses?)\s+(?:às?\s*)?(\d{1,2}):?(\d{0,2})(?:h)?/i,
|
||||
/(?:em|daqui\s+a)\s+(\d+)\s+(horas?|minutos?)/i
|
||||
];
|
||||
|
||||
for (const padrao of padroes) {
|
||||
const match = texto.match(padrao);
|
||||
if (match) {
|
||||
const quantidade = parseInt(match[1]);
|
||||
const unidade = match[2].toLowerCase();
|
||||
|
||||
let data = new Date();
|
||||
|
||||
if (unidade.includes('dia')) {
|
||||
const base = this.extrairPartesBrasil(new Date());
|
||||
const tmpDate = this.criarDataBrasil(base.ano, base.mes, base.dia, 12, 0);
|
||||
tmpDate.setDate(tmpDate.getDate() + quantidade);
|
||||
if (match[3] && match[4] !== undefined) {
|
||||
const hora = parseInt(match[3]);
|
||||
const minuto = match[4] ? parseInt(match[4]) : 0;
|
||||
if (this.isValidTime(hora, minuto)) {
|
||||
const partes = this.extrairPartesBrasil(tmpDate);
|
||||
data = this.criarDataBrasil(partes.ano, partes.mes, partes.dia, hora, minuto);
|
||||
} else {
|
||||
data = tmpDate;
|
||||
}
|
||||
} else {
|
||||
data = tmpDate;
|
||||
}
|
||||
} else if (unidade.includes('semana')) {
|
||||
const base = this.extrairPartesBrasil(new Date());
|
||||
const tmpDate = this.criarDataBrasil(base.ano, base.mes, base.dia, 12, 0);
|
||||
tmpDate.setDate(tmpDate.getDate() + (quantidade * 7));
|
||||
if (match[3] && match[4] !== undefined) {
|
||||
const hora = parseInt(match[3]);
|
||||
const minuto = match[4] ? parseInt(match[4]) : 0;
|
||||
if (this.isValidTime(hora, minuto)) {
|
||||
const partes = this.extrairPartesBrasil(tmpDate);
|
||||
data = this.criarDataBrasil(partes.ano, partes.mes, partes.dia, hora, minuto);
|
||||
} else {
|
||||
data = tmpDate;
|
||||
}
|
||||
} else {
|
||||
data = tmpDate;
|
||||
}
|
||||
} else if (unidade.includes('mes')) {
|
||||
const base = this.extrairPartesBrasil(new Date());
|
||||
const tmpDate = this.criarDataBrasil(base.ano, base.mes, base.dia, 12, 0);
|
||||
tmpDate.setMonth(tmpDate.getMonth() + quantidade);
|
||||
if (match[3] && match[4] !== undefined) {
|
||||
const hora = parseInt(match[3]);
|
||||
const minuto = match[4] ? parseInt(match[4]) : 0;
|
||||
if (this.isValidTime(hora, minuto)) {
|
||||
const partes = this.extrairPartesBrasil(tmpDate);
|
||||
data = this.criarDataBrasil(partes.ano, partes.mes, partes.dia, hora, minuto);
|
||||
} else {
|
||||
data = tmpDate;
|
||||
}
|
||||
} else {
|
||||
data = tmpDate;
|
||||
}
|
||||
} else if (unidade.includes('hora')) {
|
||||
data.setHours(data.getHours() + quantidade);
|
||||
} else if (unidade.includes('minuto')) {
|
||||
data.setMinutes(data.getMinutes() + quantidade);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data,
|
||||
originalText: texto,
|
||||
parsed: {
|
||||
type: 'data_relativa',
|
||||
quantidade,
|
||||
unidade
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* Obter próximo dia da semana
|
||||
*/
|
||||
obterProximoDiaSemana(diaSemanaTarget) {
|
||||
const hoje = new Date();
|
||||
const diaAtual = hoje.getDay();
|
||||
|
||||
let diasParaAdicionar = diaSemanaTarget - diaAtual;
|
||||
if (diasParaAdicionar <= 0) {
|
||||
diasParaAdicionar += 7; // Próxima semana
|
||||
}
|
||||
|
||||
const proximaData = new Date(hoje);
|
||||
proximaData.setDate(hoje.getDate() + diasParaAdicionar);
|
||||
|
||||
return proximaData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validar se a data é válida
|
||||
*/
|
||||
isValidDate(dia, mes, ano) {
|
||||
if (dia < 1 || dia > 31) return false;
|
||||
if (mes < 0 || mes > 11) return false;
|
||||
if (ano < 1900 || ano > 2100) return false;
|
||||
|
||||
// Verificar dias válidos para o mês
|
||||
const diasNoMes = new Date(ano, mes + 1, 0).getDate();
|
||||
return dia <= diasNoMes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validar se a hora é válida
|
||||
*/
|
||||
isValidTime(hora, minuto) {
|
||||
return hora >= 0 && hora <= 23 && minuto >= 0 && minuto <= 59;
|
||||
}
|
||||
|
||||
/**
|
||||
* Criar um Date com hora no fuso America/Araguaina,
|
||||
* independente do TZ do servidor.
|
||||
*/
|
||||
criarDataBrasil(ano, mes, dia, hora, minuto) {
|
||||
// Constrói uma string ISO no fuso -03:00 para garantir
|
||||
// que a hora digitada pelo usuário seja tratada como BR.
|
||||
const pad = (n) => String(n).padStart(2, '0');
|
||||
const isoStr = `${ano}-${pad(mes + 1)}-${pad(dia)}T${pad(hora)}:${pad(minuto)}:00-03:00`;
|
||||
return new Date(isoStr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrair {ano, mes(0-11), dia} de um Date no fuso Brasil.
|
||||
*/
|
||||
extrairPartesBrasil(data) {
|
||||
const fmt = new Intl.DateTimeFormat('en-CA', {
|
||||
timeZone: 'America/Araguaina',
|
||||
year: 'numeric', month: '2-digit', day: '2-digit'
|
||||
});
|
||||
// en-CA produz YYYY-MM-DD
|
||||
const [ano, mes, dia] = fmt.format(data).split('-').map(Number);
|
||||
return { ano, mes: mes - 1, dia };
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatar data para string legível
|
||||
*/
|
||||
formatarData(data) {
|
||||
if (!(data instanceof Date)) return 'Data inválida';
|
||||
|
||||
const opcoes = {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
timeZone: 'America/Araguaina'
|
||||
};
|
||||
|
||||
return data.toLocaleDateString('pt-BR', opcoes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatar data para o formato do sistema (YYYY-MM-DD HH:mm:ss)
|
||||
* Retorna sempre no horário de Brasília.
|
||||
*/
|
||||
formatarParaSistema(data) {
|
||||
if (!(data instanceof Date)) return null;
|
||||
|
||||
// Formata no fuso de Brasília para não depender do TZ do servidor
|
||||
const fmt = new Intl.DateTimeFormat('sv-SE', {
|
||||
timeZone: 'America/Araguaina',
|
||||
year: 'numeric', month: '2-digit', day: '2-digit',
|
||||
hour: '2-digit', minute: '2-digit', second: '2-digit',
|
||||
hour12: false
|
||||
});
|
||||
// sv-SE produz YYYY-MM-DD HH:mm:ss
|
||||
return fmt.format(data).replace('T', ' ');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DateProcessor;
|
||||
+173
-173
@@ -1,173 +1,173 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
class Logger {
|
||||
constructor() {
|
||||
this.logDir = path.join(__dirname, '../../logs');
|
||||
this.logFile = path.join(this.logDir, 'bot-debug.log');
|
||||
this.maxLogSize = 10 * 1024 * 1024; // 10MB
|
||||
this.maxBackups = 5;
|
||||
|
||||
// Criar diretório de logs se não existir
|
||||
this.ensureLogDir();
|
||||
|
||||
// Só ativa logs se estiver em modo debug
|
||||
this.isDebug = process.env.NODE_ENV === 'development' || process.env.DEBUG === 'true';
|
||||
}
|
||||
|
||||
ensureLogDir() {
|
||||
if (!fs.existsSync(this.logDir)) {
|
||||
fs.mkdirSync(this.logDir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
formatMessage(level, message, data = null) {
|
||||
const timestamp = new Date().toISOString();
|
||||
let logLine = `[${timestamp}] [${level}] ${message}`;
|
||||
|
||||
if (data) {
|
||||
if (typeof data === 'object') {
|
||||
logLine += '\n' + JSON.stringify(data, null, 2);
|
||||
} else {
|
||||
logLine += ` - ${data}`;
|
||||
}
|
||||
}
|
||||
|
||||
return logLine + '\n';
|
||||
}
|
||||
|
||||
rotateLogIfNeeded() {
|
||||
if (!fs.existsSync(this.logFile)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stats = fs.statSync(this.logFile);
|
||||
if (stats.size > this.maxLogSize) {
|
||||
// Fazer backup dos logs existentes
|
||||
for (let i = this.maxBackups - 1; i >= 0; i--) {
|
||||
const oldFile = i === 0 ? this.logFile : `${this.logFile}.${i}`;
|
||||
const newFile = `${this.logFile}.${i + 1}`;
|
||||
|
||||
if (fs.existsSync(oldFile)) {
|
||||
if (i === this.maxBackups - 1) {
|
||||
fs.unlinkSync(oldFile); // Deletar o mais antigo
|
||||
} else {
|
||||
fs.renameSync(oldFile, newFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writeLog(level, message, data = null) {
|
||||
if (!this.isDebug) {
|
||||
return; // Só loga em modo debug
|
||||
}
|
||||
|
||||
try {
|
||||
this.rotateLogIfNeeded();
|
||||
const logMessage = this.formatMessage(level, message, data);
|
||||
|
||||
// Escrever no arquivo de log
|
||||
fs.appendFileSync(this.logFile, logMessage);
|
||||
|
||||
// Também mostrar no console se for erro ou warning
|
||||
if (level === 'ERROR' || level === 'WARN') {
|
||||
console.log(logMessage.trim());
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro ao escrever log:', error);
|
||||
}
|
||||
}
|
||||
|
||||
debug(message, data = null) {
|
||||
this.writeLog('DEBUG', message, data);
|
||||
}
|
||||
|
||||
info(message, data = null) {
|
||||
this.writeLog('INFO', message, data);
|
||||
}
|
||||
|
||||
warn(message, data = null) {
|
||||
this.writeLog('WARN', message, data);
|
||||
}
|
||||
|
||||
error(message, data = null) {
|
||||
this.writeLog('ERROR', message, data);
|
||||
}
|
||||
|
||||
// Métodos específicos para o bot
|
||||
botCommand(userId, command, data = null) {
|
||||
this.debug(`Comando recebido - User: ${userId}, Command: ${command}`, data);
|
||||
}
|
||||
|
||||
apiCall(endpoint, params = null, response = null) {
|
||||
this.debug(`API Call - Endpoint: ${endpoint}`, {
|
||||
params,
|
||||
responseStatus: response?.status,
|
||||
responseDataLength: response?.data?.length
|
||||
});
|
||||
}
|
||||
|
||||
botError(userId, error, context = null) {
|
||||
this.error(`Bot Error - User: ${userId}`, {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
context
|
||||
});
|
||||
}
|
||||
|
||||
topsappOperation(operation, clientId, result = null) {
|
||||
this.debug(`TopSapp Operation - ${operation} - Cliente: ${clientId}`, {
|
||||
success: result?.success,
|
||||
dataCount: result?.data?.length || result?.servicos?.length || result?.clientes?.length,
|
||||
error: result?.error
|
||||
});
|
||||
}
|
||||
|
||||
// Limpar logs antigos (método utilitário)
|
||||
clearLogs() {
|
||||
if (!this.isDebug) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (fs.existsSync(this.logFile)) {
|
||||
fs.unlinkSync(this.logFile);
|
||||
}
|
||||
|
||||
// Limpar backups também
|
||||
for (let i = 1; i <= this.maxBackups; i++) {
|
||||
const backupFile = `${this.logFile}.${i}`;
|
||||
if (fs.existsSync(backupFile)) {
|
||||
fs.unlinkSync(backupFile);
|
||||
}
|
||||
}
|
||||
|
||||
this.info('Logs limpos com sucesso');
|
||||
} catch (error) {
|
||||
console.error('Erro ao limpar logs:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Obter últimas linhas do log
|
||||
getRecentLogs(lines = 100) {
|
||||
if (!this.isDebug || !fs.existsSync(this.logFile)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const content = fs.readFileSync(this.logFile, 'utf8');
|
||||
const allLines = content.split('\n').filter(line => line.trim());
|
||||
return allLines.slice(-lines);
|
||||
} catch (error) {
|
||||
console.error('Erro ao ler logs:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
const logger = new Logger();
|
||||
|
||||
module.exports = logger;
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
class Logger {
|
||||
constructor() {
|
||||
this.logDir = path.join(__dirname, '../../logs');
|
||||
this.logFile = path.join(this.logDir, 'bot-debug.log');
|
||||
this.maxLogSize = 10 * 1024 * 1024; // 10MB
|
||||
this.maxBackups = 5;
|
||||
|
||||
// Criar diretório de logs se não existir
|
||||
this.ensureLogDir();
|
||||
|
||||
// Só ativa logs se estiver em modo debug
|
||||
this.isDebug = process.env.NODE_ENV === 'development' || process.env.DEBUG === 'true';
|
||||
}
|
||||
|
||||
ensureLogDir() {
|
||||
if (!fs.existsSync(this.logDir)) {
|
||||
fs.mkdirSync(this.logDir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
formatMessage(level, message, data = null) {
|
||||
const timestamp = new Date().toISOString();
|
||||
let logLine = `[${timestamp}] [${level}] ${message}`;
|
||||
|
||||
if (data) {
|
||||
if (typeof data === 'object') {
|
||||
logLine += '\n' + JSON.stringify(data, null, 2);
|
||||
} else {
|
||||
logLine += ` - ${data}`;
|
||||
}
|
||||
}
|
||||
|
||||
return logLine + '\n';
|
||||
}
|
||||
|
||||
rotateLogIfNeeded() {
|
||||
if (!fs.existsSync(this.logFile)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stats = fs.statSync(this.logFile);
|
||||
if (stats.size > this.maxLogSize) {
|
||||
// Fazer backup dos logs existentes
|
||||
for (let i = this.maxBackups - 1; i >= 0; i--) {
|
||||
const oldFile = i === 0 ? this.logFile : `${this.logFile}.${i}`;
|
||||
const newFile = `${this.logFile}.${i + 1}`;
|
||||
|
||||
if (fs.existsSync(oldFile)) {
|
||||
if (i === this.maxBackups - 1) {
|
||||
fs.unlinkSync(oldFile); // Deletar o mais antigo
|
||||
} else {
|
||||
fs.renameSync(oldFile, newFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writeLog(level, message, data = null) {
|
||||
if (!this.isDebug) {
|
||||
return; // Só loga em modo debug
|
||||
}
|
||||
|
||||
try {
|
||||
this.rotateLogIfNeeded();
|
||||
const logMessage = this.formatMessage(level, message, data);
|
||||
|
||||
// Escrever no arquivo de log
|
||||
fs.appendFileSync(this.logFile, logMessage);
|
||||
|
||||
// Também mostrar no console se for erro ou warning
|
||||
if (level === 'ERROR' || level === 'WARN') {
|
||||
console.log(logMessage.trim());
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erro ao escrever log:', error);
|
||||
}
|
||||
}
|
||||
|
||||
debug(message, data = null) {
|
||||
this.writeLog('DEBUG', message, data);
|
||||
}
|
||||
|
||||
info(message, data = null) {
|
||||
this.writeLog('INFO', message, data);
|
||||
}
|
||||
|
||||
warn(message, data = null) {
|
||||
this.writeLog('WARN', message, data);
|
||||
}
|
||||
|
||||
error(message, data = null) {
|
||||
this.writeLog('ERROR', message, data);
|
||||
}
|
||||
|
||||
// Métodos específicos para o bot
|
||||
botCommand(userId, command, data = null) {
|
||||
this.debug(`Comando recebido - User: ${userId}, Command: ${command}`, data);
|
||||
}
|
||||
|
||||
apiCall(endpoint, params = null, response = null) {
|
||||
this.debug(`API Call - Endpoint: ${endpoint}`, {
|
||||
params,
|
||||
responseStatus: response?.status,
|
||||
responseDataLength: response?.data?.length
|
||||
});
|
||||
}
|
||||
|
||||
botError(userId, error, context = null) {
|
||||
this.error(`Bot Error - User: ${userId}`, {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
context
|
||||
});
|
||||
}
|
||||
|
||||
topsappOperation(operation, clientId, result = null) {
|
||||
this.debug(`TopSapp Operation - ${operation} - Cliente: ${clientId}`, {
|
||||
success: result?.success,
|
||||
dataCount: result?.data?.length || result?.servicos?.length || result?.clientes?.length,
|
||||
error: result?.error
|
||||
});
|
||||
}
|
||||
|
||||
// Limpar logs antigos (método utilitário)
|
||||
clearLogs() {
|
||||
if (!this.isDebug) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (fs.existsSync(this.logFile)) {
|
||||
fs.unlinkSync(this.logFile);
|
||||
}
|
||||
|
||||
// Limpar backups também
|
||||
for (let i = 1; i <= this.maxBackups; i++) {
|
||||
const backupFile = `${this.logFile}.${i}`;
|
||||
if (fs.existsSync(backupFile)) {
|
||||
fs.unlinkSync(backupFile);
|
||||
}
|
||||
}
|
||||
|
||||
this.info('Logs limpos com sucesso');
|
||||
} catch (error) {
|
||||
console.error('Erro ao limpar logs:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Obter últimas linhas do log
|
||||
getRecentLogs(lines = 100) {
|
||||
if (!this.isDebug || !fs.existsSync(this.logFile)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const content = fs.readFileSync(this.logFile, 'utf8');
|
||||
const allLines = content.split('\n').filter(line => line.trim());
|
||||
return allLines.slice(-lines);
|
||||
} catch (error) {
|
||||
console.error('Erro ao ler logs:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
const logger = new Logger();
|
||||
|
||||
module.exports = logger;
|
||||
Reference in new issue
Block a user