wpp-tui
v1.4.1
Published
WhatsApp TUI client — terminal interface for WhatsApp
Maintainers
Readme
wpp-tui
WhatsApp TUI client — terminal-based WhatsApp with chat, archive, rename, poll decryption, and event logging.
Built with Baileys (WhatsApp Web reverse-engineered protocol) + Ink (React for terminal).
Package
npm install -g wpp-tui # global CLI
npx wpp-tui # or run directlyOr clone the repo:
git clone https://github.com/jaweed3/wpp-tui
cd wpp-tui
pnpm install
pnpm devScan the QR code with WhatsApp > Settings > Linked Devices. On subsequent runs it reconnects automatically.
Screenshots
┌─ WA Bot | [1:Chat] 2:Stats 3:Debug 4:Status ── ● me ─┐
├─ CHATS ──────────────────────┐┌─ MESSAGES — General ───────┐
│ > # General (343) ││ 18:36 me eae ✓✓│
│ # He4rt Delas 💕 (52) ││ 18:37 Clinton oi │
│ # Vagas (343) ││ 18:38 Bruna 🖼️ sticker│
│ @ danielhe4rt ││ 18:38 me 📊 PHP │
│ ││ ■ Sim!! (3) ■□ Oke(5)│
│ ││ 18:39 Mina 🗳️ → Sim 👍│
└──────────────────────────────┘└──────────────────────────┘
┌─ f/:search i:type jk:nav hl:tab 1-4:tabs T:theme ──┐Features
TUI (Ink + React)
- 4 tabs: Chat, Status, Stats, Debug — switch via
1-4,Tab/Shift+Tab,h/l, or/chatetc. - Lazygit-style navigation: vim keys
j/kup/down,h/ltab switch, arrows everywhere - Chat tab: sidebar with groups (
#) and DMs (@), message feed with auto-scroll - Status tab: view incoming WhatsApp status updates (photos, text)
- Stats tab: top senders bar chart, message type distribution, group activity
- Debug tab: real-time event stream with scroll (
j/ktoo), connection info, store counters - Chat search:
/orfto open search bar — type to filter contacts,Enterto select - Input mode:
ito type messages,Escto exit — prevents accidental navigation - Message actions:
rto reply,dto delete selected message - Emoji reactions inline: reactions from other users appear next to the message
- Typing indicators:
typing...shown in the input bar when someone is typing - Theme system:
Shift+Tor/themeto cycle themes (default + dark/tokyonight) - QR code rendered inline in the terminal
- Message history loaded from logs on startup — history persists across restarts
- Group metadata fetched via WhatsApp API and cached to disk
- Poll decryption: votes decrypted with AES-256-GCM + HMAC-SHA256, shown per-user
- Message status indicators: ◌ pending, ✓ sent, ✓✓ delivered, ✓✓✓ blue read
- Optimistic send: messages appear instantly; deduplicated when server confirms
- Contact names from phonebook (
contacts.update), fallback to WhatsApp pushName, fallback to JID - Rename contacts from TUI (
/renameor/rn) — synced to WhatsApp server and persisted locally; applies to all messages - Archive/unarchive chats (
/archive//unarchive),Ctrl+Ktoggle archived view
Event Logging
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 |
Data Flow
WhatsApp servers
↓ (WebSocket via Baileys)
use-socket.ts hook ← manages connection, auth, reconnect
↓ emits events
├→ event-logger.ts → logs/<event>/<date>.json (raw persistence)
├→ message-store.ts → logs/message-store/<id>.json (for poll decryption)
├→ React state → messages[], chats[], pollResults{}, debugEvents[]
└→ group-cache.ts → logs/group-metadata.json (fetched once via API)
React state
↓
app.tsx (tab router + keyboard)
├→ Chat tab: chat-list.tsx + message-feed.tsx
├→ Status tab: status-view.tsx
├→ Stats tab: stats-view.tsx (computed from messages[])
└→ Debug tab: debug-view.tsx (from debugEvents[])Payload Extraction (make extract-payload)
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 |
Webhook Proxy
Optionally forward all events to an external webhook:
WEBHOOK_URL=https://your-webhook.example.com pnpm devModo headless
Para 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.
Pré-provisionar a sessão (sem QR)
O headless não exibe QR nem faz pairing-code (ver
docs/adr/0002). Ele exige uma
sessão já pareada:
- Pareie uma vez no Modo TUI (
pnpm dev) lendo o QR no celular. - Copie a pasta
baileys_auth_info/para oWorkingDirectorydo server:scp -r ./baileys_auth_info usuario@server:/opt/wpp-tui/baileys_auth_info
Se 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; pré-provisione baileys_auth_info) e sai com
código ≠ 0 — em vez de ficar gerando QRs num loop. Um loggedOut no server
também derruba o coletor: é preciso re-provisionar a sessão manualmente.
Variáveis obrigatórias
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.
Logs (JSON na stdout)
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.
Deploy via systemd
Há uma unit pronta em deploy/whatsapp-collector.service
(com comentários explicando cada diretiva e o passo de pré-provisionamento):
sudo cp deploy/whatsapp-collector.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now whatsapp-collector
# Ler os logs (a saída JSON cai no journald):
journalctl -u whatsapp-collector -fO sinal de saúde é a linha de heartbeat dos logs (não há endpoint HTTP
/health nesta versão).
Keyboard Shortcuts
| Key / Command | Action |
|---|---|
| Tab / l / → | Next tab |
| Shift+Tab / h / ← | Previous tab |
| 1/2/3/4 | Jump to Chat/Stats/Debug/Status tab |
| j / ↓ | Navigate down (chats, messages, debug scroll) |
| k / ↑ | Navigate up (chats, messages, debug scroll) |
| Enter | Open selected chat / Open full message / Go back |
| Esc | Cancel search / Exit input mode / Back to chat list |
| / or f | Search/filter chats by name (Chat tab) |
| i | Enter message-typing mode (input mode) |
| r | Reply to last/selected message |
| d | Delete selected message |
| T | Cycle theme (default → dark → default) |
| Ctrl+K | Toggle archived chats view |
| Ctrl+Q | Quit |
| /help | Show available commands |
| /theme or /t | Cycle theme or /theme dark for specific theme |
| /rename <name> or /rn <name> | Rename selected contact (synced to server) |
| /archive or /a | Archive selected chat |
| /unarchive or /ua | Unarchive selected chat |
| /arch | Toggle archived chats view |
| /delete or /d | Delete selected message |
| /chat through /debug | Switch to specific tab |
| /quit or /q | Quit |
| /s <query> | Filter chats by name |
Scripts
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 logsProject Structure
src/
├── index.tsx # entry point — routes to TUI or headless (dynamic import)
├── app.tsx # tab system, keyboard handling, layout (Chat/Status/Stats/Debug)
├── theme.tsx # theme system (ThemeProvider, useTheme, default + dark themes)
├── message-utils.ts # shared parseContent, formatContent, trunc (single source for message parsing)
├── 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
├── commands/ # slash command system (registry, chat commands, navigation commands)
│ ├── registry.ts
│ ├── types.ts
│ ├── chat-commands.ts
│ ├── navigation-commands.ts
│ └── system-commands.ts
├── 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
├── persist-archive.ts # persists archived chat state to disk
├── persist-renames.ts # persists contact renames 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 and status icons
├── input-bar.tsx # text input with commands
├── status-view.tsx # incoming WhatsApp status updates
├── 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 decryptionTech Stack
- 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
Links
- npm: wpp-tui
- GitHub: jaweed3/wpp-tui
- License: GPL-3.0
Credits
Originally based on he4rt/wpp-tui by He4rt Developers. The original project was released under the ISC License. This fork contains significant modifications and additional features, released under GPL-3.0.
License
GPL-3.0
