@lofa199419/waha-v2 2.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 +50 -0
- package/bin/wa +20 -0
- package/bin/wa-adv +9 -0
- package/bin/waha-advanced-entrypoint +12 -0
- package/bin/waha-cli +11 -0
- package/bin/waha_cli.py +549 -0
- package/index.ts +109 -0
- package/openclaw.plugin.json +10 -0
- package/package.json +59 -0
- package/scripts/install-openclaw-extension.mjs +106 -0
- package/skills/waha-v2/SKILL.md +143 -0
- package/skills/waha-v2-onboarding/SKILL.md +146 -0
- package/src/accounts.ts +133 -0
- package/src/channel.ts +823 -0
- package/src/client.ts +585 -0
- package/src/config-schema.ts +342 -0
- package/src/deliver.ts +70 -0
- package/src/gateway.ts +170 -0
- package/src/login.ts +64 -0
- package/src/outbound.ts +84 -0
- package/src/probe.ts +30 -0
- package/src/routes.ts +260 -0
- package/src/runtime.ts +56 -0
- package/src/types.ts +195 -0
- package/src/webhook.ts +841 -0
package/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# `@lofa199419/waha-v2`
|
|
2
|
+
|
|
3
|
+
OpenClaw WAHA v2 channel plugin with a packaged local CLI wrapper for WAHA session and messaging operations.
|
|
4
|
+
|
|
5
|
+
## What ships
|
|
6
|
+
|
|
7
|
+
- OpenClaw plugin files: `index.ts`, `src/`, `openclaw.plugin.json`
|
|
8
|
+
- Skill docs: `skills/waha-v2/SKILL.md`
|
|
9
|
+
- Local CLI wrappers:
|
|
10
|
+
- `bin/waha-cli`
|
|
11
|
+
- `bin/wa`
|
|
12
|
+
- `bin/wa-adv`
|
|
13
|
+
- `bin/waha-advanced-entrypoint`
|
|
14
|
+
- `bin/waha_cli.py`
|
|
15
|
+
|
|
16
|
+
## Install into an OpenClaw state dir
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @lofa199419/waha-v2
|
|
20
|
+
node node_modules/@lofa199419/waha-v2/scripts/install-openclaw-extension.mjs --state-dir /data/.openclaw --force
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or with `OPENCLAW_STATE_DIR`:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
OPENCLAW_STATE_DIR=/data/.openclaw node node_modules/@lofa199419/waha-v2/scripts/install-openclaw-extension.mjs --force
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Runtime configuration
|
|
30
|
+
|
|
31
|
+
The plugin package does not ship secrets. Provide WAHA configuration through your container or startup flow and render:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
/data/.openclaw/extensions/waha-v2/bin/.env
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Expected values:
|
|
38
|
+
|
|
39
|
+
```dotenv
|
|
40
|
+
WAHA_URL=https://your-waha-host
|
|
41
|
+
WAHA_API_KEY=your-api-key
|
|
42
|
+
WAHA_SESSION_DEFAULT=power
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Publish checklist
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm pack --dry-run
|
|
49
|
+
npm publish --access public
|
|
50
|
+
```
|
package/bin/wa
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
4
|
+
if [[ -f "$DIR/.env" ]]; then
|
|
5
|
+
set -a
|
|
6
|
+
source "$DIR/.env"
|
|
7
|
+
set +a
|
|
8
|
+
fi
|
|
9
|
+
ARGS=("$@")
|
|
10
|
+
HAS_SESSION=0
|
|
11
|
+
for ((i=0; i<${#ARGS[@]}; i++)); do
|
|
12
|
+
if [[ "${ARGS[$i]}" == "--session" ]]; then
|
|
13
|
+
HAS_SESSION=1
|
|
14
|
+
break
|
|
15
|
+
fi
|
|
16
|
+
done
|
|
17
|
+
if [[ $HAS_SESSION -eq 0 && -n "${WAHA_SESSION_DEFAULT:-}" ]]; then
|
|
18
|
+
ARGS+=(--session "$WAHA_SESSION_DEFAULT")
|
|
19
|
+
fi
|
|
20
|
+
exec "$DIR/waha-cli" "${ARGS[@]}"
|
package/bin/wa-adv
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
5
|
+
ENTRYPOINT="$DIR/waha_cli.py"
|
|
6
|
+
|
|
7
|
+
if [[ ! -f "$ENTRYPOINT" ]]; then
|
|
8
|
+
echo "waha-v2: missing WAHA CLI shim at $ENTRYPOINT" >&2
|
|
9
|
+
exit 1
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
exec python3 "$ENTRYPOINT" "$@"
|
package/bin/waha-cli
ADDED
package/bin/waha_cli.py
ADDED
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import argparse
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import pathlib
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
import urllib.error
|
|
9
|
+
import urllib.parse
|
|
10
|
+
import urllib.request
|
|
11
|
+
|
|
12
|
+
VERSION = "3.0.0"
|
|
13
|
+
SCRIPT_DIR = pathlib.Path(__file__).resolve().parent
|
|
14
|
+
OPENCLAW_CHANNEL_ID = "waha-v2"
|
|
15
|
+
DEFAULT_CHARS_PER_SECOND = 12
|
|
16
|
+
DEFAULT_MAX_CHUNK_LENGTH = 1500
|
|
17
|
+
SEEN_DELAY_MS = 3000
|
|
18
|
+
MIN_DELAY_MS = 600
|
|
19
|
+
MAX_DELAY_MS = 8000
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def env(name: str, default: str | None = None) -> str | None:
|
|
23
|
+
return os.environ.get(name, default)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def state_dir() -> pathlib.Path:
|
|
27
|
+
configured = env("OPENCLAW_STATE_DIR")
|
|
28
|
+
if configured:
|
|
29
|
+
return pathlib.Path(configured)
|
|
30
|
+
home = pathlib.Path(env("HOME", str(pathlib.Path.home()))).expanduser()
|
|
31
|
+
return home / ".openclaw"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def parse_dotenv(path: pathlib.Path) -> dict[str, str]:
|
|
35
|
+
values: dict[str, str] = {}
|
|
36
|
+
if not path.exists():
|
|
37
|
+
return values
|
|
38
|
+
for raw_line in path.read_text().splitlines():
|
|
39
|
+
line = raw_line.strip()
|
|
40
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
41
|
+
continue
|
|
42
|
+
key, value = line.split("=", 1)
|
|
43
|
+
values[key.strip()] = value.strip().strip("'").strip('"')
|
|
44
|
+
return values
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def load_openclaw_channel_config() -> dict:
|
|
48
|
+
config_path = state_dir() / "openclaw.json"
|
|
49
|
+
if not config_path.exists():
|
|
50
|
+
return {}
|
|
51
|
+
try:
|
|
52
|
+
data = json.loads(config_path.read_text())
|
|
53
|
+
except Exception:
|
|
54
|
+
return {}
|
|
55
|
+
channels = data.get("channels")
|
|
56
|
+
if not isinstance(channels, dict):
|
|
57
|
+
return {}
|
|
58
|
+
channel = channels.get(OPENCLAW_CHANNEL_ID)
|
|
59
|
+
return channel if isinstance(channel, dict) else {}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def resolve_account_config(channel_config: dict, session_name: str | None) -> dict:
|
|
63
|
+
accounts = channel_config.get("accounts")
|
|
64
|
+
if not isinstance(accounts, dict):
|
|
65
|
+
return {}
|
|
66
|
+
wanted = (session_name or "").strip().lower()
|
|
67
|
+
if not wanted:
|
|
68
|
+
return {}
|
|
69
|
+
exact = accounts.get(session_name)
|
|
70
|
+
if isinstance(exact, dict):
|
|
71
|
+
return exact
|
|
72
|
+
for account_id, config in accounts.items():
|
|
73
|
+
if not isinstance(config, dict):
|
|
74
|
+
continue
|
|
75
|
+
account_name = str(config.get("name", account_id)).strip().lower()
|
|
76
|
+
account_session = str(config.get("session", "")).strip().lower()
|
|
77
|
+
if wanted in {str(account_id).strip().lower(), account_name, account_session}:
|
|
78
|
+
return config
|
|
79
|
+
return {}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def resolved_config(session_name: str | None = None) -> dict[str, str]:
|
|
83
|
+
dotenv = parse_dotenv(SCRIPT_DIR / ".env")
|
|
84
|
+
channel = load_openclaw_channel_config()
|
|
85
|
+
account = resolve_account_config(channel, session_name)
|
|
86
|
+
|
|
87
|
+
session_value = (
|
|
88
|
+
env("WAHA_SESSION_DEFAULT")
|
|
89
|
+
or env("WAHA_DEFAULT_SESSION")
|
|
90
|
+
or str(account.get("session", "")).strip()
|
|
91
|
+
or str(channel.get("session", "")).strip()
|
|
92
|
+
or dotenv.get("WAHA_SESSION_DEFAULT")
|
|
93
|
+
or dotenv.get("WAHA_DEFAULT_SESSION")
|
|
94
|
+
or "default"
|
|
95
|
+
)
|
|
96
|
+
return {
|
|
97
|
+
"WAHA_URL": (
|
|
98
|
+
str(account.get("baseUrl", "")).strip()
|
|
99
|
+
or str(channel.get("baseUrl", "")).strip()
|
|
100
|
+
or env("WAHA_URL")
|
|
101
|
+
or dotenv.get("WAHA_URL", "")
|
|
102
|
+
),
|
|
103
|
+
"WAHA_API_KEY": (
|
|
104
|
+
str(account.get("apiKey", "")).strip()
|
|
105
|
+
or str(channel.get("apiKey", "")).strip()
|
|
106
|
+
or env("WAHA_API_KEY")
|
|
107
|
+
or dotenv.get("WAHA_API_KEY", "")
|
|
108
|
+
),
|
|
109
|
+
"WAHA_SESSION_DEFAULT": session_value,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def resolved_typing_config(session_name: str | None = None) -> dict[str, object]:
|
|
114
|
+
channel = load_openclaw_channel_config()
|
|
115
|
+
account = resolve_account_config(channel, session_name)
|
|
116
|
+
typing: dict = channel.get("typing") if isinstance(channel.get("typing"), dict) else {}
|
|
117
|
+
account_typing: dict = account.get("typing") if isinstance(account.get("typing"), dict) else {}
|
|
118
|
+
|
|
119
|
+
def pick(name: str, default):
|
|
120
|
+
if name in account_typing:
|
|
121
|
+
return account_typing[name]
|
|
122
|
+
if name in typing:
|
|
123
|
+
return typing[name]
|
|
124
|
+
return default
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
"enabled": pick("enabled", True) is not False,
|
|
128
|
+
"chunking": pick("chunking", True) is not False,
|
|
129
|
+
"charsPerSecond": int(pick("charsPerSecond", DEFAULT_CHARS_PER_SECOND)),
|
|
130
|
+
"maxChunkLength": int(pick("maxChunkLength", DEFAULT_MAX_CHUNK_LENGTH)),
|
|
131
|
+
"debug": pick("debug", False) is True,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def require_resolved(name: str, session_name: str | None = None) -> str:
|
|
136
|
+
value = resolved_config(session_name).get(name, "")
|
|
137
|
+
if not value:
|
|
138
|
+
if name == "WAHA_URL" or name == "WAHA_API_KEY":
|
|
139
|
+
print(
|
|
140
|
+
f"Missing required config: {name}. Checked channels.{OPENCLAW_CHANNEL_ID} in "
|
|
141
|
+
f"{state_dir() / 'openclaw.json'}, then environment, then {SCRIPT_DIR / '.env'}",
|
|
142
|
+
file=sys.stderr,
|
|
143
|
+
)
|
|
144
|
+
else:
|
|
145
|
+
print(f"Missing required config: {name}", file=sys.stderr)
|
|
146
|
+
sys.exit(2)
|
|
147
|
+
return value
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def default_session() -> str:
|
|
151
|
+
return require_resolved("WAHA_SESSION_DEFAULT")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def normalize_base_url(value: str) -> str:
|
|
155
|
+
return value.rstrip("/")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def sleep_ms(delay_ms: int) -> None:
|
|
159
|
+
time.sleep(max(0, delay_ms) / 1000.0)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def calc_typing_delay_ms(text: str, chars_per_second: int) -> int:
|
|
163
|
+
raw = round((len(text) / max(chars_per_second, 1)) * 1000)
|
|
164
|
+
return max(MIN_DELAY_MS, min(MAX_DELAY_MS, raw))
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def chunk_waha_message(text: str, max_length: int) -> list[str]:
|
|
168
|
+
chunks: list[str] = []
|
|
169
|
+
paragraphs = text.split("\n\n")
|
|
170
|
+
for para in paragraphs:
|
|
171
|
+
trimmed = para.strip()
|
|
172
|
+
if not trimmed:
|
|
173
|
+
continue
|
|
174
|
+
if len(trimmed) <= max_length:
|
|
175
|
+
chunks.append(trimmed)
|
|
176
|
+
continue
|
|
177
|
+
current = trimmed
|
|
178
|
+
while len(current) > max_length:
|
|
179
|
+
split_at = max(current.rfind("\n", 0, max_length), current.rfind(" ", 0, max_length))
|
|
180
|
+
if split_at <= 0:
|
|
181
|
+
split_at = max_length
|
|
182
|
+
part = current[:split_at].strip()
|
|
183
|
+
if part:
|
|
184
|
+
chunks.append(part)
|
|
185
|
+
current = current[split_at:].strip()
|
|
186
|
+
if current:
|
|
187
|
+
chunks.append(current)
|
|
188
|
+
if chunks:
|
|
189
|
+
return chunks
|
|
190
|
+
stripped = text.strip()
|
|
191
|
+
return [stripped] if stripped else []
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def debug_log(enabled: bool, message: str) -> None:
|
|
195
|
+
if not enabled:
|
|
196
|
+
return
|
|
197
|
+
print(f"[waha-cli delivery] {time.strftime('%Y-%m-%dT%H:%M:%S%z')} {message}", file=sys.stderr)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def send_presence(endpoint: str, session: str, chat_id: str) -> None:
|
|
201
|
+
waha_request("POST", endpoint, {"session": session, "chatId": chat_id}, session_name=session)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def waha_request(method: str, path: str, body: dict | None = None, query: dict | None = None, session_name: str | None = None):
|
|
205
|
+
base_url = normalize_base_url(require_resolved("WAHA_URL", session_name))
|
|
206
|
+
api_key = require_resolved("WAHA_API_KEY", session_name)
|
|
207
|
+
url = f"{base_url}{path}"
|
|
208
|
+
if query:
|
|
209
|
+
filtered = {k: v for k, v in query.items() if v is not None}
|
|
210
|
+
if filtered:
|
|
211
|
+
url = f"{url}?{urllib.parse.urlencode(filtered)}"
|
|
212
|
+
data = None
|
|
213
|
+
headers = {
|
|
214
|
+
"X-Api-Key": api_key,
|
|
215
|
+
"User-Agent": f"waha-cli/{VERSION}",
|
|
216
|
+
"Accept": "application/json, text/plain;q=0.9, */*;q=0.8",
|
|
217
|
+
}
|
|
218
|
+
if body is not None:
|
|
219
|
+
data = json.dumps(body).encode("utf-8")
|
|
220
|
+
headers["Content-Type"] = "application/json"
|
|
221
|
+
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
|
222
|
+
try:
|
|
223
|
+
with urllib.request.urlopen(req) as resp:
|
|
224
|
+
payload = resp.read()
|
|
225
|
+
content_type = resp.headers.get("content-type", "")
|
|
226
|
+
if "application/json" in content_type:
|
|
227
|
+
return json.loads(payload.decode("utf-8"))
|
|
228
|
+
return payload.decode("utf-8")
|
|
229
|
+
except urllib.error.HTTPError as exc:
|
|
230
|
+
try:
|
|
231
|
+
error_body = exc.read().decode("utf-8")
|
|
232
|
+
parsed = json.loads(error_body)
|
|
233
|
+
message = parsed.get("message", error_body)
|
|
234
|
+
except Exception:
|
|
235
|
+
message = exc.reason
|
|
236
|
+
print(f"WAHA API error ({exc.code}): {message}", file=sys.stderr)
|
|
237
|
+
sys.exit(1)
|
|
238
|
+
except urllib.error.URLError as exc:
|
|
239
|
+
print(f"WAHA API request failed: {exc.reason}", file=sys.stderr)
|
|
240
|
+
sys.exit(1)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def print_json(data):
|
|
244
|
+
print(json.dumps(data, indent=2, ensure_ascii=False))
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def print_help():
|
|
248
|
+
print(
|
|
249
|
+
f"""waha-cli v{VERSION}
|
|
250
|
+
|
|
251
|
+
Usage:
|
|
252
|
+
waha-cli --help
|
|
253
|
+
waha-cli --version
|
|
254
|
+
waha-cli <command> [options]
|
|
255
|
+
|
|
256
|
+
Session and Auth:
|
|
257
|
+
waha-list-sessions
|
|
258
|
+
waha-get-session --session NAME
|
|
259
|
+
waha-create-session --name NAME
|
|
260
|
+
waha-start-session --session NAME
|
|
261
|
+
waha-stop-session --session NAME
|
|
262
|
+
waha-restart-session --session NAME
|
|
263
|
+
waha-delete-session --session NAME
|
|
264
|
+
waha-logout-session --session NAME
|
|
265
|
+
waha-get-qr-code --session NAME
|
|
266
|
+
waha-request-pairing-code --session NAME --phone-number NUMBER
|
|
267
|
+
waha-check-auth-status --session NAME
|
|
268
|
+
|
|
269
|
+
Chats and Messages:
|
|
270
|
+
waha-get-chats [--session NAME] [--limit N] [--offset N]
|
|
271
|
+
waha-get-messages --chat-id ID [--session NAME] [--limit N] [--offset N]
|
|
272
|
+
waha-send-text --chat-id ID --text TEXT [--session NAME]
|
|
273
|
+
|
|
274
|
+
Optional env:
|
|
275
|
+
WAHA_SESSION_DEFAULT / WAHA_DEFAULT_SESSION
|
|
276
|
+
Fallback env:
|
|
277
|
+
WAHA_URL
|
|
278
|
+
WAHA_API_KEY
|
|
279
|
+
|
|
280
|
+
Default config source:
|
|
281
|
+
OPENCLAW_STATE_DIR/openclaw.json -> channels.waha-v2"""
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def add_session_arg(parser):
|
|
286
|
+
parser.add_argument("--session", default=default_session())
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def build_parser():
|
|
290
|
+
parser = argparse.ArgumentParser(add_help=False)
|
|
291
|
+
parser.add_argument("--help", action="store_true")
|
|
292
|
+
parser.add_argument("--version", action="store_true")
|
|
293
|
+
parser.add_argument("command", nargs="?")
|
|
294
|
+
parser.add_argument("rest", nargs=argparse.REMAINDER)
|
|
295
|
+
return parser
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def cmd_list_sessions(_args):
|
|
299
|
+
print_json(waha_request("GET", "/api/sessions"))
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def cmd_get_session(args):
|
|
303
|
+
print_json(waha_request("GET", f"/api/sessions/{urllib.parse.quote(args.session, safe='')}", session_name=args.session))
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def cmd_create_session(args):
|
|
307
|
+
body = {"name": args.name}
|
|
308
|
+
print_json(waha_request("POST", "/api/sessions", body, session_name=args.name))
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def cmd_simple_post(args, suffix):
|
|
312
|
+
print_json(
|
|
313
|
+
waha_request(
|
|
314
|
+
"POST",
|
|
315
|
+
f"/api/sessions/{urllib.parse.quote(args.session, safe='')}/{suffix}",
|
|
316
|
+
session_name=args.session,
|
|
317
|
+
)
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def cmd_delete_session(args):
|
|
322
|
+
waha_request("DELETE", f"/api/sessions/{urllib.parse.quote(args.session, safe='')}", session_name=args.session)
|
|
323
|
+
print(f'Session "{args.session}" deleted successfully.')
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def cmd_logout_session(args):
|
|
327
|
+
waha_request("POST", f"/api/sessions/{urllib.parse.quote(args.session, safe='')}/logout", session_name=args.session)
|
|
328
|
+
print(f'Session "{args.session}" logged out successfully.')
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def cmd_get_qr_code(args):
|
|
332
|
+
qr = waha_request(
|
|
333
|
+
"GET",
|
|
334
|
+
f"/api/{urllib.parse.quote(args.session, safe='')}/auth/qr",
|
|
335
|
+
query={"format": "raw"},
|
|
336
|
+
session_name=args.session,
|
|
337
|
+
)
|
|
338
|
+
if isinstance(qr, dict) and "value" in qr:
|
|
339
|
+
qr = qr["value"]
|
|
340
|
+
print(f'QR Code value for session "{args.session}":')
|
|
341
|
+
print(qr)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def cmd_request_pairing_code(args):
|
|
345
|
+
result = waha_request(
|
|
346
|
+
"POST",
|
|
347
|
+
f"/api/{urllib.parse.quote(args.session, safe='')}/auth/request-code",
|
|
348
|
+
{"phoneNumber": args.phone_number},
|
|
349
|
+
session_name=args.session,
|
|
350
|
+
)
|
|
351
|
+
code = result.get("code", "<missing>")
|
|
352
|
+
print(
|
|
353
|
+
f'Pairing code requested for {args.phone_number} on session "{args.session}".\n'
|
|
354
|
+
f"Code: {code}\n"
|
|
355
|
+
"Enter this code in WhatsApp on your phone."
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def cmd_check_auth_status(args):
|
|
360
|
+
info = waha_request("GET", f"/api/sessions/{urllib.parse.quote(args.session, safe='')}", session_name=args.session)
|
|
361
|
+
messages = {
|
|
362
|
+
"STOPPED": "Session is stopped. Start it first.",
|
|
363
|
+
"STARTING": "Session is starting up...",
|
|
364
|
+
"SCAN_QR_CODE": "Waiting for QR code scan. Use waha-get-qr-code or waha-request-pairing-code.",
|
|
365
|
+
"WORKING": "Session is authenticated and working.",
|
|
366
|
+
"FAILED": "Session has failed. Try restarting or re-authenticating.",
|
|
367
|
+
}
|
|
368
|
+
text = [
|
|
369
|
+
f"Session: {args.session}",
|
|
370
|
+
f"Status: {info.get('status', 'UNKNOWN')}",
|
|
371
|
+
messages.get(info.get("status"), f"Unknown status: {info.get('status')}"),
|
|
372
|
+
]
|
|
373
|
+
me = info.get("me")
|
|
374
|
+
if isinstance(me, dict):
|
|
375
|
+
if me.get("id"):
|
|
376
|
+
text.append(f"Account ID: {me['id']}")
|
|
377
|
+
if me.get("pushName"):
|
|
378
|
+
text.append(f"Name: {me['pushName']}")
|
|
379
|
+
print("\n".join(text))
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def cmd_get_chats(args):
|
|
383
|
+
print_json(
|
|
384
|
+
waha_request(
|
|
385
|
+
"GET",
|
|
386
|
+
f"/api/{urllib.parse.quote(args.session, safe='')}/chats",
|
|
387
|
+
query={"limit": args.limit, "offset": args.offset},
|
|
388
|
+
session_name=args.session,
|
|
389
|
+
)
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def cmd_get_messages(args):
|
|
394
|
+
query = {"limit": args.limit, "offset": args.offset}
|
|
395
|
+
print_json(
|
|
396
|
+
waha_request(
|
|
397
|
+
"GET",
|
|
398
|
+
f"/api/{urllib.parse.quote(args.session, safe='')}/chats/{urllib.parse.quote(args.chat_id, safe='')}/messages",
|
|
399
|
+
query=query,
|
|
400
|
+
session_name=args.session,
|
|
401
|
+
)
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def cmd_send_text(args):
|
|
406
|
+
typing_cfg = resolved_typing_config(args.session)
|
|
407
|
+
typing_enabled = bool(typing_cfg["enabled"])
|
|
408
|
+
chunking_enabled = bool(typing_cfg["chunking"])
|
|
409
|
+
chars_per_second = int(typing_cfg["charsPerSecond"])
|
|
410
|
+
max_chunk_length = int(typing_cfg["maxChunkLength"])
|
|
411
|
+
debug_enabled = bool(typing_cfg["debug"])
|
|
412
|
+
|
|
413
|
+
chunks = chunk_waha_message(args.text, max_chunk_length) if chunking_enabled else [args.text.strip()]
|
|
414
|
+
chunks = [chunk for chunk in chunks if chunk]
|
|
415
|
+
if not chunks:
|
|
416
|
+
print("Nothing to send.", file=sys.stderr)
|
|
417
|
+
sys.exit(1)
|
|
418
|
+
|
|
419
|
+
debug_log(debug_enabled, f"queued outbound send chat={args.chat_id} session={args.session}")
|
|
420
|
+
sleep_ms(SEEN_DELAY_MS)
|
|
421
|
+
waha_request(
|
|
422
|
+
"POST",
|
|
423
|
+
"/api/sendSeen",
|
|
424
|
+
{"session": args.session, "chatId": args.chat_id},
|
|
425
|
+
session_name=args.session,
|
|
426
|
+
)
|
|
427
|
+
debug_log(debug_enabled, f"marked seen after {SEEN_DELAY_MS}ms chat={args.chat_id}")
|
|
428
|
+
|
|
429
|
+
pre_output_typing_started = False
|
|
430
|
+
if typing_enabled:
|
|
431
|
+
send_presence("/api/startTyping", args.session, args.chat_id)
|
|
432
|
+
pre_output_typing_started = True
|
|
433
|
+
debug_log(debug_enabled, f"pre-output started typing chat={args.chat_id}")
|
|
434
|
+
|
|
435
|
+
debug_log(
|
|
436
|
+
debug_enabled,
|
|
437
|
+
f"output ready chat={args.chat_id} chunking={chunking_enabled} chunks={len(chunks)} totalLen={len(args.text)}",
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
message_ids: list[str] = []
|
|
441
|
+
if pre_output_typing_started:
|
|
442
|
+
send_presence("/api/stopTyping", args.session, args.chat_id)
|
|
443
|
+
debug_log(debug_enabled, f"pre-output stopped typing chat={args.chat_id}")
|
|
444
|
+
|
|
445
|
+
if len(chunks) == 1:
|
|
446
|
+
result = waha_request(
|
|
447
|
+
"POST",
|
|
448
|
+
"/api/sendText",
|
|
449
|
+
{"session": args.session, "chatId": args.chat_id, "text": chunks[0]},
|
|
450
|
+
session_name=args.session,
|
|
451
|
+
)
|
|
452
|
+
message_ids.append(str(result.get("id", "<missing>")))
|
|
453
|
+
debug_log(debug_enabled, f"sent single reply chat={args.chat_id} len={len(chunks[0])}")
|
|
454
|
+
else:
|
|
455
|
+
for i, chunk in enumerate(chunks):
|
|
456
|
+
if i > 0 and typing_enabled:
|
|
457
|
+
send_presence("/api/startTyping", args.session, args.chat_id)
|
|
458
|
+
debug_log(debug_enabled, f"chunk {i + 1}/{len(chunks)} started typing chat={args.chat_id}")
|
|
459
|
+
sleep_ms(calc_typing_delay_ms(chunk, chars_per_second))
|
|
460
|
+
send_presence("/api/stopTyping", args.session, args.chat_id)
|
|
461
|
+
debug_log(debug_enabled, f"chunk {i + 1}/{len(chunks)} stopped typing chat={args.chat_id}")
|
|
462
|
+
|
|
463
|
+
result = waha_request(
|
|
464
|
+
"POST",
|
|
465
|
+
"/api/sendText",
|
|
466
|
+
{"session": args.session, "chatId": args.chat_id, "text": chunk},
|
|
467
|
+
session_name=args.session,
|
|
468
|
+
)
|
|
469
|
+
message_ids.append(str(result.get("id", "<missing>")))
|
|
470
|
+
debug_log(debug_enabled, f"sent chunk {i + 1}/{len(chunks)} chat={args.chat_id} len={len(chunk)}")
|
|
471
|
+
|
|
472
|
+
print("Message sent successfully.")
|
|
473
|
+
for index, message_id in enumerate(message_ids, start=1):
|
|
474
|
+
if len(message_ids) == 1:
|
|
475
|
+
print(f"Message ID: {message_id}")
|
|
476
|
+
else:
|
|
477
|
+
print(f"Chunk {index} Message ID: {message_id}")
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
COMMANDS = {
|
|
481
|
+
"waha-list-sessions": ("List sessions", cmd_list_sessions),
|
|
482
|
+
"waha-get-session": ("Get session", cmd_get_session),
|
|
483
|
+
"waha-create-session": ("Create session", cmd_create_session),
|
|
484
|
+
"waha-start-session": ("Start session", lambda args: cmd_simple_post(args, "start")),
|
|
485
|
+
"waha-stop-session": ("Stop session", lambda args: cmd_simple_post(args, "stop")),
|
|
486
|
+
"waha-restart-session": ("Restart session", lambda args: cmd_simple_post(args, "restart")),
|
|
487
|
+
"waha-delete-session": ("Delete session", cmd_delete_session),
|
|
488
|
+
"waha-logout-session": ("Logout session", cmd_logout_session),
|
|
489
|
+
"waha-get-qr-code": ("Get QR code", cmd_get_qr_code),
|
|
490
|
+
"waha-request-pairing-code": ("Request pairing code", cmd_request_pairing_code),
|
|
491
|
+
"waha-check-auth-status": ("Check auth status", cmd_check_auth_status),
|
|
492
|
+
"waha-get-chats": ("Get chats", cmd_get_chats),
|
|
493
|
+
"waha-list-chats": ("List chats", cmd_get_chats),
|
|
494
|
+
"waha-get-messages": ("Get messages", cmd_get_messages),
|
|
495
|
+
"waha-send-text": ("Send text", cmd_send_text),
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def command_parser(command: str):
|
|
500
|
+
parser = argparse.ArgumentParser(prog=f"waha-cli {command}")
|
|
501
|
+
if command in {"waha-get-session", "waha-start-session", "waha-stop-session", "waha-restart-session", "waha-delete-session", "waha-logout-session", "waha-get-qr-code", "waha-check-auth-status"}:
|
|
502
|
+
add_session_arg(parser)
|
|
503
|
+
elif command == "waha-create-session":
|
|
504
|
+
parser.add_argument("--name", required=True)
|
|
505
|
+
elif command == "waha-request-pairing-code":
|
|
506
|
+
add_session_arg(parser)
|
|
507
|
+
parser.add_argument("--phone-number", required=True)
|
|
508
|
+
elif command in {"waha-get-chats", "waha-list-chats"}:
|
|
509
|
+
add_session_arg(parser)
|
|
510
|
+
parser.add_argument("--limit", type=int, default=50)
|
|
511
|
+
parser.add_argument("--offset", type=int, default=0)
|
|
512
|
+
elif command == "waha-get-messages":
|
|
513
|
+
add_session_arg(parser)
|
|
514
|
+
parser.add_argument("--chat-id", required=True)
|
|
515
|
+
parser.add_argument("--limit", type=int, default=20)
|
|
516
|
+
parser.add_argument("--offset", type=int, default=0)
|
|
517
|
+
elif command == "waha-send-text":
|
|
518
|
+
add_session_arg(parser)
|
|
519
|
+
parser.add_argument("--chat-id", required=True)
|
|
520
|
+
parser.add_argument("--text", required=True)
|
|
521
|
+
return parser
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def main():
|
|
525
|
+
parser = build_parser()
|
|
526
|
+
args = parser.parse_args()
|
|
527
|
+
|
|
528
|
+
if args.help or not args.command:
|
|
529
|
+
print_help()
|
|
530
|
+
return 0
|
|
531
|
+
if args.version:
|
|
532
|
+
print(VERSION)
|
|
533
|
+
return 0
|
|
534
|
+
|
|
535
|
+
command = args.command.replace("_", "-")
|
|
536
|
+
handler_entry = COMMANDS.get(command)
|
|
537
|
+
if handler_entry is None:
|
|
538
|
+
print(f"Unknown command: {args.command}", file=sys.stderr)
|
|
539
|
+
print_help()
|
|
540
|
+
return 2
|
|
541
|
+
|
|
542
|
+
cmd_parser = command_parser(command)
|
|
543
|
+
cmd_args = cmd_parser.parse_args(args.rest)
|
|
544
|
+
handler_entry[1](cmd_args)
|
|
545
|
+
return 0
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
if __name__ == "__main__":
|
|
549
|
+
sys.exit(main())
|