WhatsApp TUI client built with Baileys + Ink (React for terminal).
Real-time chat, poll vote decryption, event logging, and payload extraction.
pnpm install
pnpm devScan the QR code with WhatsApp > Settings > Linked Devices. On subsequent runs it reconnects automatically.
┌─ WA Bot | [1:Chat] 2:Stats 3:Debug ──── ● danielzin ─┐
├─ CHATS ──────────────────────┐┌─ MESSAGES — General ─────┐
│ > # General (343) ││ 18:36 danielhe4rt eae │
│ # He4rt Delas 💕 (52) ││ 18:37 Clinton oi │
│ # Vagas (343) ││ 18:37 Bruna 🖼️ sticker│
│ # Comunidade Exemplo (89) ││ 18:38 danielhe4rt 📊 PHP │
│ @ danielhe4rt ││ ■ Sim!! (3) │
│ ││ □ Não (correto) (5) │
│ ││ 18:39 Mina 🗳️ → Sim │
└──────────────────────────────┘└──────────────────────────┘
┌─ > Message General... or /help ──────────────────────────┐
- 3 tabs: Chat, Stats, Debug — cycle with
Tabor/chat,/stats,/debug - Chat tab: sidebar with groups (
#) and DMs (@), message feed with auto-scroll - Stats tab: top senders bar chart, message type distribution, group activity
- Debug tab: real-time event stream with scroll, connection info, store counters
- QR code rendered inline in the terminal
- Message history loaded from logs on startup
- Group metadata fetched via WhatsApp API and cached to disk
- Poll decryption: votes decrypted with AES-256-GCM + HMAC-SHA256, shown per-user
Every WhatsApp event is persisted to logs/<event>/<date>.json with timestamps. Supported events:
| Event | Description |
|---|---|
messages.upsert |
Incoming/outgoing messages |
messages.update |
Delivery/read status changes |
messages.reaction |
Emoji reactions |
chats.update |
Chat metadata changes |
presence.update |
Typing/online indicators |
message-receipt.update |
Read receipts |
contacts.update |
Contact name changes |
group.member-tag.update |
Group member tags/roles |
connection.update |
Connection state changes |
creds.update |
Auth credential updates |
Parses raw logs into structured JSON files in extracted/<date>/:
| File | Content |
|---|---|
messages.json |
All messages with parsed content |
users.json |
Users with phone, name, groups |
groups-full.json |
Group metadata, admins, description, member tags |
reactions.json |
Emoji reactions per message, top emojis, top reactors |
threads.json |
Reply chains, most quoted messages |
polls.json |
Decrypted poll results with voter names |
member-profiles.json |
Users with group tags, admin status, activity breakdown |
activity.json |
Messages per hour, media breakdown, conversation starters |
stickers.json |
Sticker metadata (animated, AI, lottie) |
receipts.json |
Read/delivery timestamps |
presence.json |
Typing/online events |
timeline.json |
Unified chronological feed |
stats.json |
Summary statistics |
Optionally forward all events to an external webhook:
WEBHOOK_URL=https://your-webhook.example.com pnpm devPara rodar o coletor como serviço 24/7 (sem TUI, ex.: num server via systemd),
use o Modo headless. Ele reaproveita o mesmo núcleo de conexão da TUI
(startCollectorCore, ver docs/adr/0001):
uma única fonte de verdade para os dois modos.
pnpm build # gera dist/
pnpm start:headless # = node --env-file-if-exists=.env dist/index.js --headlessO modo é ativado por --headless na linha de comando ou pela env
HEADLESS=1 (aceita 1/true/yes). Sem nenhum dos dois, o dist/index.js
sobe a TUI normalmente.
O serviço headless de longa duração não pareia: continua fail-fast. Quem
estabelece a sessão é o Comando de pareamento one-shot, um processo à parte
que usa o pairing-code do Baileys — sem QR, sem TUI, sem scp do diretório de
auth (ver docs/adr/0003,
que supersede o 0002).
No próprio server, dentro do WorkingDirectory do serviço:
pnpm pair 5500900000002 # apenas dígitos com DDI (sem +, (), -, espaços)
# ou: make pair NUMBER=5500900000002- O comando imprime um código de 8 caracteres no stdout (estado e logs vão para o stderr — o stdout carrega só o código).
- No celular: Aparelhos conectados > Conectar com número e digite o código.
- Ao conectar, a sessão é gravada em
baileys_auth_info/e o processo sai com código 0. Sem parear em 120s, sai com código ≠ 0.
Detalhes:
- O comando não sobe o Coletor e não exige
WEBHOOK_URL/WHATSAPP_WEBHOOK_SECRET— é o caminho mínimo de login. - Se já existir uma sessão registrada, ele recusa (exit ≠ 0) para não destruir
uma sessão viva por engano. Use
--force(make pair NUMBER=… FORCE=1) para descartar a sessão atual e re-parear. - Número ausente (nem argumento nem env
PAIR_NUMBER) ou inválido → erro claro e exit ≠ 0, sem tocar o socket.
Se o serviço headless receber um evento de QR (sinal de que não há sessão
válida), ele loga FATAL (sem sessão válida) e sai com código ≠ 0 — em vez
de ficar gerando QRs num loop. Um loggedOut no server também derruba o coletor:
rode o Comando de pareamento de novo e reinicie o serviço (recuperação manual e
explícita, ADR-0003).
No headless, WEBHOOK_URL e WHATSAPP_WEBHOOK_SECRET são obrigatórias.
Se faltar qualquer uma, o processo loga FATAL e sai com código ≠ 0 antes de
conectar (fail-fast). No Modo TUI o coletor continua opcional. Veja o
.env.example para todas as variáveis.
O headless emite logs em JSON na stdout por padrão (ideal para journald).
Defina LOG_PRETTY=1 para formato legível via pino-pretty (debug local).
Não escreve wa-logs.txt a menos que WA_LOG_FILE seja definido explicitamente.
Há dois loggers, controlados separadamente, e toda linha carrega um campo
component (whatsapp / collector / retention / baileys):
| Variável | Logger | Padrão |
|---|---|---|
LOG_LEVEL |
app / coletor | info |
BAILEYS_LOG_LEVEL |
protocolo Baileys (barulhento) | warn |
Se LOG_RETENTION_DAYS estiver ausente/0, o boot emite um WARN alertando
sobre crescimento ilimitado de disco.
O serviço roda como unit de usuário (systemd --user), com a conta do
próprio login — sem sudo e sem /etc/systemd/system/. A unit-fonte
está em deploy/whatsapp-collector.service
e os alvos make svc-* cuidam do ciclo de vida. Nenhum precisa de sudo,
exceto make svc-linger.
pnpm install && pnpm build # 1. dependências + dist/
make pair NUMBER=5500900000002 # 2. pareia a sessão (ver seção acima)
make svc-install # 3. instala a unit em ~/.config/systemd/user,
# resolve o node absoluto e habilita+inicia
make svc-linger # 4. (único com sudo) roda sem login aberto
make svc-logs # segue os logs (JSON no journald do usuário)Configure ~/wpp-tui/.env (base no .env.example) antes do
svc-install — WEBHOOK_URL e WHATSAPP_WEBHOOK_SECRET são obrigatórias.
Demais alvos: svc-start, svc-stop, svc-restart, svc-status,
svc-redeploy (build + restart) e svc-uninstall.
O sinal de saúde é a linha de heartbeat dos logs (não há endpoint HTTP
/health nesta versão).
| Key | Action |
|---|---|
Tab |
Cycle tabs |
↑↓ |
Navigate chats (Chat tab) / Scroll (Debug tab) |
Ctrl+Q |
Quit |
/chat |
Switch to Chat tab |
/stats |
Switch to Stats tab |
/debug |
Switch to Debug tab |
/quit |
Quit |
pnpm dev # dev server with hot reload
pnpm build # production build with tsup
pnpm start # run production build (TUI)
pnpm start:headless # run production build in headless mode (collector, no TUI)
pnpm typecheck # type check without emitting
make extract-payload # extract structured data from logssrc/
├── index.tsx # entry point — routes to TUI or headless (dynamic import)
├── app.tsx # tab system, keyboard handling, layout
├── app-render.tsx # TUI renderer (extracted from index.tsx for dynamic import)
├── headless.ts # headless runner — bootstraps the collector core without UI
├── logging.ts # logger factory (app + baileys loggers with component tags)
├── logger.ts # legacy pino logger (file output, used by the TUI)
├── types.ts # shared TypeScript types
├── event-logger.ts # persists raw events to disk + optional webhook
├── history.ts # loads message history from logs on startup
├── message-store.ts # persists raw messages for poll decryption (TUI-only)
├── group-cache.ts # caches group metadata to disk
├── poll-decrypt.ts # AES-256-GCM poll vote decryption
├── retention.ts # periodic cleanup of old raw logs
├── collector/
│ ├── core.ts # collector core (connection, auth, routing) — shared by TUI + headless
│ ├── headless-config.ts # environment resolution for headless mode (fail-fast validation)
│ ├── shutdown.ts # graceful shutdown handler (SIGTERM/SIGINT + force-exit timeout)
│ ├── outbox.ts # SQLite event queue
│ ├── event-router.ts # routes events to the collector outbox
│ ├── webhook-sender.ts # sends queued events to the webhook
│ ├── heartbeat.ts # periodic health signal
│ ├── extractors.ts # event data extraction helpers
│ ├── helpers.ts # collector utility functions
│ └── types.ts # collector-specific types
├── hooks/
│ └── use-socket.ts # React hook wrapping the collector core (TUI-only: storeMessage, parseContent, dedup)
└── components/
├── header.tsx # status bar with tab navigation
├── chat-list.tsx # sidebar with groups/DMs
├── message-feed.tsx # message display with poll results
├── input-bar.tsx # text input with commands
├── qr-view.tsx # QR code renderer
├── stats-view.tsx # statistics dashboard
└── debug-view.tsx # real-time event log
scripts/
└── extract.py # payload extraction with poll decryption
- Runtime: Node.js 24+ / tsx
- WhatsApp: @whiskeysockets/baileys 7.0
- TUI: Ink 7 + React 19 + @inkjs/ui
- Build: tsup + TypeScript 6
- Logging: pino
- Extraction: Python 3
ISC