COMO CONFIGURAR O GLPI AO WHATSAPP
Implantação do GLPI com Integração de Atendimento via WhatsApp[editar]
Objetivo desta documentação
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.
Visão geral[editar]
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[editar]
- 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[editar]
Usuário -> WhatsApp -> Bot Node.js -> API REST do GLPI -> Ticket
Componentes utilizados[editar]
| 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[editar]
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
Nota
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.
Instalação do GLPI[editar]
Atualização do sistema[editar]
<syntaxhighlight lang="bash"> sudo apt update && sudo apt full-upgrade -y </syntaxhighlight>
Instalação dos pacotes necessários[editar]
<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[editar]
<syntaxhighlight lang="bash"> sudo systemctl enable --now apache2 sudo systemctl enable --now mariadb </syntaxhighlight>
Segurança inicial do banco[editar]
<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[editar]
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[editar]
<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:
/var/www/glpi
Configuração do Apache[editar]
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>
[editar]
Acessar:
http://IP_DO_SERVIDOR
Seguir o assistente gráfico do GLPI e informar:
- banco:
glpidb - usuário:
glpi - senha: a definida na criação do banco
Configuração da API do GLPI[editar]
Habilitar a API REST[editar]
No GLPI, acessar:
Configurar > Geral > API
Habilitar:
- API REST
- login com credenciais
- login externo por token
Salvar as alterações.
Criar cliente da API[editar]
Ainda em:
Configurar > Geral > API
Criar um cliente com o nome:
bot-whatsapp
Após salvar, copiar o App-Token gerado.
Esse valor será usado na variável:
GLPI_APP_TOKEN
Criar usuário técnico do bot[editar]
No GLPI, acessar:
Administração > Usuários
Criar o usuário técnico:
| Campo | Valor |
|---|---|
| Usuário | wabot |
| Nome | Bot WhatsApp |
| Ativo | Sim |
Gerar token do usuário[editar]
No cadastro do usuário wabot, acessar:
Senhas e chaves de acesso
Gerar ou regenerar o Token de API.
Esse valor será usado na variável:
GLPI_USER_TOKEN
Atenção
Não confundir:
- GLPI_APP_TOKEN = token do cliente da API
- GLPI_USER_TOKEN = token do usuário técnico
Testes iniciais da API[editar]
Teste de sessão[editar]
<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[editar]
<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[editar]
Instalar Node.js[editar]
<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[editar]
<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[editar]
<syntaxhighlight lang="bash"> mkdir -p /opt/wa-glpi-bot cd /opt/wa-glpi-bot </syntaxhighlight>
Inicializar projeto Node.js[editar]
<syntaxhighlight lang="bash"> npm init -y npm install whatsapp-web.js qrcode-terminal axios dotenv </syntaxhighlight>
Configuração do arquivo .env[editar]
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[editar]
| 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[editar]
Remover arquivo antigo, se existir[editar]
<syntaxhighlight lang="bash"> rm -f /opt/wa-glpi-bot/index.js </syntaxhighlight>
Criar novo arquivo[editar]
<syntaxhighlight lang="bash"> nano /opt/wa-glpi-bot/index.js </syntaxhighlight>
Código-fonte completo[editar]
<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[editar]
Salvar no nano:
Ctrl + OEnterCtrl + X
Testar sintaxe:
<syntaxhighlight lang="bash"> node --check /opt/wa-glpi-bot/index.js </syntaxhighlight>
Execução do bot[editar]
Iniciar manualmente[editar]
<syntaxhighlight lang="bash"> cd /opt/wa-glpi-bot node index.js </syntaxhighlight>
Saída esperada:
Bot do WhatsApp pronto.
Se a sessão ainda não estiver autenticada, aparecerá o QR Code no terminal.
Autenticação no WhatsApp[editar]
Como ler o QR Code[editar]
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[editar]
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[editar]
Menu principal[editar]
| Opção | Ação |
|---|---|
| 1 | Abrir chamado |
| 2 | Consultar chamado |
| 3 | Falar com TI |
Abertura do chamado[editar]
O usuário pode enviar:
- texto
- imagem
- texto + anexo na mesma interação
Exemplo de fluxo[editar]
oi 1 2 notebook não liga
Ou:
oi 1 2 [envio de imagem com legenda]
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[editar]
Fluxo exemplo:
2 154
Retorno esperado:
- situação do chamado
- status interno
- data da última atualização
- último retorno da TI
Comportamento atual dos anexos[editar]
O que acontece nesta versão[editar]
Registrado no GLPI:
- ticket
- descrição textual
- categoria
- quantidade de anexos recebidos
Ainda não enviado fisicamente ao ticket:
- imagem
Observação técnica
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.
Erros encontrados e correções[editar]
ERROR_WRONG_APP_TOKEN_PARAMETER[editar]
Causa provável:
- App-Token incorreto
- token do usuário usado no lugar do App-Token
Correção:
Revisar o arquivo .env:
<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'[editar]
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[editar]
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[editar]
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[editar]
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
node --check
Comandos operacionais rápidos[editar]
Iniciar o bot[editar]
<syntaxhighlight lang="bash"> cd /opt/wa-glpi-bot node index.js </syntaxhighlight>
Validar sintaxe do código[editar]
<syntaxhighlight lang="bash"> node --check /opt/wa-glpi-bot/index.js </syntaxhighlight>
Resetar sessão do WhatsApp[editar]
<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[editar]
- manter backup do arquivo
.env - nunca expor tokens em prints ou documentação pública
- validar o código com
node --checkantes de executar - manter o Chromium instalado e funcional
- documentar qualquer alteração de menu, categorias ou comportamento do bot
Conclusão[editar]
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
Criado por: Iran Ribeiro - Analista de Infraestrutura 25/03/2026 - 16:30PM