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!
= Implantação do GLPI com Integração de Atendimento via WhatsApp = __TOC__ <div style="border:1px solid #a2a9b1; background:#f8f9fa; padding:12px; margin:10px 0;"> '''Objetivo desta documentação'''<br> Documentar a implantação completa do ambiente '''GLPI + Bot de WhatsApp''', incluindo instalação, configuração da API, criação do bot, autenticação via QR Code, fluxo de funcionamento, testes e erros comuns encontrados durante a implantação. </div> == Visão geral == Esta solução foi implantada para permitir que usuários abram e consultem chamados de TI por meio do '''WhatsApp''', com integração ao '''GLPI''' via '''API REST'''. === Funcionalidades entregues === * Abertura de chamado via WhatsApp * Escolha de categoria por menu * Envio de descrição textual * Envio de imagem * Envio de PDF * Envio de texto junto com imagem/PDF * Consulta de chamado pelo número * Retorno do status do chamado * Exibição do último retorno registrado pela TI * Opção para contato direto com a equipe de TI por e-mail == Arquitetura da solução == <pre> Usuário -> WhatsApp -> Bot Node.js -> API REST do GLPI -> Ticket </pre> === Componentes utilizados === {| class="wikitable" ! Componente ! Tecnologia / Ferramenta |- | Sistema operacional | Debian 12 |- | Servidor web | Apache2 |- | Banco de dados | MariaDB |- | Plataforma de chamados | GLPI |- | Runtime do bot | Node.js |- | Navegador para sessão do WhatsApp | Chromium |- | Biblioteca de integração WhatsApp | whatsapp-web.js |- | Variáveis de ambiente | dotenv |- | Cliente HTTP | axios |- | Geração de QR Code no terminal | qrcode-terminal |} == Pré-requisitos == Antes de iniciar, garantir que o servidor possua: * acesso à internet * privilégios administrativos * IP acessível na rede local * portas necessárias liberadas * ambiente Debian 12 atualizado <div style="border-left:4px solid #36c; background:#eef6ff; padding:10px; margin:10px 0;"> '''Nota'''<br> Os exemplos abaixo consideram que o GLPI será publicado localmente por IP e que o bot será executado no mesmo servidor ou em servidor com acesso HTTP ao GLPI. </div> == Instalação do GLPI == === Atualização do sistema === <syntaxhighlight lang="bash"> sudo apt update && sudo apt full-upgrade -y </syntaxhighlight> === Instalação dos pacotes necessários === <syntaxhighlight lang="bash"> sudo apt install -y \ apache2 \ mariadb-server \ wget tar bzip2 unzip curl \ php8.2 \ libapache2-mod-php8.2 \ php8.2-cli \ php8.2-common \ php8.2-mysql \ php8.2-xml \ php8.2-curl \ php8.2-gd \ php8.2-intl \ php8.2-bcmath \ php8.2-mbstring \ php8.2-zip \ php8.2-bz2 \ php8.2-ldap \ php8.2-opcache </syntaxhighlight> === Habilitar os serviços === <syntaxhighlight lang="bash"> sudo systemctl enable --now apache2 sudo systemctl enable --now mariadb </syntaxhighlight> === Segurança inicial do banco === <syntaxhighlight lang="bash"> sudo mysql_secure_installation </syntaxhighlight> Durante a execução, aplicar as opções recomendadas: * definir senha para o usuário root do banco * remover usuários anônimos * desabilitar login root remoto * remover base de testes * recarregar privilégios === Criação do banco do GLPI === Entrar no MariaDB: <syntaxhighlight lang="bash"> sudo mariadb </syntaxhighlight> Executar: <syntaxhighlight lang="sql"> CREATE DATABASE glpidb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER 'glpi'@'localhost' IDENTIFIED BY 'SUA_SENHA_FORTE_AQUI'; GRANT ALL PRIVILEGES ON glpidb.* TO 'glpi'@'localhost'; FLUSH PRIVILEGES; EXIT; </syntaxhighlight> === Download e extração do GLPI === <syntaxhighlight lang="bash"> cd /tmp wget https://github.com/glpi-project/glpi/releases/download/11.0.6/glpi-11.0.6.tgz sudo tar -xzf glpi-11.0.6.tgz -C /var/www/ </syntaxhighlight> O diretório final ficará em: <pre> /var/www/glpi </pre> === Configuração do Apache === Criar o arquivo de virtual host: <syntaxhighlight lang="bash"> sudo nano /etc/apache2/sites-available/glpi.conf </syntaxhighlight> Inserir: <syntaxhighlight lang="apache"> <VirtualHost *:80> ServerName glpi.local DocumentRoot /var/www/glpi/public <Directory /var/www/glpi/public> Require all granted RewriteEngine On RewriteCond %{HTTP:Authorization} ^(.+)$ RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^(.*)$ index.php [QSA,L] </Directory> ErrorLog ${APACHE_LOG_DIR}/glpi_error.log CustomLog ${APACHE_LOG_DIR}/glpi_access.log combined </VirtualHost> </syntaxhighlight> Ativar a configuração: <syntaxhighlight lang="bash"> sudo a2enmod rewrite sudo a2dissite 000-default.conf sudo a2ensite glpi.conf sudo systemctl reload apache2 </syntaxhighlight> === Finalização pelo navegador === Acessar: <pre> http://IP_DO_SERVIDOR </pre> Seguir o assistente gráfico do GLPI e informar: * banco: <code>glpidb</code> * usuário: <code>glpi</code> * senha: a definida na criação do banco == Configuração da API do GLPI == === Habilitar a API REST === No GLPI, acessar: <pre> Configurar > Geral > API </pre> Habilitar: * API REST * login com credenciais * login externo por token Salvar as alterações. === Criar cliente da API === Ainda em: <pre> Configurar > Geral > API </pre> Criar um cliente com o nome: <pre> bot-whatsapp </pre> Após salvar, copiar o '''App-Token''' gerado. Esse valor será usado na variável: <pre> GLPI_APP_TOKEN </pre> === Criar usuário técnico do bot === No GLPI, acessar: <pre> Administração > Usuários </pre> Criar o usuário técnico: {| class="wikitable" ! Campo ! Valor |- | Usuário | wabot |- | Nome | Bot WhatsApp |- | Ativo | Sim |} === Gerar token do usuário === No cadastro do usuário <code>wabot</code>, acessar: <pre> Senhas e chaves de acesso </pre> Gerar ou regenerar o '''Token de API'''. Esse valor será usado na variável: <pre> GLPI_USER_TOKEN </pre> <div style="border-left:4px solid #d33; background:#fff5f5; padding:10px; margin:10px 0;"> '''Atenção'''<br> Não confundir: * '''GLPI_APP_TOKEN''' = token do cliente da API * '''GLPI_USER_TOKEN''' = token do usuário técnico </div> == Testes iniciais da API == === Teste de sessão === <syntaxhighlight lang="bash"> curl -s -X GET \ -H "Content-Type: application/json" \ -H "Authorization: user_token SEU_TOKEN_USUARIO" \ -H "App-Token: SEU_APP_TOKEN" \ "http://SEU_IP/apirest.php/initSession" </syntaxhighlight> Saída esperada: <syntaxhighlight lang="json"> {"session_token":"TOKEN_AQUI"} </syntaxhighlight> === Teste de criação de ticket === <syntaxhighlight lang="bash"> curl -s -X POST \ -H "Content-Type: application/json" \ -H "Session-Token: SEU_SESSION_TOKEN" \ -H "App-Token: SEU_APP_TOKEN" \ -d '{"input":{"name":"Teste API GLPI","content":"Chamado criado via API","urgency":3,"impact":3,"priority":3}}' \ "http://SEU_IP/apirest.php/Ticket/" </syntaxhighlight> == Instalação do bot do WhatsApp == === Instalar Node.js === <syntaxhighlight lang="bash"> sudo apt update sudo apt install -y curl ca-certificates gnupg curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - sudo apt install -y nodejs </syntaxhighlight> Validar a instalação: <syntaxhighlight lang="bash"> node -v npm -v </syntaxhighlight> === Instalar Chromium e dependências === <syntaxhighlight lang="bash"> sudo apt install -y chromium fonts-liberation libasound2 libatk-bridge2.0-0 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm1 libglib2.0-0 libgtk-3-0 libnspr4 libnss3 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 xdg-utils </syntaxhighlight> === Criar diretório do projeto === <syntaxhighlight lang="bash"> mkdir -p /opt/wa-glpi-bot cd /opt/wa-glpi-bot </syntaxhighlight> === Inicializar projeto Node.js === <syntaxhighlight lang="bash"> npm init -y npm install whatsapp-web.js qrcode-terminal axios dotenv </syntaxhighlight> == Configuração do arquivo .env == Criar o arquivo: <syntaxhighlight lang="bash"> nano /opt/wa-glpi-bot/.env </syntaxhighlight> Conteúdo: <syntaxhighlight lang="ini"> GLPI_URL=http://192.168.1.81 GLPI_APP_TOKEN=SEU_APP_TOKEN GLPI_USER_TOKEN=SEU_TOKEN_USUARIO CHROMIUM_PATH=/usr/bin/chromium TICKET_PREFIX=WhatsApp </syntaxhighlight> === Significado das variáveis === {| class="wikitable" ! Variável ! Função |- | GLPI_URL | Endereço base do GLPI |- | GLPI_APP_TOKEN | Token do cliente da API |- | GLPI_USER_TOKEN | Token do usuário técnico |- | CHROMIUM_PATH | Caminho do executável do Chromium |- | TICKET_PREFIX | Prefixo usado no título dos chamados |} == Criação do arquivo principal do bot == === Remover arquivo antigo, se existir === <syntaxhighlight lang="bash"> rm -f /opt/wa-glpi-bot/index.js </syntaxhighlight> === Criar novo arquivo === <syntaxhighlight lang="bash"> nano /opt/wa-glpi-bot/index.js </syntaxhighlight> === 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> === Salvar e validar === Salvar no nano: * <code>Ctrl + O</code> * <code>Enter</code> * <code>Ctrl + X</code> Testar sintaxe: <syntaxhighlight lang="bash"> node --check /opt/wa-glpi-bot/index.js </syntaxhighlight> == Execução do bot == === Iniciar manualmente === <syntaxhighlight lang="bash"> cd /opt/wa-glpi-bot node index.js </syntaxhighlight> Saída esperada: <pre> Bot do WhatsApp pronto. </pre> Se a sessão ainda não estiver autenticada, aparecerá o QR Code no terminal. == Autenticação no WhatsApp == === Como ler o QR Code === No celular: * abrir o WhatsApp * acessar '''Aparelhos conectados''' * clicar em '''Conectar um aparelho''' * escanear o QR Code exibido no terminal do servidor === Como forçar um novo QR Code === Se o aparelho foi desconectado ou a sessão foi perdida: <syntaxhighlight lang="bash"> pkill -f "node index.js" pkill -f chromium pkill -f chrome rm -rf /opt/wa-glpi-bot/.wwebjs_auth rm -rf /opt/wa-glpi-bot/.wwebjs_cache cd /opt/wa-glpi-bot node index.js </syntaxhighlight> == Fluxo funcional para o usuário == === Menu principal === {| class="wikitable" ! Opção ! Ação |- | 1 | Abrir chamado |- | 2 | Consultar chamado |- | 3 | Falar com TI |} === Abertura do chamado === O usuário pode enviar: * texto * imagem * PDF * texto + anexo na mesma interação === Exemplo de fluxo === <pre> oi 1 2 notebook não liga </pre> Ou: <pre> oi 1 2 [envio de imagem com legenda] </pre> Resultado esperado: * o bot recebe a categoria * recebe a descrição * registra que houve anexo, quando existir * cria o chamado no GLPI * devolve o número do ticket ao usuário === Consulta de chamado === Fluxo exemplo: <pre> 2 154 </pre> Retorno esperado: * situação do chamado * status interno * data da última atualização * último retorno da TI == Comportamento atual dos anexos == === O que acontece nesta versão === '''Registrado no GLPI:''' * ticket * descrição textual * categoria * quantidade de anexos recebidos '''Ainda não enviado fisicamente ao ticket:''' * imagem * PDF <div style="border-left:4px solid #eaecf0; background:#f8f9fa; padding:10px; margin:10px 0;"> '''Observação técnica'''<br> O bot recebe os anexos, mas o vínculo físico do documento ao ticket no GLPI foi removido nesta versão porque a associação via API não funcionou corretamente no ambiente implantado. </div> == Erros encontrados e correções == === ERROR_WRONG_APP_TOKEN_PARAMETER === '''Causa provável:''' * App-Token incorreto * token do usuário usado no lugar do App-Token '''Correção:''' Revisar o arquivo <code>.env</code>: <syntaxhighlight lang="ini"> GLPI_APP_TOKEN=TOKEN_DO_CLIENTE_API GLPI_USER_TOKEN=TOKEN_DO_USUARIO_WABOT </syntaxhighlight> === Cannot find module '/home/glpi/index.js' === '''Causa provável:''' Execução do comando no diretório errado. '''Correção:''' <syntaxhighlight lang="bash"> cd /opt/wa-glpi-bot node index.js </syntaxhighlight> === The browser is already running === '''Causa provável:''' Há processo antigo do Chromium ou Node ainda em execução. '''Correção:''' <syntaxhighlight lang="bash"> pkill -f "node index.js" pkill -f chromium pkill -f chrome cd /opt/wa-glpi-bot node index.js </syntaxhighlight> === QR Code não aparece === '''Correção recomendada:''' <syntaxhighlight lang="bash"> rm -rf /opt/wa-glpi-bot/.wwebjs_auth rm -rf /opt/wa-glpi-bot/.wwebjs_cache cd /opt/wa-glpi-bot node index.js </syntaxhighlight> === Unexpected token / arquivo JS quebrado === '''Causa provável:''' * comandos bash colados dentro do arquivo JavaScript * código JavaScript executado direto no shell '''Correção:''' * apagar o arquivo * recriar no editor correto * colar apenas o código JavaScript * validar com <code>node --check</code> == Comandos operacionais rápidos == === Iniciar o bot === <syntaxhighlight lang="bash"> cd /opt/wa-glpi-bot node index.js </syntaxhighlight> === Validar sintaxe do código === <syntaxhighlight lang="bash"> node --check /opt/wa-glpi-bot/index.js </syntaxhighlight> === Resetar sessão do WhatsApp === <syntaxhighlight lang="bash"> pkill -f "node index.js" pkill -f chromium pkill -f chrome rm -rf /opt/wa-glpi-bot/.wwebjs_auth rm -rf /opt/wa-glpi-bot/.wwebjs_cache cd /opt/wa-glpi-bot node index.js </syntaxhighlight> == Boas práticas operacionais == * manter backup do arquivo <code>.env</code> * nunca expor tokens em prints ou documentação pública * validar o código com <code>node --check</code> antes de executar * manter o Chromium instalado e funcional * documentar qualquer alteração de menu, categorias ou comportamento do bot == Conclusão == A implantação documentada permite operar um fluxo básico e funcional de abertura e consulta de chamados via WhatsApp integrado ao GLPI, com autenticação por QR Code e uso da API REST do GLPI para criação e leitura de tickets. A solução atende bem ao cenário de suporte inicial e pode ser evoluída futuramente para: * anexar arquivos fisicamente ao ticket * identificar usuário por base cadastral * direcionar categoria automaticamente * executar como serviço no sistema * registrar logs estruturados * integrar com banco de conhecimento ---- [[Categoria:GLPI]] [[Categoria:WhatsApp]] [[Categoria:Automação]] [[Categoria:Service Desk]] [[Categoria:Infraestrutura]] [[Criado por: Iran Ribeiro - Analista de Infraestrutura]] 25/03/2026 - 16:30PM
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