Chatbot basico (echo)
Receita minima — todo ola que chega volta como “voce disse: ola”. Pode evoluir trocando a logica de resposta por chamada a LLM, lookup em base, etc.
Pre-requisitos
- App criada com
webhook_urlapontando pro seu servidor - Chip vinculado com
can_receive=trueEcan_send=true webhook_secretsalvo em variavel de ambiente
Codigo (Node + Express)
import 'dotenv/config'import express from 'express'import crypto from 'node:crypto'
const HUB = process.env.WPPHUB_BASE_URL // https://hub.gustavomaritan.comconst APP_KEY = process.env.WPPHUB_APP_KEY // ak_live_...const SECRET = process.env.WPPHUB_WEBHOOK_SECRET // whsec_...const PORT = process.env.PORT || 3001
const app = express()
// CRITICO: precisa do body bruto pra validar HMAC.app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => { // 1. Valida HMAC const sig = req.header('x-hub-signature') || '' const expected = 'sha256=' + crypto.createHmac('sha256', SECRET).update(req.body).digest('hex') if ( sig.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)) ) { return res.status(401).send('invalid signature') }
const payload = JSON.parse(req.body.toString())
// 2. So responde a `message.received` direction=in (mensagens recebidas). if (payload.event !== 'message.received' || payload.data.direction !== 'in') { return res.sendStatus(200) // ignora outros eventos }
// 3. Ignora se nao for texto (pra simplificar) if (payload.data.type !== 'text') { return res.sendStatus(200) }
// 4. Responde imediatamente 2xx pra hub nao retentar res.sendStatus(200)
// 5. Dispara resposta async const m = payload.data const reply = `voce disse: ${m.content.text}`
try { await sendMessage({ chipId: m.chip_id, to: m.sender.phone_number || m.from, // fallback pra JID se phone vazio text: reply, idempotencyKey: `echo-${m.message_id}`, // dedup }) } catch (e) { console.error('failed to reply', e.message) }})
async function sendMessage({ chipId, to, text, idempotencyKey }) { const res = await fetch(`${HUB}/v1/messages`, { method: 'POST', headers: { Authorization: `Bearer ${APP_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ chip_id: chipId, to, type: 'text', content: { text }, idempotency_key: idempotencyKey, }), }) if (!res.ok) { const problem = await res.json().catch(() => ({})) throw new Error(`${res.status} ${problem.code || ''}: ${problem.detail || ''}`) } return res.json()}
app.listen(PORT, () => console.log(`echo bot on :${PORT}`))package.json
{ "type": "module", "dependencies": { "dotenv": "^16", "express": "^4" }}.env
WPPHUB_BASE_URL=https://hub.gustavomaritan.comWPPHUB_APP_KEY=ak_live_...WPPHUB_WEBHOOK_SECRET=whsec_...PORT=3001Como rodar
npm installnode server.jsExpor via ngrok http 3001 em DEV, configurar a URL no campo webhook_url da app.
Variacoes
Responder so a comandos especificos
const text = m.content.text.toLowerCase().trim()
if (text === '/hora') { await sendMessage({ ..., text: `agora: ${new Date().toLocaleString('pt-BR')}` })} else if (text.startsWith('/cep ')) { const cep = text.slice(5) // chama ViaCEP, responde endereco}Ignorar mensagens de grupo
if (m.chat.is_group) return res.sendStatus(200)Filtrar por business contact
if (!m.sender.is_business) return res.sendStatus(200) // so responde a contas businessLLM como cerebro
const response = await openai.chat.completions.create({ model: 'gpt-4o-mini', messages: [ { role: 'system', content: 'Atendente da loja X. Responda curto.' }, { role: 'user', content: m.content.text }, ],})const reply = response.choices[0].message.contentawait sendMessage({ ..., text: reply })Cuidados
- Rate limit do chip: se 100 pessoas mandam ao mesmo tempo, sua respostas vao acumular na fila do hub (rate limit + jitter). Backoff natural — nao precisa rate limit do seu lado.
- Loops infinitos: se voce responde a TODA mensagem recebida, e um contato comeca a responder, vai loopar. Mantenha logica defensiva (ignorar
from_me, ignorar mensagens recentes do proprio bot). - Idempotency: sempre mande
idempotency_key. Se webhook chegar duplicado (retry hub), voce nao reenvia.