Ir para o conteúdo
Menu principal
Menu principal
mover para a barra lateral
Esconder
Navegação
Página principal
Mudanças recentes
Página aleatória
Ajuda do MediaWiki
Wiki Grupo LLAL
Pesquisa
Pesquisar
Crie uma conta
Entrar
Ferramentas pessoais
Crie uma conta
Entrar
Páginas para editores conectados
saiba mais
Contribuições
Discussão
Editando
COMO CONFIGURAR O GLPI AO WHATSAPP
(seção)
Página
Discussão
português do Brasil
Ler
Editar
Ver histórico
Ferramentas
Ferramentas
mover para a barra lateral
Esconder
Ações
Ler
Editar
Ver histórico
Geral
Páginas afluentes
Mudanças relacionadas
Páginas especiais
Informações da página
Aviso:
Você não está conectado. Seu endereço IP será visível publicamente se você fizer alguma edição. Se você
fizer login
ou
criar uma conta
, suas edições serão atribuídas ao seu nome de usuário, juntamente com outros benefícios.
Verificação contra spam.
Não
preencha isto!
=== Código-fonte completo === <syntaxhighlight lang="javascript"> require('dotenv').config(); const axios = require('axios'); const qrcode = require('qrcode-terminal'); const { Client, LocalAuth } = require('whatsapp-web.js'); const GLPI_URL = (process.env.GLPI_URL || '').replace(/\/$/, ''); const GLPI_APP_TOKEN = process.env.GLPI_APP_TOKEN; const GLPI_USER_TOKEN = process.env.GLPI_USER_TOKEN; const CHROMIUM_PATH = process.env.CHROMIUM_PATH || '/usr/bin/chromium'; const TICKET_PREFIX = process.env.TICKET_PREFIX || 'WhatsApp'; if (!GLPI_URL || !GLPI_APP_TOKEN || !GLPI_USER_TOKEN) { console.error('Faltam variáveis no arquivo .env'); process.exit(1); } const TI_EMAIL = 'tecnologia@grupollal.com.br'; const SESSION_TIMEOUT_MS = 15 * 60 * 1000; const MIN_MESSAGE_INTERVAL_MS = 1200; const MAX_DESCRIPTION_LENGTH = 1800; const MAX_ATTACHMENTS = 5; const ETAPAS = { MENU_PRINCIPAL: 'menu_principal', AGUARDANDO_CATEGORIA: 'aguardando_categoria', AGUARDANDO_DESCRICAO: 'aguardando_descricao', AGUARDANDO_CONSULTA: 'aguardando_consulta' }; const conversas = new Map(); const ultimoProcessamento = new Map(); const MSG_MENU_PRINCIPAL = `👋 *Olá! Sou o assistente de chamados da TI.* Escolha uma opção: *1* - 📝 Abrir chamado *2* - 🔎 Consultar chamado *3* - 👨💻 Falar com TI Comandos: *0* - ↩️ Voltar *#* - 🏠 Menu inicial`; const MSG_MENU_CATEGORIA = `🗂️ *Escolha a categoria do chamado:* *1* - 🌐 Rede / Internet *2* - 💻 Computador / Notebook *3* - 🖨️ Impressora *4* - 🔐 Acesso / Senha *5* - 🧩 Sistema / ERP *6* - 📦 Outro Comandos: *0* - ↩️ Voltar *#* - 🏠 Menu inicial`; const MSG_PEDIR_DESCRICAO = `✍️ *Agora descreva o problema com detalhes.* Você também pode enviar: 📷 imagem 📄 PDF 📝 texto + imagem/PDF na mesma mensagem 🚫 Vídeos não são aceitos. Comandos: *0* - ↩️ Voltar *#* - 🏠 Menu inicial`; const MSG_PEDIR_NUMERO_CHAMADO = `🔎 *Informe o número do chamado.* Exemplo: *154* Comandos: *0* - ↩️ Voltar *#* - 🏠 Menu inicial`; function textoInvalido(menuTexto) { return `❌ *Opção inválida.*\n\n${menuTexto}`; } function normalizarTexto(texto) { return (texto || '').trim().replace(/\s+/g, ' '); } function isMenuCommand(texto) { const t = normalizarTexto(texto).toLowerCase(); return t === '#' || t === 'menu' || t === 'inicio' || t === 'início' || t === 'oi' || t === 'olá' || t === 'ola'; } function criarEstadoInicial() { return { etapa: ETAPAS.MENU_PRINCIPAL, historico: [], updatedAt: Date.now(), categoria: null, anexos: [] }; } function atualizarEstado(numero, novoEstado) { conversas.set(numero, { ...novoEstado, updatedAt: Date.now() }); } function expirarSeNecessario(numero) { const estado = conversas.get(numero); if (!estado) return null; if (Date.now() - (estado.updatedAt || 0) > SESSION_TIMEOUT_MS) { conversas.delete(numero); return null; } return estado; } function pushHistorico(estado, etapaAtual) { const historico = Array.isArray(estado.historico) ? [...estado.historico] : []; historico.push(etapaAtual); return historico; } function getCategoria(opcao) { const mapa = { '1': { nome: '🌐 Rede / Internet' }, '2': { nome: '💻 Computador / Notebook' }, '3': { nome: '🖨️ Impressora' }, '4': { nome: '🔐 Acesso / Senha' }, '5': { nome: '🧩 Sistema / ERP' }, '6': { nome: '📦 Outro' } }; return mapa[opcao] || null; } function voltarEstado(numero) { const estado = conversas.get(numero); if (!estado) { const inicial = criarEstadoInicial(); atualizarEstado(numero, inicial); return { mensagem: MSG_MENU_PRINCIPAL }; } const historico = Array.isArray(estado.historico) ? [...estado.historico] : []; const etapaAnterior = historico.pop(); if (!etapaAnterior) { const inicial = criarEstadoInicial(); atualizarEstado(numero, inicial); return { mensagem: `↩️ Você já está no início.\n\n${MSG_MENU_PRINCIPAL}` }; } let novoEstado = { ...estado, historico }; if (etapaAnterior === ETAPAS.MENU_PRINCIPAL) { novoEstado = criarEstadoInicial(); atualizarEstado(numero, novoEstado); return { mensagem: MSG_MENU_PRINCIPAL }; } if (etapaAnterior === ETAPAS.AGUARDANDO_CATEGORIA) { novoEstado.etapa = ETAPAS.AGUARDANDO_CATEGORIA; novoEstado.categoria = null; atualizarEstado(numero, novoEstado); return { mensagem: MSG_MENU_CATEGORIA }; } if (etapaAnterior === ETAPAS.AGUARDANDO_DESCRICAO) { novoEstado.etapa = ETAPAS.AGUARDANDO_DESCRICAO; atualizarEstado(numero, novoEstado); return { mensagem: MSG_PEDIR_DESCRICAO }; } if (etapaAnterior === ETAPAS.AGUARDANDO_CONSULTA) { novoEstado.etapa = ETAPAS.AGUARDANDO_CONSULTA; atualizarEstado(numero, novoEstado); return { mensagem: MSG_PEDIR_NUMERO_CHAMADO }; } novoEstado = criarEstadoInicial(); atualizarEstado(numero, novoEstado); return { mensagem: MSG_MENU_PRINCIPAL }; } function statusTicketLabel(status) { const mapa = { 1: 'Novo', 2: 'Em atendimento', 3: 'Planejado', 4: 'Pendente', 5: 'Solucionado', 6: 'Fechado' }; return mapa[status] || `Status ${status}`; } function statusUsuarioFinal(status) { if (status === 5 || status === 6) return 'Finalizado'; return 'Em andamento'; } function formatDateBr(value) { if (!value) return 'Não informado'; const d = new Date(value); if (isNaN(d.getTime())) return String(value); return d.toLocaleString('pt-BR'); } function stripHtml(html) { if (!html) return ''; return String(html) .replace(/<br\s*\/?>/gi, '\n') .replace(/<\/p>/gi, '\n') .replace(/<[^>]*>/g, '') .replace(/ /g, ' ') .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .trim(); } function extractTicketId(texto) { const m = normalizarTexto(texto).match(/\d+/); return m ? parseInt(m[0], 10) : null; } function sanitizeFilename(name) { return (name || 'arquivo') .replace(/[^\w.\-]/g, '_') .slice(0, 120); } function getMediaTypeInfo(mimetype) { const mt = (mimetype || '').toLowerCase(); if (mt === 'application/pdf') { return { accepted: true, type: 'pdf', ext: '.pdf' }; } if (mt === 'image/jpeg' || mt === 'image/jpg') { return { accepted: true, type: 'image', ext: '.jpg' }; } if (mt === 'image/png') { return { accepted: true, type: 'image', ext: '.png' }; } if (mt === 'image/webp') { return { accepted: true, type: 'image', ext: '.webp' }; } if (mt.startsWith('video/')) { return { accepted: false, type: 'video', ext: '' }; } return { accepted: false, type: 'other', ext: '' }; } async function initGlpiSession() { const response = await axios.get(`${GLPI_URL}/apirest.php/initSession`, { headers: { 'Content-Type': 'application/json', 'Authorization': `user_token ${GLPI_USER_TOKEN}`, 'App-Token': GLPI_APP_TOKEN }, timeout: 15000 }); return response.data.session_token; } async function killGlpiSession(sessionToken) { try { await axios.get(`${GLPI_URL}/apirest.php/killSession`, { headers: { 'Session-Token': sessionToken, 'App-Token': GLPI_APP_TOKEN }, timeout: 10000 }); } catch (_) {} } async function criarTicketGLPI({ numero, nome, categoria, descricao, anexos }) { const sessionToken = await initGlpiSession(); try { const qtdAnexos = Array.isArray(anexos) ? anexos.length : 0; const linhaAnexos = qtdAnexos > 0 ? `\n\n📎 Anexos enviados pelo WhatsApp: ${qtdAnexos}` : ''; const payload = { input: { name: `${TICKET_PREFIX} - ${categoria.nome} - ${numero}`, content: `📲 Origem: WhatsApp 👤 Nome: ${nome || 'Não informado'} 📞 Número: ${numero} 🗂️ Categoria: ${categoria.nome} 📝 Descrição: ${descricao}${linhaAnexos}`, urgency: 3, impact: 3, priority: 3 } }; const response = await axios.post(`${GLPI_URL}/apirest.php/Ticket/`, payload, { headers: { 'Content-Type': 'application/json', 'Session-Token': sessionToken, 'App-Token': GLPI_APP_TOKEN }, timeout: 20000 }); return response.data.id; } finally { await killGlpiSession(sessionToken); } } async function consultarTicketResumo(ticketId) { const sessionToken = await initGlpiSession(); try { const ticketRes = await axios.get(`${GLPI_URL}/apirest.php/Ticket/${ticketId}`, { headers: { 'Session-Token': sessionToken, 'App-Token': GLPI_APP_TOKEN }, timeout: 15000 }); const ticket = ticketRes.data; let followups = []; try { const followRes = await axios.get(`${GLPI_URL}/apirest.php/Ticket/${ticketId}/ITILFollowup`, { headers: { 'Session-Token': sessionToken, 'App-Token': GLPI_APP_TOKEN }, timeout: 15000 }); followups = Array.isArray(followRes.data) ? followRes.data : []; } catch (_) { followups = []; } let ultimaMensagem = ''; if (followups.length > 0) { const ultima = followups[followups.length - 1]; ultimaMensagem = stripHtml(ultima.content || ultima.name || ''); } if (!ultimaMensagem && ticket.content) { ultimaMensagem = stripHtml(ticket.content); } return { id: ticket.id, status: ticket.status, statusLabel: statusTicketLabel(ticket.status), statusUsuario: statusUsuarioFinal(ticket.status), date_mod: ticket.date_mod, mensagemUsuario: ultimaMensagem || 'Ainda não há retorno registrado pela TI.' }; } finally { await killGlpiSession(sessionToken); } } async function receberAnexo(msg, estado) { if (!msg.hasMedia) return { saved: false }; if (!estado || estado.etapa !== ETAPAS.AGUARDANDO_DESCRICAO) { return { saved: false, warning: '📎 Recebi um arquivo, mas primeiro escolha *1 - Abrir chamado* no menu.' }; } if ((estado.anexos || []).length >= MAX_ATTACHMENTS) { return { saved: false, warning: `⚠️ Limite de anexos atingido. Máximo: ${MAX_ATTACHMENTS} arquivos.` }; } const media = await msg.downloadMedia(); if (!media) { return { saved: false, warning: '⚠️ Não consegui baixar o arquivo. Tente enviar novamente.' }; } const mediaInfo = getMediaTypeInfo(media.mimetype); if (!mediaInfo.accepted) { if (mediaInfo.type === 'video') { return { saved: false, warning: '🚫 *Vídeos não são aceitos.*\n\nEnvie apenas *imagens* ou *PDF*.' }; } return { saved: false, warning: '⚠️ Tipo de arquivo não aceito.\n\nEnvie apenas *imagens* ou *PDF*.' }; } const file = { filename: sanitizeFilename(media.filename || `anexo_${Date.now()}${mediaInfo.ext}`), mimetype: media.mimetype, data: media.data }; estado.anexos = estado.anexos || []; estado.anexos.push(file); return { saved: true, count: estado.anexos.length, file }; } const client = new Client({ authStrategy: new LocalAuth({ clientId: 'glpi-whatsapp-bot' }), puppeteer: { headless: true, executablePath: CHROMIUM_PATH, args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu' ] }, qrMaxRetries: 5 }); client.on('qr', (qr) => { console.log('\n=== ESCANEIE O QR CODE NO WHATSAPP ===\n'); qrcode.generate(qr, { small: true }); }); client.on('authenticated', () => { console.log('WhatsApp autenticado com sucesso.'); }); client.on('ready', () => { console.log('Bot do WhatsApp pronto.'); }); client.on('auth_failure', (msg) => { console.error('Falha na autenticação:', msg); }); client.on('disconnected', (reason) => { console.error('WhatsApp desconectado:', reason); }); client.on('message', async (msg) => { try { if (msg.fromMe) return; if (msg.from.includes('@g.us')) return; const agora = Date.now(); const ultimo = ultimoProcessamento.get(msg.from) || 0; if (agora - ultimo < MIN_MESSAGE_INTERVAL_MS) return; ultimoProcessamento.set(msg.from, agora); const numero = msg.from.replace('@c.us', ''); const contato = await msg.getContact(); const nome = contato.pushname || contato.name || 'Sem nome'; const texto = normalizarTexto(msg.body || ''); let estado = expirarSeNecessario(numero); if (isMenuCommand(texto)) { estado = criarEstadoInicial(); atualizarEstado(numero, estado); await msg.reply(MSG_MENU_PRINCIPAL); return; } if (texto === '0') { const retorno = voltarEstado(numero); await msg.reply(retorno.mensagem); return; } if (!estado) { estado = criarEstadoInicial(); atualizarEstado(numero, estado); await msg.reply(MSG_MENU_PRINCIPAL); return; } if (estado.etapa === ETAPAS.MENU_PRINCIPAL) { if (texto === '1') { atualizarEstado(numero, { ...estado, etapa: ETAPAS.AGUARDANDO_CATEGORIA, historico: pushHistorico(estado, ETAPAS.MENU_PRINCIPAL), anexos: [] }); await msg.reply(MSG_MENU_CATEGORIA); return; } if (texto === '2') { atualizarEstado(numero, { ...estado, etapa: ETAPAS.AGUARDANDO_CONSULTA, historico: pushHistorico(estado, ETAPAS.MENU_PRINCIPAL) }); await msg.reply(MSG_PEDIR_NUMERO_CHAMADO); return; } if (texto === '3') { await msg.reply( `👨💻 *Falar com TI* Você pode entrar em contato pelo e-mail: 📧 *${TI_EMAIL}* Se quiser voltar, envie *#*.` ); return; } await msg.reply(textoInvalido(MSG_MENU_PRINCIPAL)); return; } if (estado.etapa === ETAPAS.AGUARDANDO_CATEGORIA) { const categoria = getCategoria(texto); if (!categoria) { await msg.reply(textoInvalido(MSG_MENU_CATEGORIA)); return; } atualizarEstado(numero, { ...estado, etapa: ETAPAS.AGUARDANDO_DESCRICAO, historico: pushHistorico(estado, ETAPAS.AGUARDANDO_CATEGORIA), categoria, anexos: estado.anexos || [] }); await msg.reply(`✅ Categoria selecionada: ${categoria.nome}\n\n${MSG_PEDIR_DESCRICAO}`); return; } if (estado.etapa === ETAPAS.AGUARDANDO_DESCRICAO) { const mediaResult = await receberAnexo(msg, estado); if (mediaResult.warning) { await msg.reply(mediaResult.warning); return; } atualizarEstado(numero, estado); if (msg.hasMedia && !texto) { await msg.reply(`📎 Arquivo recebido com sucesso.\nTotal de anexos: *${estado.anexos.length}*\n\nAgora envie a descrição do chamado.`); return; } if (!texto) { await msg.reply(MSG_PEDIR_DESCRICAO); return; } if (texto.length < 5) { await msg.reply(`❌ *Descrição muito curta.*\n\n${MSG_PEDIR_DESCRICAO}`); return; } const descricao = texto.slice(0, MAX_DESCRIPTION_LENGTH); const ticketId = await criarTicketGLPI({ numero, nome, categoria: estado.categoria, descricao, anexos: estado.anexos }); const anexosOk = Array.isArray(estado.anexos) ? estado.anexos.length : 0; const linhaAnexo = anexosOk > 0 ? `📎 Arquivos recebidos: *${anexosOk}*\n` : ''; await msg.reply( `✅ *Chamado criado com sucesso!* 🎫 Número: *#${ticketId}* 🗂️ Categoria: ${estado.categoria.nome} ${linhaAnexo}Se quiser consultar depois, envie *#* e escolha *2 - Consultar chamado*.` ); conversas.delete(numero); return; } if (estado.etapa === ETAPAS.AGUARDANDO_CONSULTA) { const ticketId = extractTicketId(texto); if (!ticketId) { await msg.reply(`❌ Número inválido.\n\n${MSG_PEDIR_NUMERO_CHAMADO}`); return; } const resumo = await consultarTicketResumo(ticketId); await msg.reply( `🔎 *Chamado #${resumo.id}* 📌 Situação: *${resumo.statusUsuario}* 📍 Status interno: ${resumo.statusLabel} 🕒 Última atualização: ${formatDateBr(resumo.date_mod)} 💬 Retorno da TI: ${resumo.mensagemUsuario} Se quiser voltar ao menu, envie *#*.` ); conversas.delete(numero); return; } const inicial = criarEstadoInicial(); atualizarEstado(numero, inicial); await msg.reply(MSG_MENU_PRINCIPAL); } catch (error) { console.error('Erro ao processar mensagem:', error.response?.data || error.message); try { await msg.reply(`⚠️ *Não consegui processar sua solicitação agora.*\n\nEnvie *#* para voltar ao menu inicial.`); } catch (_) {} } }); client.initialize(); </syntaxhighlight>
Resumo da edição:
Por favor, note que todas as suas contribuições em Wiki Grupo LLAL podem ser editadas, alteradas ou removidas por outros contribuidores. Se você não deseja que o seu texto seja inexoravelmente editado, não o envie.
Você está, ao mesmo tempo, a garantir-nos que isto é algo escrito por si, ou algo copiado de alguma fonte de textos em domínio público ou similarmente de teor livre (veja
My wiki:Direitos de autor
para detalhes).
NÃO ENVIE TRABALHO PROTEGIDO POR DIREITOS DE AUTOR SEM A DEVIDA PERMISSÃO!
Cancelar
Ajuda de edição
(abre numa nova janela)
Alternar largura de conteúdo limitada