waha-openclaw-channel 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +589 -0
- package/config-example.json +48 -0
- package/index.ts +17 -0
- package/package.json +54 -0
- package/src/accounts.ts +149 -0
- package/src/channel.ts +357 -0
- package/src/config-schema.ts +85 -0
- package/src/directory.ts +233 -0
- package/src/dm-filter.ts +145 -0
- package/src/inbound.ts +419 -0
- package/src/monitor.ts +1317 -0
- package/src/normalize.ts +26 -0
- package/src/presence.ts +176 -0
- package/src/runtime.ts +14 -0
- package/src/secret-input.ts +19 -0
- package/src/send.ts +302 -0
- package/src/signature.ts +29 -0
- package/src/types.ts +128 -0
package/README.md
ADDED
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
# OpenClaw WAHA Plugin — Developer Reference
|
|
2
|
+
|
|
3
|
+
**Plugin ID:** `waha`
|
|
4
|
+
**Platform:** WhatsApp (via WAHA HTTP API)
|
|
5
|
+
**Last updated:** 2026-03-08
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Install from GitHub
|
|
11
|
+
npm install github:omernesh/openclaw-waha-plugin
|
|
12
|
+
|
|
13
|
+
# Or clone and install locally
|
|
14
|
+
git clone https://github.com/omernesh/openclaw-waha-plugin.git
|
|
15
|
+
cd openclaw-waha-plugin
|
|
16
|
+
npm install
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The plugin requires `better-sqlite3` (native addon) — ensure you have a C++ build toolchain available (`node-gyp`).
|
|
20
|
+
|
|
21
|
+
After installation, add the WAHA channel configuration to your `openclaw.json` under `channels.waha`. See the [Configuration Reference](#7-configuration-reference) and `config-example.json` for a full example.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 1. Overview
|
|
26
|
+
|
|
27
|
+
This plugin bridges OpenClaw AI agents to WhatsApp through the WAHA (WhatsApp HTTP API) server. It enables your "OpenClaw assistant" bot to receive WhatsApp messages via webhook, route them through OpenClaw's AI agent pipeline, and deliver replies back through WAHA — including text responses and TTS-generated voice notes.
|
|
28
|
+
|
|
29
|
+
The plugin operates as a channel adapter within the OpenClaw plugin-sdk framework. It:
|
|
30
|
+
|
|
31
|
+
- Runs an HTTP webhook server to receive inbound WAHA events
|
|
32
|
+
- Applies access control (DM policy, group allowlists with both `@c.us` and `@lid` JID formats)
|
|
33
|
+
- Simulates human-like presence (read receipts, typing indicators with random pauses) before replying
|
|
34
|
+
- Delivers AI-generated text and voice replies through WAHA's REST API
|
|
35
|
+
- Enforces session guardrails (only the bot session can send outbound messages)
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 2. File Listing
|
|
40
|
+
|
|
41
|
+
| File | Lines | Description |
|
|
42
|
+
|------|-------|-------------|
|
|
43
|
+
| `channel.ts` | ~340 | Channel plugin registration and lifecycle. Exports the `ChannelPlugin` definition with metadata, capabilities (reactions, media, markdown), account resolution, and outbound delivery adapter. Wires up the webhook monitor, inbound handler, and send functions. |
|
|
44
|
+
| `inbound.ts` | ~420 | Inbound message handler. Receives parsed `WahaInboundMessage` from the monitor, applies DM/group access control, runs the DM keyword filter, tracks contacts in the SQLite directory, enforces per-DM settings (listen-only, mention-only), starts the human presence simulation, dispatches the message to the AI agent, and delivers the reply. |
|
|
45
|
+
| `dm-filter.ts` | ~145 | DM keyword filter. `DmFilter` class with regex caching, god mode bypass for super-users, and stats tracking (dropped/allowed/tokensEstimatedSaved). Fail-open: any error allows messages through. |
|
|
46
|
+
| `send.ts` | ~250 | WAHA REST API wrappers. Provides `sendWahaText()`, `sendWahaMediaBatch()`, `sendWahaReaction()`, `sendWahaPresence()`, `sendWahaSeen()`, and the internal `callWahaApi()` HTTP client. Includes `assertAllowedSession()` guardrail, `buildFilePayload()` for base64 encoding of local TTS files, and `resolveMime()` for MIME type detection with file-extension fallback. |
|
|
47
|
+
| `presence.ts` | ~170 | Human mimicry presence system. Implements the 4-phase presence simulation: seen, read delay, typing with random pauses (flicker), and reply-length padding. Exports `startHumanPresence()` which returns a `PresenceController` with `finishTyping()` and `cancelTyping()` methods. |
|
|
48
|
+
| `types.ts` | ~130 | TypeScript type definitions. Defines `CoreConfig`, `WahaChannelConfig`, `WahaAccountConfig`, `PresenceConfig`, `DmFilterConfig`, `WahaWebhookEnvelope`, `WahaInboundMessage`, `WahaReactionEvent`, and `WahaWebhookConfig`. |
|
|
49
|
+
| `config-schema.ts` | ~86 | Zod validation schema for the `channels.waha` config section. Validates all account-level and channel-level settings including secret inputs, policies, presence parameters, DM filter config, and markdown options. |
|
|
50
|
+
| `accounts.ts` | ~140 | Multi-account resolution. Resolves which WAHA account (baseUrl, apiKey, session) to use for a given operation. Supports a default account plus named sub-accounts under `channels.waha.accounts`. Handles API key resolution from env vars, files, or direct strings. |
|
|
51
|
+
| `normalize.ts` | ~30 | JID normalization utilities. `normalizeWahaMessagingTarget()` strips `waha:`, `whatsapp:`, `chat:` prefixes. `normalizeWahaAllowEntry()` lowercases for allowlist comparison. `resolveWahaAllowlistMatch()` checks if a sender JID is in the allowlist (supports `*` wildcard). |
|
|
52
|
+
| `directory.ts` | ~120 | SQLite-backed contact directory. `DirectoryDb` class using `better-sqlite3` stores contacts (JID, display name, message count, first/last seen) and per-DM settings (mode, mention-only, custom keywords, can-initiate). Module-level singleton `Map<accountId, DirectoryDb>`. Database at `~/.openclaw/data/waha-directory-{accountId}.db`. |
|
|
53
|
+
| `monitor.ts` | ~1500 | Webhook HTTP server, health monitoring, and multi-tab admin panel SPA. Starts an HTTP server on the configured port (default 8050). Handles `/healthz`, `/admin` (4-tab HTML SPA), `/api/admin/stats`, `/api/admin/config` (GET/POST), `/api/admin/directory` (REST API), and the main webhook path. Validates HMAC signatures and dispatches inbound events. |
|
|
54
|
+
| `runtime.ts` | ~15 | Runtime singleton access. `setWahaRuntime()` / `getWahaRuntime()` store and retrieve the OpenClaw `PluginRuntime` instance for use across modules. |
|
|
55
|
+
| `signature.ts` | ~30 | HMAC webhook verification. `verifyWahaWebhookHmac()` validates the `X-Webhook-Hmac` header using SHA-512, accepting hex or base64 signature formats. Uses `crypto.timingSafeEqual()` for constant-time comparison. |
|
|
56
|
+
| `secret-input.ts` | ~15 | Secret field schema. Re-exports OpenClaw SDK secret input utilities and provides `buildSecretInputSchema()` which accepts either a plain string or a `{ source, provider, id }` object for env/file/exec-based secret resolution. |
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 3. DM Keyword Filter
|
|
61
|
+
|
|
62
|
+
The DM keyword filter (`dm-filter.ts`) gates inbound DMs by keyword BEFORE they reach the AI agent. Only messages matching at least one pattern are processed; others are silently dropped. This prevents the AI from consuming tokens on irrelevant or unsolicited messages.
|
|
63
|
+
|
|
64
|
+
### Config (under `channels.waha`)
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
"dmFilter": {
|
|
68
|
+
"enabled": true,
|
|
69
|
+
"mentionPatterns": ["yourbot", "help", "hello", "bot", "ai"],
|
|
70
|
+
"godModeBypass": true,
|
|
71
|
+
"godModeSuperUsers": [
|
|
72
|
+
{ "identifier": "15551234567", "platform": "whatsapp", "passwordRequired": false }
|
|
73
|
+
],
|
|
74
|
+
"tokenEstimate": 2500
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
| Field | Type | Default | Description |
|
|
79
|
+
|-------|------|---------|-------------|
|
|
80
|
+
| `enabled` | `boolean` | `false` | Enable/disable the filter |
|
|
81
|
+
| `mentionPatterns` | `string[]` | `[]` | Regex patterns (case-insensitive). Message must match at least one. Empty list means no restriction. |
|
|
82
|
+
| `godModeBypass` | `boolean` | `true` | Super-users bypass the filter entirely |
|
|
83
|
+
| `godModeSuperUsers` | `array` | `[]` | List of users who bypass the filter (phone in E.164 or JID format) |
|
|
84
|
+
| `tokenEstimate` | `number` | `2500` | Estimated tokens saved per dropped message (used for stats display) |
|
|
85
|
+
|
|
86
|
+
### Behavior
|
|
87
|
+
|
|
88
|
+
- **Filter disabled**: All messages pass through (stats count as allowed)
|
|
89
|
+
- **No patterns**: All messages pass through (no restriction configured)
|
|
90
|
+
- **God mode**: Super-users bypass pattern matching entirely. Israeli phone normalization handles 05X/972X/+972X and JID suffixes (`@c.us`, `@lid`, `@s.whatsapp.net`)
|
|
91
|
+
- **Pattern match**: Message is allowed if ANY pattern matches (case-insensitive regex)
|
|
92
|
+
- **No match**: Message is silently dropped — no reply, no error, no pairing message
|
|
93
|
+
- **Fail-open**: Any error in the filter allows the message through (avoids outages from filter bugs)
|
|
94
|
+
|
|
95
|
+
### Regex caching
|
|
96
|
+
|
|
97
|
+
Patterns are compiled to `RegExp` objects once and cached. The cache key is the joined pattern array. If config updates (e.g. via `updateConfig()`), the cache is invalidated and rebuilt on next check.
|
|
98
|
+
|
|
99
|
+
### Stats tracking
|
|
100
|
+
|
|
101
|
+
The filter maintains runtime counters per account:
|
|
102
|
+
- `dropped`: messages silently dropped
|
|
103
|
+
- `allowed`: messages passed through
|
|
104
|
+
- `tokensEstimatedSaved`: `dropped * tokenEstimate` — rough estimate of AI tokens saved
|
|
105
|
+
|
|
106
|
+
Recent events (last 50) are stored in memory with timestamp, pass/fail, reason, and text preview.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## 4. Admin Panel
|
|
111
|
+
|
|
112
|
+
A browser-based multi-tab SPA admin panel is served at `http://<host>:<webhookPort>/admin` (default port 8050).
|
|
113
|
+
|
|
114
|
+
### Access
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
http://your-server-ip:8050/admin
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Tabs
|
|
121
|
+
|
|
122
|
+
**Dashboard** — Real-time overview of plugin status:
|
|
123
|
+
- DM Filter stats (allowed/dropped/tokens saved, patterns, recent events)
|
|
124
|
+
- Presence System configuration display
|
|
125
|
+
- Access Control policies and allowlists
|
|
126
|
+
- Session info (session name, baseUrl, port, uptime)
|
|
127
|
+
- Auto-refresh every 30 seconds
|
|
128
|
+
|
|
129
|
+
**Settings** — Edit all plugin configuration fields via browser:
|
|
130
|
+
- 6 collapsible sections: Connection, Access Control, DM Keyword Filter, Presence, Markdown, Features
|
|
131
|
+
- Every field has a tooltip (?) explaining what it does and sensible defaults
|
|
132
|
+
- "Save Settings" persists changes directly to `openclaw.json`
|
|
133
|
+
- Visual success/error toast notifications
|
|
134
|
+
|
|
135
|
+
**Directory** — SQLite-backed contact database:
|
|
136
|
+
- Searchable list of all contacts who have messaged the bot
|
|
137
|
+
- Avatar placeholders with initials, display name, last message time, message count
|
|
138
|
+
- Per-DM settings panel per contact (mode, mention-only, custom keywords, can-initiate)
|
|
139
|
+
- Pagination with "Load More" for large contact lists
|
|
140
|
+
|
|
141
|
+
**Docs** — Built-in help documentation:
|
|
142
|
+
- Getting Started, DM Keyword Filter, Per-DM Settings, Presence System, Access Control, Troubleshooting
|
|
143
|
+
- Collapsible sections with clean typography
|
|
144
|
+
|
|
145
|
+
**Footer** — "Created with love by omer nesher" with link to GitHub repository
|
|
146
|
+
|
|
147
|
+
### Admin API
|
|
148
|
+
|
|
149
|
+
| Method | Endpoint | Description |
|
|
150
|
+
|--------|----------|-------------|
|
|
151
|
+
| `GET` | `/api/admin/stats` | Dashboard stats (DM filter, presence, access, session) |
|
|
152
|
+
| `GET` | `/api/admin/config` | Full editable config for Settings tab |
|
|
153
|
+
| `POST` | `/api/admin/config` | Write config to `openclaw.json` (deep merge) |
|
|
154
|
+
| `GET` | `/api/admin/directory` | List contacts with DM settings (`?search=&limit=&offset=`) |
|
|
155
|
+
| `GET` | `/api/admin/directory/:jid` | Single contact detail |
|
|
156
|
+
| `PUT` | `/api/admin/directory/:jid/settings` | Update per-DM settings for a contact |
|
|
157
|
+
|
|
158
|
+
### Stats API Example
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
curl http://your-server-ip:8050/api/admin/stats
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Returns JSON:
|
|
165
|
+
```json
|
|
166
|
+
{
|
|
167
|
+
"dmFilter": {
|
|
168
|
+
"enabled": true,
|
|
169
|
+
"patterns": ["yourbot", "help"],
|
|
170
|
+
"stats": { "dropped": 5, "allowed": 12, "tokensEstimatedSaved": 12500 },
|
|
171
|
+
"recentEvents": [
|
|
172
|
+
{ "ts": 1772902231754, "pass": false, "reason": "no_keyword_match", "preview": "hello world" }
|
|
173
|
+
]
|
|
174
|
+
},
|
|
175
|
+
"presence": { "enabled": true, "wpm": 42, ... },
|
|
176
|
+
"access": { "dmPolicy": "pairing", "allowFrom": [...], ... },
|
|
177
|
+
"session": "your_session_name",
|
|
178
|
+
"webhookPort": 8050,
|
|
179
|
+
"serverTime": "2026-03-07T18:50:00.000Z"
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
### Implementation notes
|
|
183
|
+
|
|
184
|
+
- Zero build tooling: the entire admin panel is an embedded HTML/CSS/JS template string in `monitor.ts`
|
|
185
|
+
- 4-tab SPA with URL hash persistence (#dashboard, #settings, #directory, #docs)
|
|
186
|
+
- Admin routes are added BEFORE the POST-only webhook guard in the HTTP server handler
|
|
187
|
+
- No authentication on admin routes (restrict via firewall if needed)
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## 5. Directory Module & Per-DM Settings
|
|
192
|
+
|
|
193
|
+
### Contact Directory
|
|
194
|
+
|
|
195
|
+
The directory module (`directory.ts`) maintains a SQLite database of all contacts who have messaged the bot. Contacts are automatically tracked by the inbound handler — no manual setup required.
|
|
196
|
+
|
|
197
|
+
**Database location:** `~/.openclaw/data/waha-directory-{accountId}.db`
|
|
198
|
+
|
|
199
|
+
**SQLite schema:**
|
|
200
|
+
|
|
201
|
+
```sql
|
|
202
|
+
CREATE TABLE contacts (
|
|
203
|
+
jid TEXT PRIMARY KEY,
|
|
204
|
+
display_name TEXT,
|
|
205
|
+
first_seen_at INTEGER NOT NULL,
|
|
206
|
+
last_message_at INTEGER NOT NULL,
|
|
207
|
+
message_count INTEGER DEFAULT 1,
|
|
208
|
+
is_group INTEGER DEFAULT 0
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
CREATE TABLE dm_settings (
|
|
212
|
+
jid TEXT PRIMARY KEY,
|
|
213
|
+
mode TEXT DEFAULT 'active' CHECK(mode IN ('active','listen_only')),
|
|
214
|
+
mention_only INTEGER DEFAULT 0,
|
|
215
|
+
custom_keywords TEXT DEFAULT '',
|
|
216
|
+
can_initiate INTEGER DEFAULT 1,
|
|
217
|
+
updated_at INTEGER NOT NULL,
|
|
218
|
+
FOREIGN KEY (jid) REFERENCES contacts(jid)
|
|
219
|
+
);
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Per-DM Settings
|
|
223
|
+
|
|
224
|
+
Each contact can have individual settings that override global behavior:
|
|
225
|
+
|
|
226
|
+
| Setting | Type | Default | Description |
|
|
227
|
+
|---------|------|---------|-------------|
|
|
228
|
+
| `mode` | `'active' \| 'listen_only'` | `active` | `active`: bot responds normally. `listen_only`: messages are tracked but bot does not respond. |
|
|
229
|
+
| `mentionOnly` | `boolean` | `false` | When true, bot only responds if a mention pattern matches (uses DM filter patterns). |
|
|
230
|
+
| `customKeywords` | `string` | `''` | Comma-separated additional keywords that trigger bot response for this contact. |
|
|
231
|
+
| `canInitiate` | `boolean` | `true` | Whether the bot can proactively send messages to this contact. |
|
|
232
|
+
|
|
233
|
+
Per-DM settings are enforced by `inbound.ts` after the global DM filter check. They are managed via the Admin Panel's Directory tab or the REST API.
|
|
234
|
+
|
|
235
|
+
### Singleton Pattern
|
|
236
|
+
|
|
237
|
+
`DirectoryDb` instances are managed as a module-level `Map<string, DirectoryDb>` keyed by `accountId`, following the same pattern as `DmFilter`. Access via `getDirectoryDb(accountId)`.
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## 6. Human Mimicry Presence System
|
|
242
|
+
|
|
243
|
+
### Problem
|
|
244
|
+
|
|
245
|
+
A bot that instantly shows "typing..." and replies in 200ms is obviously non-human. WhatsApp users notice deterministic timing patterns, which degrades the conversational experience.
|
|
246
|
+
|
|
247
|
+
### Solution
|
|
248
|
+
|
|
249
|
+
The presence system simulates a 4-phase human interaction pattern with randomized timing at every step:
|
|
250
|
+
|
|
251
|
+
```
|
|
252
|
+
Phase 1: SEEN Phase 2: READ Phase 3: TYPING Phase 4: REPLY
|
|
253
|
+
(with pauses)
|
|
254
|
+
[msg arrives] --> [blue ticks] --> [typing... ···] --> [send message]
|
|
255
|
+
| | |
|
|
256
|
+
v v v
|
|
257
|
+
sendSeen() sleep(readDelay) typing ON/OFF flicker
|
|
258
|
+
(random pauses)
|
|
259
|
+
+ padding if AI was fast
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Flow Detail
|
|
263
|
+
|
|
264
|
+
1. **Seen** (`sendSeen`): If enabled, immediately marks the message as read (blue ticks).
|
|
265
|
+
2. **Read Delay** (`readDelayMs`): Pauses to simulate the time a human takes to read the incoming message. Duration scales with message length (`msPerReadChar * charCount`), clamped to `readDelayMs` bounds, then jittered.
|
|
266
|
+
3. **Typing with Flicker**: Sets typing indicator ON, then enters a loop where it randomly pauses typing (OFF for `pauseDurationMs`, then ON again) at `pauseIntervalMs` intervals with `pauseChance` probability. This continues while the AI generates its response.
|
|
267
|
+
4. **Reply-Length Padding** (`finishTyping`): After the AI responds, calculates how long a human would take to type the reply at `wpm` words-per-minute. If the AI was faster than that, pads with additional typing flicker. If the AI was slower, no padding is needed.
|
|
268
|
+
|
|
269
|
+
### Timing Parameters
|
|
270
|
+
|
|
271
|
+
| Parameter | Type | Default | Description |
|
|
272
|
+
|-----------|------|---------|-------------|
|
|
273
|
+
| `enabled` | `boolean` | `true` | Master switch for the entire presence system |
|
|
274
|
+
| `sendSeen` | `boolean` | `true` | Send read receipt (blue ticks) before typing |
|
|
275
|
+
| `wpm` | `number` | `42` | Simulated typing speed in words-per-minute |
|
|
276
|
+
| `readDelayMs` | `[min, max]` | `[500, 4000]` | Clamp range for read delay (ms) |
|
|
277
|
+
| `msPerReadChar` | `number` | `30` | Base read time per character of incoming message |
|
|
278
|
+
| `typingDurationMs` | `[min, max]` | `[1500, 15000]` | Clamp range for total typing duration (ms) |
|
|
279
|
+
| `pauseChance` | `number` | `0.3` | Probability (0-1) of pausing typing each interval |
|
|
280
|
+
| `pauseDurationMs` | `[min, max]` | `[500, 2000]` | Duration range for each typing pause (ms) |
|
|
281
|
+
| `pauseIntervalMs` | `[min, max]` | `[2000, 5000]` | Interval range between pause-chance checks (ms) |
|
|
282
|
+
| `jitter` | `[min, max]` | `[0.7, 1.3]` | Multiplier range applied to all computed durations |
|
|
283
|
+
|
|
284
|
+
### Jitter Mechanics
|
|
285
|
+
|
|
286
|
+
Every computed duration is multiplied by `rand(jitter[0], jitter[1])` before use. With the default `[0.7, 1.3]`, a base delay of 2000ms becomes anywhere from 1400ms to 2600ms. This prevents timing fingerprinting.
|
|
287
|
+
|
|
288
|
+
### AI Fast vs Slow
|
|
289
|
+
|
|
290
|
+
- **AI responds in 2s, human typing estimate is 8s**: Presence pads with 6s of additional typing flicker before sending.
|
|
291
|
+
- **AI responds in 12s, human typing estimate is 8s**: No padding needed. Reply sends immediately after AI finishes (typing indicator was already running during generation).
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## 7. Configuration Reference
|
|
296
|
+
|
|
297
|
+
All configuration lives in `~/.openclaw/openclaw.json` AND `~/.openclaw/workspace/openclaw.json` under `channels.waha`. The gateway uses the **workspace config** (set by `OPENCLAW_CONFIG_PATH`), so changes must be applied there.
|
|
298
|
+
|
|
299
|
+
### Full Config Structure
|
|
300
|
+
|
|
301
|
+
```jsonc
|
|
302
|
+
{
|
|
303
|
+
"channels": {
|
|
304
|
+
"waha": {
|
|
305
|
+
// --- Connection ---
|
|
306
|
+
"enabled": true,
|
|
307
|
+
"baseUrl": "http://127.0.0.1:3004", // WAHA server URL
|
|
308
|
+
"apiKey": "your-api-key-here", // WHATSAPP_API_KEY (NOT WAHA_API_KEY)
|
|
309
|
+
"session": "your_session_name", // WAHA session name
|
|
310
|
+
|
|
311
|
+
// --- Webhook Server ---
|
|
312
|
+
"webhookHost": "0.0.0.0", // Bind address (default: 0.0.0.0)
|
|
313
|
+
"webhookPort": 8050, // Webhook listener port (default: 8050)
|
|
314
|
+
"webhookPath": "/webhook/waha", // Webhook URL path
|
|
315
|
+
"webhookHmacKey": "your-hmac-key-here", // HMAC-SHA512 key for signature verification
|
|
316
|
+
|
|
317
|
+
// --- Access Control ---
|
|
318
|
+
"dmPolicy": "allowlist", // "pairing" | "open" | "closed" | "allowlist"
|
|
319
|
+
"groupPolicy": "allowlist", // "allowlist" | "open" | "closed"
|
|
320
|
+
"allowFrom": [ // DM senders allowed (when dmPolicy=allowlist)
|
|
321
|
+
"15551234567@c.us",
|
|
322
|
+
"123456789012345@lid"
|
|
323
|
+
],
|
|
324
|
+
"groupAllowFrom": [ // Group message senders allowed
|
|
325
|
+
"15551234567@c.us", // @c.us JID
|
|
326
|
+
"123456789012345@lid" // @lid JID (NOWEB engine sends these!)
|
|
327
|
+
],
|
|
328
|
+
|
|
329
|
+
// --- Presence (Human Mimicry) ---
|
|
330
|
+
"presence": {
|
|
331
|
+
"enabled": true,
|
|
332
|
+
"sendSeen": true,
|
|
333
|
+
"wpm": 42,
|
|
334
|
+
"readDelayMs": [500, 4000],
|
|
335
|
+
"msPerReadChar": 30,
|
|
336
|
+
"typingDurationMs": [1500, 15000],
|
|
337
|
+
"pauseChance": 0.3,
|
|
338
|
+
"pauseDurationMs": [500, 2000],
|
|
339
|
+
"pauseIntervalMs": [2000, 5000],
|
|
340
|
+
"jitter": [0.7, 1.3]
|
|
341
|
+
},
|
|
342
|
+
|
|
343
|
+
// --- Optional Features ---
|
|
344
|
+
"actions": {
|
|
345
|
+
"reactions": true // Enable emoji reactions
|
|
346
|
+
},
|
|
347
|
+
"markdown": {
|
|
348
|
+
"enabled": true,
|
|
349
|
+
"tables": "auto" // "auto" | "markdown" | "text"
|
|
350
|
+
},
|
|
351
|
+
"replyPrefix": {
|
|
352
|
+
"enabled": false
|
|
353
|
+
},
|
|
354
|
+
"blockStreaming": false,
|
|
355
|
+
|
|
356
|
+
// --- Multi-Account (optional) ---
|
|
357
|
+
"accounts": {
|
|
358
|
+
"secondary": {
|
|
359
|
+
"baseUrl": "http://other-waha:3004",
|
|
360
|
+
"apiKey": "...",
|
|
361
|
+
"session": "other_session"
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
"defaultAccount": "default"
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### Access Control Notes
|
|
371
|
+
|
|
372
|
+
- `dmPolicy: "allowlist"` + `allowFrom` restricts DMs to listed JIDs only
|
|
373
|
+
- `groupPolicy: "allowlist"` + `groupAllowFrom` restricts group responses to messages from listed sender JIDs
|
|
374
|
+
- `groupAllowFrom` filters by **sender JID** (participant), NOT by group JID
|
|
375
|
+
- Use `"*"` to allow all senders (dangerous in production)
|
|
376
|
+
- **CRITICAL**: WAHA NOWEB engine sends sender JIDs as `@lid`, not `@c.us`. You MUST include BOTH formats for each allowed user.
|
|
377
|
+
|
|
378
|
+
### Finding a User's LID
|
|
379
|
+
|
|
380
|
+
```bash
|
|
381
|
+
docker exec -i postgres-waha psql -U admin -d waha_noweb_your_session_name \
|
|
382
|
+
-c "SELECT id, pn FROM lid_map WHERE pn LIKE '%PHONE_NUMBER%'"
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
## 8. Installation / Reinstallation
|
|
388
|
+
|
|
389
|
+
### File Locations
|
|
390
|
+
|
|
391
|
+
The plugin source exists in TWO locations that must always be kept in sync:
|
|
392
|
+
|
|
393
|
+
| Location | Purpose |
|
|
394
|
+
|----------|---------|
|
|
395
|
+
| `~/.openclaw/extensions/waha/src/` | **Runtime** — what OpenClaw actually loads |
|
|
396
|
+
| `~/.openclaw/workspace/skills/waha-openclaw-channel/src/` | **Development** — workspace copy |
|
|
397
|
+
|
|
398
|
+
The main config file is at `~/.openclaw/openclaw.json` under `channels.waha`.
|
|
399
|
+
|
|
400
|
+
### Deploying Changes
|
|
401
|
+
|
|
402
|
+
After editing source files, deploy to BOTH locations and restart:
|
|
403
|
+
|
|
404
|
+
```bash
|
|
405
|
+
# 1. Copy files (if editing in workspace)
|
|
406
|
+
cp ~/.openclaw/workspace/skills/waha-openclaw-channel/src/*.ts \
|
|
407
|
+
~/.openclaw/extensions/waha/src/
|
|
408
|
+
|
|
409
|
+
# 2. Verify both copies match
|
|
410
|
+
md5sum ~/.openclaw/extensions/waha/src/*.ts
|
|
411
|
+
md5sum ~/.openclaw/workspace/skills/waha-openclaw-channel/src/*.ts
|
|
412
|
+
|
|
413
|
+
# 3. Restart gateway (systemd auto-restarts on kill)
|
|
414
|
+
kill -9 $(pgrep -f "openclaw-gatewa") 2>/dev/null
|
|
415
|
+
|
|
416
|
+
# 4. Wait ~5 seconds, then verify it came back up
|
|
417
|
+
ss -tlnp | grep 18789
|
|
418
|
+
curl -s http://127.0.0.1:8050/healthz
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
### Remote Deployment (from Windows dev machine)
|
|
422
|
+
|
|
423
|
+
Use base64 transfer to avoid shell escaping issues with TypeScript `!==` operators:
|
|
424
|
+
|
|
425
|
+
```bash
|
|
426
|
+
# Encode locally
|
|
427
|
+
B64=$(base64 -w 0 /path/to/file.ts)
|
|
428
|
+
|
|
429
|
+
# Transfer to both locations
|
|
430
|
+
ssh user@your-server-ip "echo '$B64' | base64 -d > ~/.openclaw/extensions/waha/src/file.ts"
|
|
431
|
+
ssh user@your-server-ip "echo '$B64' | base64 -d > ~/.openclaw/workspace/skills/waha-openclaw-channel/src/file.ts"
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
---
|
|
435
|
+
|
|
436
|
+
## 9. Troubleshooting
|
|
437
|
+
|
|
438
|
+
### CRITICAL: Gateway Uses Workspace Config, Not ~/.openclaw/openclaw.json
|
|
439
|
+
|
|
440
|
+
The openclaw gateway service sets `OPENCLAW_CONFIG_PATH=~/.openclaw/workspace/openclaw.json` (visible in `/proc/<pid>/environ`). The gateway reads FROM and writes TO this file, NOT `~/.openclaw/openclaw.json`.
|
|
441
|
+
|
|
442
|
+
When WAHA is not starting (port 8050 not bound), verify the **workspace config** has the waha section:
|
|
443
|
+
|
|
444
|
+
```bash
|
|
445
|
+
python3 -c "import json; cfg=json.load(open('~/.openclaw/workspace/openclaw.json')); print(list(cfg.get('channels',{}).keys()))"
|
|
446
|
+
# Should show: ['telegram', 'waha']
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
To sync WAHA config from `~/.openclaw/openclaw.json` to workspace:
|
|
450
|
+
```bash
|
|
451
|
+
python3 << 'PYEOF'
|
|
452
|
+
import json, shutil
|
|
453
|
+
full = json.load(open('~/.openclaw/openclaw.json'))
|
|
454
|
+
ws = json.load(open('~/.openclaw/workspace/openclaw.json'))
|
|
455
|
+
ws.setdefault('channels', {})['waha'] = full['channels']['waha']
|
|
456
|
+
shutil.copy('~/.openclaw/workspace/openclaw.json', '~/.openclaw/workspace/openclaw.json.bak')
|
|
457
|
+
json.dump(ws, open('~/.openclaw/workspace/openclaw.json', 'w'), indent=2)
|
|
458
|
+
print('Done')
|
|
459
|
+
PYEOF
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
### WAHA API Key: Use WHATSAPP_API_KEY, Not WAHA_API_KEY
|
|
463
|
+
|
|
464
|
+
WAHA defines two keys in its `.env`. Only `WHATSAPP_API_KEY` authenticates API calls (returns 200). Using `WAHA_API_KEY` returns 401 on every request. The `channels.waha.apiKey` config value must use the correct key.
|
|
465
|
+
|
|
466
|
+
**Test which key works:**
|
|
467
|
+
```bash
|
|
468
|
+
curl -s -o /dev/null -w "%{http_code}" \
|
|
469
|
+
-H "X-Api-Key: YOUR_KEY_HERE" \
|
|
470
|
+
http://127.0.0.1:3004/api/sessions
|
|
471
|
+
# 200 = correct key, 401 = wrong key
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
### groupAllowFrom Needs BOTH @c.us AND @lid JIDs
|
|
475
|
+
|
|
476
|
+
WAHA's NOWEB engine sends group message sender JIDs as `@lid` (linked device ID), not `@c.us`. If you only list `@c.us` JIDs in `groupAllowFrom`, all group messages will be silently dropped.
|
|
477
|
+
|
|
478
|
+
**Fix:** Add both formats for each allowed user:
|
|
479
|
+
```json
|
|
480
|
+
"groupAllowFrom": ["15551234567@c.us", "123456789012345@lid"]
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
### Voice Files: Use /api/sendVoice, Not Base64 File Conversion
|
|
484
|
+
|
|
485
|
+
The `send.ts` module includes `buildFilePayload()` which automatically handles local TTS file paths by reading them as base64. Audio files with recognized MIME types (`audio/*`) are routed to WAHA's `/api/sendVoice` endpoint with `convert: true` to produce proper WhatsApp voice bubbles (PTT format).
|
|
486
|
+
|
|
487
|
+
If voice notes appear as document attachments instead of voice bubbles, check that:
|
|
488
|
+
1. `resolveMime()` correctly detects the audio MIME type
|
|
489
|
+
2. The WAHA endpoint is `/api/sendVoice` (not `/api/sendFile`)
|
|
490
|
+
3. The payload includes `"convert": true`
|
|
491
|
+
|
|
492
|
+
### TTS Local Paths: buildFilePayload() Handles Base64 Automatically
|
|
493
|
+
|
|
494
|
+
OpenClaw TTS generates voice files at `/tmp/openclaw/tts-*/voice-*.mp3`. WAHA cannot access local filesystem paths. The `buildFilePayload()` function in `send.ts` detects paths starting with `/` or `file://`, reads the file with `readFileSync`, converts to base64, and builds the correct WAHA payload format with `{ data, mimetype, filename }`.
|
|
495
|
+
|
|
496
|
+
### Plugin ID Mismatch Warning (Benign)
|
|
497
|
+
|
|
498
|
+
```
|
|
499
|
+
plugin id mismatch (config uses "waha-openclaw-channel", export uses "waha")
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
This warning appears because the config `plugins.entries` key is `waha-openclaw-channel` but the plugin exports `id: "waha"`. It is cosmetic only — the plugin loads and operates normally.
|
|
503
|
+
|
|
504
|
+
### Shell ! Escaping: Use Base64 Transfer for TypeScript Files Over SSH
|
|
505
|
+
|
|
506
|
+
SSH heredocs with `!` characters (in `!==`, `!response.ok`, etc.) trigger bash history expansion, which inserts backslashes into the file content and causes TypeScript parse errors. Always use the base64 transfer pattern (see Section 5) when deploying TypeScript files remotely.
|
|
507
|
+
|
|
508
|
+
### Gateway Not Responding After Restart
|
|
509
|
+
|
|
510
|
+
1. Check if the process is running: `pgrep -af "openclaw-gateway"`
|
|
511
|
+
2. Check if port 18789 is bound: `ss -tlnp | grep 18789`
|
|
512
|
+
3. Check webhook port: `ss -tlnp | grep 8050`
|
|
513
|
+
4. Check logs: `tail -100 /tmp/openclaw/openclaw-gateway.log`
|
|
514
|
+
5. If an old process holds the port, force kill: `kill -9 $(pgrep -f "openclaw-gatewa")`
|
|
515
|
+
6. Systemd auto-restarts the gateway — wait ~5 seconds after kill
|
|
516
|
+
|
|
517
|
+
---
|
|
518
|
+
|
|
519
|
+
## 10. Key Guardrails
|
|
520
|
+
|
|
521
|
+
### Session Blocking (`assertAllowedSession`)
|
|
522
|
+
|
|
523
|
+
The `send.ts` module enforces a hard guardrail that prevents the bot from sending messages as the owner:
|
|
524
|
+
|
|
525
|
+
```typescript
|
|
526
|
+
if (normalized === "owner" || normalized.endsWith("_owner")) {
|
|
527
|
+
throw new Error(`WAHA session '${normalized}' is explicitly blocked by guardrail`);
|
|
528
|
+
}
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
Only sessions matching or are allowed to send outbound messages. This prevents accidental or malicious use of the owner's personal WhatsApp session by the AI bot.
|
|
532
|
+
|
|
533
|
+
### HMAC Webhook Verification
|
|
534
|
+
|
|
535
|
+
All incoming webhooks are verified against the configured `webhookHmacKey` using SHA-512 HMAC. Requests without a valid `X-Webhook-Hmac` header receive HTTP 401. This prevents unauthorized parties from injecting fake messages into the bot pipeline.
|
|
536
|
+
|
|
537
|
+
### Access Control Enforcement
|
|
538
|
+
|
|
539
|
+
Messages are dropped silently (with logging) if:
|
|
540
|
+
- The sender JID is not in `allowFrom` (for DMs when `dmPolicy: "allowlist"`)
|
|
541
|
+
- The sender JID is not in `groupAllowFrom` (for group messages when `groupPolicy: "allowlist"`)
|
|
542
|
+
- The session in the webhook payload does not match the configured session
|
|
543
|
+
|
|
544
|
+
---
|
|
545
|
+
|
|
546
|
+
## Appendix: Architecture Diagram
|
|
547
|
+
|
|
548
|
+
```
|
|
549
|
+
WAHA (your-server:3004)
|
|
550
|
+
|
|
|
551
|
+
|--- webhook (X-Webhook-Hmac signed) ---> OpenClaw Webhook Server (your-server:8050)
|
|
552
|
+
| |
|
|
553
|
+
| monitor.ts (verify HMAC, parse envelope)
|
|
554
|
+
| |
|
|
555
|
+
| inbound.ts (access control, presence start)
|
|
556
|
+
| |
|
|
557
|
+
| OpenClaw AI Agent (generate reply)
|
|
558
|
+
| |
|
|
559
|
+
| presence.ts (pad typing to human speed)
|
|
560
|
+
| |
|
|
561
|
+
| <--- WAHA REST API (sendText/sendVoice) --- send.ts (assertAllowedSession)
|
|
562
|
+
|
|
|
563
|
+
v
|
|
564
|
+
WhatsApp recipient sees: blue ticks -> typing... -> text reply -> voice note
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
---
|
|
568
|
+
|
|
569
|
+
## Appendix: Useful Commands
|
|
570
|
+
|
|
571
|
+
```bash
|
|
572
|
+
# Check gateway status
|
|
573
|
+
pgrep -af "openclaw gateway"
|
|
574
|
+
|
|
575
|
+
# Check webhook health
|
|
576
|
+
curl -s http://127.0.0.1:8050/healthz
|
|
577
|
+
|
|
578
|
+
# Check WAHA sessions
|
|
579
|
+
curl -s -H "X-Api-Key: $WHATSAPP_API_KEY" http://127.0.0.1:3004/api/sessions
|
|
580
|
+
|
|
581
|
+
# View recent gateway logs
|
|
582
|
+
tail -50 /tmp/openclaw/openclaw-gateway.log | grep waha
|
|
583
|
+
|
|
584
|
+
# Restart gateway (systemd auto-restarts)
|
|
585
|
+
kill -9 $(pgrep -f "openclaw-gatewa") 2>/dev/null
|
|
586
|
+
|
|
587
|
+
# Verify port binding after restart
|
|
588
|
+
ss -tlnp | grep 18789
|
|
589
|
+
```
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"channels": {
|
|
3
|
+
"waha": {
|
|
4
|
+
"enabled": true,
|
|
5
|
+
"baseUrl": "http://localhost:3004",
|
|
6
|
+
"apiKey": "your-waha-api-key",
|
|
7
|
+
"session": "your-session-name",
|
|
8
|
+
"webhookHost": "0.0.0.0",
|
|
9
|
+
"webhookPort": 8050,
|
|
10
|
+
"webhookPath": "/webhook/waha",
|
|
11
|
+
"webhookHmacKey": "your-hmac-key",
|
|
12
|
+
"dmPolicy": "allowlist",
|
|
13
|
+
"groupPolicy": "allowlist",
|
|
14
|
+
"allowFrom": [
|
|
15
|
+
"15551234567@c.us",
|
|
16
|
+
"123456789012345@lid"
|
|
17
|
+
],
|
|
18
|
+
"groupAllowFrom": [
|
|
19
|
+
"15551234567@c.us",
|
|
20
|
+
"123456789012345@lid"
|
|
21
|
+
],
|
|
22
|
+
"allowedGroups": [
|
|
23
|
+
"120363000000000000@g.us"
|
|
24
|
+
],
|
|
25
|
+
"presence": {
|
|
26
|
+
"enabled": true,
|
|
27
|
+
"sendSeen": true,
|
|
28
|
+
"wpm": 42,
|
|
29
|
+
"readDelayMs": [500, 4000],
|
|
30
|
+
"msPerReadChar": 30,
|
|
31
|
+
"typingDurationMs": [1500, 15000],
|
|
32
|
+
"pauseChance": 0.3,
|
|
33
|
+
"pauseDurationMs": [500, 2000],
|
|
34
|
+
"pauseIntervalMs": [2000, 5000],
|
|
35
|
+
"jitter": [0.7, 1.3]
|
|
36
|
+
},
|
|
37
|
+
"dmFilter": {
|
|
38
|
+
"enabled": true,
|
|
39
|
+
"mentionPatterns": ["yourbot", "help", "hello"],
|
|
40
|
+
"godModeBypass": true,
|
|
41
|
+
"godModeSuperUsers": [
|
|
42
|
+
{ "identifier": "your-phone-number", "platform": "whatsapp", "passwordRequired": false }
|
|
43
|
+
],
|
|
44
|
+
"tokenEstimate": 2500
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
3
|
+
import { wahaPlugin } from "./src/channel.js";
|
|
4
|
+
import { setWahaRuntime } from "./src/runtime.js";
|
|
5
|
+
|
|
6
|
+
const plugin = {
|
|
7
|
+
id: "waha",
|
|
8
|
+
name: "WAHA",
|
|
9
|
+
description: "WAHA (WhatsApp HTTP API) channel plugin",
|
|
10
|
+
configSchema: emptyPluginConfigSchema(),
|
|
11
|
+
register(api: OpenClawPluginApi) {
|
|
12
|
+
setWahaRuntime(api.runtime);
|
|
13
|
+
api.registerChannel({ plugin: wahaPlugin });
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default plugin;
|