crewly 1.20.121 → 1.20.123
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/config/skills/agent/core/whatsapp-draft/SKILL.md +80 -0
- package/config/skills/agent/core/whatsapp-draft/execute.sh +84 -0
- package/config/skills/agent/core/whatsapp-draft/execute.test.sh +70 -0
- package/config/skills/agent/core/whatsapp-inbox/SKILL.md +75 -0
- package/config/skills/agent/core/whatsapp-inbox/execute.sh +87 -0
- package/config/skills/agent/core/whatsapp-inbox/execute.test.sh +68 -0
- package/config/skills/agent/core/whatsapp-read/SKILL.md +80 -0
- package/config/skills/agent/core/whatsapp-read/execute.sh +116 -0
- package/config/skills/agent/core/whatsapp-read/execute.test.sh +84 -0
- package/config/skills/agent/core/whatsapp-send/SKILL.md +70 -0
- package/config/skills/agent/core/whatsapp-send/execute.sh +84 -0
- package/config/skills/agent/core/whatsapp-send/execute.test.sh +79 -0
- package/dist/backend/backend/src/constants.d.ts +107 -0
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +105 -0
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/whatsapp/whatsapp-inbox.controller.d.ts +73 -0
- package/dist/backend/backend/src/controllers/whatsapp/whatsapp-inbox.controller.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/whatsapp/whatsapp-inbox.controller.js +310 -0
- package/dist/backend/backend/src/controllers/whatsapp/whatsapp-inbox.controller.js.map +1 -0
- package/dist/backend/backend/src/controllers/whatsapp/whatsapp.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/whatsapp/whatsapp.controller.js +53 -4
- package/dist/backend/backend/src/controllers/whatsapp/whatsapp.controller.js.map +1 -1
- package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
- package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js +5 -0
- package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
- package/dist/backend/backend/src/services/browser/browser-session.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/browser/browser-session.service.js +13 -0
- package/dist/backend/backend/src/services/browser/browser-session.service.js.map +1 -1
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js +29 -1
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js.map +1 -1
- package/dist/backend/backend/src/services/whatsapp/index.d.ts +5 -1
- package/dist/backend/backend/src/services/whatsapp/index.d.ts.map +1 -1
- package/dist/backend/backend/src/services/whatsapp/index.js +5 -1
- package/dist/backend/backend/src/services/whatsapp/index.js.map +1 -1
- package/dist/backend/backend/src/services/whatsapp/whatsapp-connection-config.d.ts +59 -0
- package/dist/backend/backend/src/services/whatsapp/whatsapp-connection-config.d.ts.map +1 -0
- package/dist/backend/backend/src/services/whatsapp/whatsapp-connection-config.js +108 -0
- package/dist/backend/backend/src/services/whatsapp/whatsapp-connection-config.js.map +1 -0
- package/dist/backend/backend/src/services/whatsapp/whatsapp-draft-gate.d.ts +74 -0
- package/dist/backend/backend/src/services/whatsapp/whatsapp-draft-gate.d.ts.map +1 -0
- package/dist/backend/backend/src/services/whatsapp/whatsapp-draft-gate.js +82 -0
- package/dist/backend/backend/src/services/whatsapp/whatsapp-draft-gate.js.map +1 -0
- package/dist/backend/backend/src/services/whatsapp/whatsapp-inbox-capture.d.ts +184 -0
- package/dist/backend/backend/src/services/whatsapp/whatsapp-inbox-capture.d.ts.map +1 -0
- package/dist/backend/backend/src/services/whatsapp/whatsapp-inbox-capture.js +420 -0
- package/dist/backend/backend/src/services/whatsapp/whatsapp-inbox-capture.js.map +1 -0
- package/dist/backend/backend/src/services/whatsapp/whatsapp-inbox.store.d.ts +243 -0
- package/dist/backend/backend/src/services/whatsapp/whatsapp-inbox.store.d.ts.map +1 -0
- package/dist/backend/backend/src/services/whatsapp/whatsapp-inbox.store.js +538 -0
- package/dist/backend/backend/src/services/whatsapp/whatsapp-inbox.store.js.map +1 -0
- package/dist/backend/backend/src/services/whatsapp/whatsapp-initializer.d.ts +32 -2
- package/dist/backend/backend/src/services/whatsapp/whatsapp-initializer.d.ts.map +1 -1
- package/dist/backend/backend/src/services/whatsapp/whatsapp-initializer.js +65 -9
- package/dist/backend/backend/src/services/whatsapp/whatsapp-initializer.js.map +1 -1
- package/dist/backend/backend/src/services/whatsapp/whatsapp-orchestrator-bridge.d.ts.map +1 -1
- package/dist/backend/backend/src/services/whatsapp/whatsapp-orchestrator-bridge.js +11 -0
- package/dist/backend/backend/src/services/whatsapp/whatsapp-orchestrator-bridge.js.map +1 -1
- package/dist/backend/backend/src/services/whatsapp/whatsapp.service.d.ts +59 -1
- package/dist/backend/backend/src/services/whatsapp/whatsapp.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/whatsapp/whatsapp.service.js +159 -9
- package/dist/backend/backend/src/services/whatsapp/whatsapp.service.js.map +1 -1
- package/dist/backend/backend/src/types/whatsapp.types.d.ts +108 -0
- package/dist/backend/backend/src/types/whatsapp.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/whatsapp.types.js +9 -0
- package/dist/backend/backend/src/types/whatsapp.types.js.map +1 -1
- package/dist/backend/build-info.json +2 -2
- package/dist/cli/backend/src/constants.d.ts +107 -0
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +105 -0
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
- package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js +5 -0
- package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
- package/frontend/dist/assets/{index-07e338a9.js → index-daf43f1a.js} +268 -268
- package/frontend/dist/index.html +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: WhatsApp Draft
|
|
3
|
+
description: Write a reply draft for one of the owner's WhatsApp chats. It is saved with a short code (W12) and NEVER sent — the owner releases it by replying 「发 W12」 or from the dashboard.
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
category: communication
|
|
6
|
+
skillType: claude-skill
|
|
7
|
+
assignableRoles:
|
|
8
|
+
- orchestrator
|
|
9
|
+
- team-leader
|
|
10
|
+
- generalist
|
|
11
|
+
- operations
|
|
12
|
+
- ops
|
|
13
|
+
- support
|
|
14
|
+
- sales
|
|
15
|
+
triggers:
|
|
16
|
+
- draft whatsapp reply
|
|
17
|
+
- reply on whatsapp
|
|
18
|
+
- write a whatsapp message
|
|
19
|
+
tags:
|
|
20
|
+
- whatsapp
|
|
21
|
+
- draft
|
|
22
|
+
execution:
|
|
23
|
+
type: script
|
|
24
|
+
script:
|
|
25
|
+
file: execute.sh
|
|
26
|
+
interpreter: bash
|
|
27
|
+
timeoutMs: 30000
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
# WhatsApp Draft
|
|
31
|
+
|
|
32
|
+
Saves a proposed reply. **It never sends.**
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
bash execute.sh --chat 4915550001@s.whatsapp.net --text "Yes, 8 works — see you there"
|
|
36
|
+
bash execute.sh --chat 4915550001@s.whatsapp.net --text-file /tmp/reply.txt
|
|
37
|
+
bash execute.sh '{"chatId":"4915550001@s.whatsapp.net","text":"…"}'
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## After drafting — what you must do
|
|
41
|
+
|
|
42
|
+
Show the owner, in your reply to them:
|
|
43
|
+
|
|
44
|
+
1. **who** it goes to (`draft.recipient`),
|
|
45
|
+
2. **the exact text** (`draft.text`),
|
|
46
|
+
3. **the code** (`draft.code`, e.g. `W12`),
|
|
47
|
+
|
|
48
|
+
and ask them to reply **「发 W12」** to send it (or 发送 / 丢弃 on the dashboard's
|
|
49
|
+
WhatsApp tab). Then stop. Only after they have replied 「发 W12」 may you run
|
|
50
|
+
`whatsapp-send --draft W12`. If they change the wording, write a new draft.
|
|
51
|
+
|
|
52
|
+
## Rules (owner decision — apply to every whatsapp-* skill)
|
|
53
|
+
|
|
54
|
+
- **Never send anything without the owner's explicit 「发 <code>」.** One draft,
|
|
55
|
+
one confirmation, one message.
|
|
56
|
+
- **Never auto-reply to anyone** — no acknowledgements, no "will get back to you".
|
|
57
|
+
- **Summarise; don't paste private chats wholesale** into other channels. The
|
|
58
|
+
owner needs the gist and your proposed reply, not a transcript.
|
|
59
|
+
- **Groups are low priority.** Draft for a group only when the owner asks.
|
|
60
|
+
|
|
61
|
+
## Parameters
|
|
62
|
+
|
|
63
|
+
| Name | Required | Meaning |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| `--chat` (`chatId`) | yes | Recipient chat JID; must already be in the inbox |
|
|
66
|
+
| `--text` (`text`) | yes* | Reply text (max 4000 chars) |
|
|
67
|
+
| `--text-file` | yes* | Read the text from a file instead |
|
|
68
|
+
|
|
69
|
+
## Output
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{"success":true,"sent":false,
|
|
73
|
+
"draft":{"id":"…","code":"W12","recipient":"Ann","chatId":"4915550001@s.whatsapp.net","text":"Yes, 8 works — see you there"},
|
|
74
|
+
"nextStep":"NOT SENT. Show the owner: recipient Ann, the exact text above, and code W12. Ask them to reply 「发 W12」 …"}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Failures
|
|
78
|
+
|
|
79
|
+
`{"success":false,"reason":"chat_not_found"}` (exit 1) — you can only draft to a
|
|
80
|
+
chat that is in the inbox; find it with `whatsapp-read --chats --q <name>`.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# whatsapp-draft — Propose a reply to a WhatsApp chat. NEVER sends.
|
|
4
|
+
#
|
|
5
|
+
# Backed by POST /api/whatsapp/drafts {chatId, text}. The draft gets a short
|
|
6
|
+
# code (W12). Only the owner releases it: by replying 「发 W12」 in chat (then
|
|
7
|
+
# whatsapp-send), or by pressing 发送 in the dashboard.
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# bash execute.sh --chat <jid> --text "reply text"
|
|
11
|
+
# bash execute.sh --chat <jid> --text-file /path/to/reply.txt
|
|
12
|
+
# bash execute.sh '{"chatId":"…","text":"…"}'
|
|
13
|
+
# =============================================================================
|
|
14
|
+
set -euo pipefail
|
|
15
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
16
|
+
source "${SCRIPT_DIR}/../../_common/lib.sh"
|
|
17
|
+
|
|
18
|
+
print_usage() {
|
|
19
|
+
cat <<'EOF_USAGE'
|
|
20
|
+
Usage:
|
|
21
|
+
bash execute.sh --chat <jid> --text "reply text"
|
|
22
|
+
bash execute.sh --chat <jid> --text-file /path/to/reply.txt
|
|
23
|
+
bash execute.sh '{"chatId":"…","text":"…"}'
|
|
24
|
+
|
|
25
|
+
Options:
|
|
26
|
+
--chat Recipient chat JID (must already be in the inbox)
|
|
27
|
+
--text Reply text
|
|
28
|
+
--text-file Read the reply text from a file
|
|
29
|
+
--help | -h Show this help
|
|
30
|
+
|
|
31
|
+
This only saves a draft. It never sends.
|
|
32
|
+
EOF_USAGE
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
INPUT_JSON=""
|
|
36
|
+
CHAT=""; TEXT=""; TEXT_FILE=""
|
|
37
|
+
|
|
38
|
+
if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
|
|
39
|
+
INPUT_JSON="$1"
|
|
40
|
+
shift || true
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
while [[ $# -gt 0 ]]; do
|
|
44
|
+
case "$1" in
|
|
45
|
+
--chat|-c) [ $# -ge 2 ] || error_exit "--chat requires a value"; CHAT="$2"; shift 2 ;;
|
|
46
|
+
--text) [ $# -ge 2 ] || error_exit "--text requires a value"; TEXT="$2"; shift 2 ;;
|
|
47
|
+
--text-file) [ $# -ge 2 ] || error_exit "--text-file requires a value"; TEXT_FILE="$2"; shift 2 ;;
|
|
48
|
+
--help|-h) print_usage; exit 0 ;;
|
|
49
|
+
*) error_exit "Unknown option: $1" ;;
|
|
50
|
+
esac
|
|
51
|
+
done
|
|
52
|
+
|
|
53
|
+
if [ -n "$INPUT_JSON" ]; then
|
|
54
|
+
INPUT=$(read_json_input "$INPUT_JSON")
|
|
55
|
+
[ -z "$CHAT" ] && CHAT=$(printf '%s' "$INPUT" | jq -r '.chatId // .chat // empty')
|
|
56
|
+
[ -z "$TEXT" ] && TEXT=$(printf '%s' "$INPUT" | jq -r '.text // empty')
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
if [ -n "$TEXT_FILE" ]; then
|
|
60
|
+
[ -f "$TEXT_FILE" ] || error_exit "text file not found: $TEXT_FILE"
|
|
61
|
+
TEXT=$(cat "$TEXT_FILE")
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
require_param "chat (--chat)" "$CHAT"
|
|
65
|
+
require_param "text (--text or --text-file)" "$TEXT"
|
|
66
|
+
|
|
67
|
+
BODY=$(jq -cn --arg chatId "$CHAT" --arg text "$TEXT" '{chatId: $chatId, text: $text}')
|
|
68
|
+
|
|
69
|
+
# stderr is kept apart so lib.sh's session warning cannot pollute a good response.
|
|
70
|
+
ERR_FILE=$(mktemp); trap 'rm -f "$ERR_FILE"' EXIT
|
|
71
|
+
RESPONSE=$(api_call POST "/whatsapp/drafts" "$BODY" 2>"$ERR_FILE") || {
|
|
72
|
+
ERR=$(tail -n 1 "$ERR_FILE")
|
|
73
|
+
printf '%s' "$ERR" | jq -c '{success: false, reason: (.details.code // .details.error // .details // .error // "unknown"), message: (.details.error // "")}' 2>/dev/null \
|
|
74
|
+
|| jq -n --arg r "$ERR" '{success: false, reason: $r}'
|
|
75
|
+
exit 1
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# The next step is the owner's, not ours: say so in the output itself.
|
|
79
|
+
printf '%s' "$RESPONSE" | jq -c '.data as $d | {
|
|
80
|
+
success: true,
|
|
81
|
+
sent: false,
|
|
82
|
+
draft: {id: $d.id, code: $d.code, recipient: $d.recipient, chatId: $d.chatId, text: $d.text},
|
|
83
|
+
nextStep: ("NOT SENT. Show the owner: recipient \($d.recipient), the exact text above, and code \($d.code). Ask them to reply 「发 \($d.code)」 to send it (or use 发送/丢弃 in the dashboard). Do not call whatsapp-send until they have replied 「发 \($d.code)」.")
|
|
84
|
+
}'
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Tests for whatsapp-draft — run with: bash execute.test.sh </dev/null
|
|
3
|
+
# Spins up a python HTTP stub as the backend, asserts the request body the
|
|
4
|
+
# skill sends and the JSON it prints. Exit 0 on pass.
|
|
5
|
+
set -u
|
|
6
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
+
EXEC="$HERE/execute.sh"
|
|
8
|
+
PASS=0; FAIL=0
|
|
9
|
+
check() {
|
|
10
|
+
local name="$1"; local got="$2"; local want="$3"
|
|
11
|
+
if [ "$got" = "$want" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: $name"; echo " want: $want"; echo " got: $got"; fi
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
PORT=18843
|
|
15
|
+
STUB_LOG="$(mktemp)"; export STUB_LOG; export STUB_PORT="$PORT"
|
|
16
|
+
python3 -u <<'PY' >/dev/null 2>&1 &
|
|
17
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
18
|
+
import json, os
|
|
19
|
+
LOG = os.environ['STUB_LOG']; PORT = int(os.environ['STUB_PORT'])
|
|
20
|
+
class H(BaseHTTPRequestHandler):
|
|
21
|
+
def reply(self, code, body):
|
|
22
|
+
self.send_response(code); self.send_header('Content-Type', 'application/json'); self.end_headers()
|
|
23
|
+
self.wfile.write(json.dumps(body).encode())
|
|
24
|
+
def do_POST(self):
|
|
25
|
+
body = json.loads(self.rfile.read(int(self.headers.get('Content-Length', 0))) or b'{}')
|
|
26
|
+
open(LOG, 'w').write(json.dumps({'method': 'POST', 'path': self.path, 'body': body, 'agent': self.headers.get('X-Agent-Session')}))
|
|
27
|
+
if body.get('chatId') == 'unknown@s.whatsapp.net':
|
|
28
|
+
return self.reply(404, {'success': False, 'code': 'chat_not_found', 'error': 'Unknown chat: unknown@s.whatsapp.net'})
|
|
29
|
+
self.reply(201, {'success': True, 'data': {'id': 'd-1', 'code': 'W12', 'seq': 12, 'chatId': body['chatId'], 'text': body['text'],
|
|
30
|
+
'status': 'pending', 'recipient': 'Ann', 'instruction': 'server says'}})
|
|
31
|
+
def log_message(self, *a, **k): pass
|
|
32
|
+
HTTPServer(('127.0.0.1', PORT), H).serve_forever()
|
|
33
|
+
PY
|
|
34
|
+
STUB_PID=$!
|
|
35
|
+
disown "$STUB_PID" 2>/dev/null || true
|
|
36
|
+
TMP_TEXT="$(mktemp)"
|
|
37
|
+
trap 'kill "$STUB_PID" >/dev/null 2>&1; rm -f "$STUB_LOG" "$TMP_TEXT"' EXIT
|
|
38
|
+
for i in $(seq 1 30); do curl -s -o /dev/null -m 0.2 "http://127.0.0.1:${PORT}/" 2>/dev/null && break; sleep 0.1; done
|
|
39
|
+
|
|
40
|
+
run() { CREWLY_SESSION_NAME=test-agent CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" "$@" 2>/dev/null; }
|
|
41
|
+
run_err() { CREWLY_SESSION_NAME=test-agent CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" "$@" 2>&1 >/dev/null | jq -r '.error' 2>/dev/null; }
|
|
42
|
+
|
|
43
|
+
OUT=$(run --chat 491@s.whatsapp.net --text 'Yes, 8 works — see you "there"')
|
|
44
|
+
check "flags: path" "$(jq -r .path "$STUB_LOG")" "/api/whatsapp/drafts"
|
|
45
|
+
check "flags: body" "$(jq -c .body "$STUB_LOG")" '{"chatId":"491@s.whatsapp.net","text":"Yes, 8 works — see you \"there\""}'
|
|
46
|
+
check "flags: agent header" "$(jq -r .agent "$STUB_LOG")" "test-agent"
|
|
47
|
+
check "output: not sent" "$(printf '%s' "$OUT" | jq -r .sent)" "false"
|
|
48
|
+
check "output: draft" "$(printf '%s' "$OUT" | jq -c .draft)" '{"id":"d-1","code":"W12","recipient":"Ann","chatId":"491@s.whatsapp.net","text":"Yes, 8 works — see you \"there\""}'
|
|
49
|
+
NEXT=$(printf '%s' "$OUT" | jq -r .nextStep)
|
|
50
|
+
[[ "$NEXT" == *"NOT SENT"* && "$NEXT" == *"Ann"* && "$NEXT" == *"「发 W12」"* ]] && OK=yes || OK=no
|
|
51
|
+
check "output: nextStep tells the agent to ask the owner for 「发 W12」" "$OK" "yes"
|
|
52
|
+
|
|
53
|
+
printf 'line one\nline two' > "$TMP_TEXT"
|
|
54
|
+
run --chat 491@s.whatsapp.net --text-file "$TMP_TEXT" >/dev/null
|
|
55
|
+
check "text-file: body text" "$(jq -r .body.text "$STUB_LOG")" "$(printf 'line one\nline two')"
|
|
56
|
+
|
|
57
|
+
run '{"chatId":"491@s.whatsapp.net","text":"json hi"}' >/dev/null
|
|
58
|
+
check "json: body" "$(jq -c .body "$STUB_LOG")" '{"chatId":"491@s.whatsapp.net","text":"json hi"}'
|
|
59
|
+
|
|
60
|
+
OUT=$(CREWLY_SESSION_NAME=test-agent CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" --chat unknown@s.whatsapp.net --text hi 2>/dev/null); RC=$?
|
|
61
|
+
check "404: exit code" "$RC" "1"
|
|
62
|
+
check "404: reason" "$(printf '%s' "$OUT" | jq -r .reason)" "chat_not_found"
|
|
63
|
+
|
|
64
|
+
check "missing chat" "$(run_err --text hi)" "Missing required parameter: chat (--chat)"
|
|
65
|
+
check "missing text" "$(run_err --chat 491@s.whatsapp.net)" "Missing required parameter: text (--text or --text-file)"
|
|
66
|
+
check "missing text file" "$(run_err --chat x --text-file /nonexistent/file)" "text file not found: /nonexistent/file"
|
|
67
|
+
check "unknown option errors" "$(run_err --send)" "Unknown option: --send"
|
|
68
|
+
|
|
69
|
+
echo "=== Results: $PASS passed, $FAIL failed ==="
|
|
70
|
+
[ "$FAIL" -eq 0 ]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: WhatsApp Inbox
|
|
3
|
+
description: List the owner's WhatsApp chats that need a reply (last message not from the owner), with how many messages are unanswered. Read-only. 1:1 chats by default; groups are low priority.
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
category: communication
|
|
6
|
+
skillType: claude-skill
|
|
7
|
+
assignableRoles:
|
|
8
|
+
- orchestrator
|
|
9
|
+
- team-leader
|
|
10
|
+
- generalist
|
|
11
|
+
- operations
|
|
12
|
+
- ops
|
|
13
|
+
- support
|
|
14
|
+
- sales
|
|
15
|
+
triggers:
|
|
16
|
+
- whatsapp inbox
|
|
17
|
+
- whatsapp messages to reply
|
|
18
|
+
- who needs a reply on whatsapp
|
|
19
|
+
- unread whatsapp
|
|
20
|
+
tags:
|
|
21
|
+
- whatsapp
|
|
22
|
+
- inbox
|
|
23
|
+
execution:
|
|
24
|
+
type: script
|
|
25
|
+
script:
|
|
26
|
+
file: execute.sh
|
|
27
|
+
interpreter: bash
|
|
28
|
+
timeoutMs: 30000
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
# WhatsApp Inbox
|
|
32
|
+
|
|
33
|
+
Which of the owner's WhatsApp chats are waiting on them.
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
bash execute.sh # 1:1 chats needing a reply, newest first
|
|
37
|
+
bash execute.sh --limit 50
|
|
38
|
+
bash execute.sh --include-groups # groups too (noisy, low priority)
|
|
39
|
+
bash execute.sh '{"limit":20,"includeGroups":true}'
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Rules (owner decision — apply to every whatsapp-* skill)
|
|
43
|
+
|
|
44
|
+
- **Never send anything without the owner's explicit 「发 <code>」.** You may only
|
|
45
|
+
write drafts (`whatsapp-draft`). Sending is one draft at a time, after the owner
|
|
46
|
+
replies 「发 W12」 in chat or presses 发送 in the dashboard.
|
|
47
|
+
- **Never auto-reply to anyone.** Not even "got it" or "will get back to you".
|
|
48
|
+
- **Summarise; don't paste private chats wholesale** into Slack, team channels,
|
|
49
|
+
the wiki, tasks, or any other place. Quote only the line you need.
|
|
50
|
+
- **Groups are low priority.** Look at 1:1 chats first; only surface a group
|
|
51
|
+
when the owner is addressed directly or something is clearly urgent.
|
|
52
|
+
|
|
53
|
+
## Parameters
|
|
54
|
+
|
|
55
|
+
| Name | Required | Meaning |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| `--limit` (`limit`) | no | Max chats, default 20, max 200 |
|
|
58
|
+
| `--include-groups` (`includeGroups`) | no | Include group chats (default off) |
|
|
59
|
+
|
|
60
|
+
## Output
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{"count":1,"chats":[{"chatId":"4915550001@s.whatsapp.net","name":"Ann","isGroup":false,"unanswered":2,"lastKind":"text","lastFrom":"Ann","lastText":"at 8?","lastAt":"2026-09-24T18:02:11Z"}]}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`unanswered` counts messages from them since the owner last wrote in that chat.
|
|
67
|
+
`lastText` is cut to 200 characters; use `whatsapp-read --chat <chatId>` for the
|
|
68
|
+
conversation. `lastKind` is `text`, `image`, `document`, `audio`, `video`,
|
|
69
|
+
`sticker` or `other` (media is never downloaded — you see the caption only).
|
|
70
|
+
|
|
71
|
+
## Failures
|
|
72
|
+
|
|
73
|
+
`{"success":false,"reason":"…"}` (exit 1). An empty list with `count: 0` just
|
|
74
|
+
means nothing is waiting — or WhatsApp is not connected in inbox mode yet
|
|
75
|
+
(the owner connects it under Settings → WhatsApp).
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# whatsapp-inbox — Which WhatsApp chats need a reply (read-only)
|
|
4
|
+
#
|
|
5
|
+
# Backed by GET /api/whatsapp/inbox?limit=&includeGroups=.
|
|
6
|
+
# A chat "needs a reply" when its newest message is not the owner's.
|
|
7
|
+
# Groups are excluded unless --include-groups is given (they are noisy).
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# bash execute.sh [--limit 20] [--include-groups]
|
|
11
|
+
# bash execute.sh '{"limit":20,"includeGroups":true}'
|
|
12
|
+
# =============================================================================
|
|
13
|
+
set -euo pipefail
|
|
14
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
15
|
+
source "${SCRIPT_DIR}/../../_common/lib.sh"
|
|
16
|
+
|
|
17
|
+
# Characters of each chat's last message shown (keeps the output small).
|
|
18
|
+
PREVIEW_CHARS="${CREWLY_WHATSAPP_PREVIEW_CHARS:-200}"
|
|
19
|
+
|
|
20
|
+
print_usage() {
|
|
21
|
+
cat <<'EOF_USAGE'
|
|
22
|
+
Usage:
|
|
23
|
+
bash execute.sh [--limit 20] [--include-groups]
|
|
24
|
+
bash execute.sh '{"limit":20,"includeGroups":true}'
|
|
25
|
+
|
|
26
|
+
Options:
|
|
27
|
+
--limit Max chats (default 20, max 200)
|
|
28
|
+
--include-groups Also list group chats (default: 1:1 chats only)
|
|
29
|
+
--help | -h Show this help
|
|
30
|
+
EOF_USAGE
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
INPUT_JSON=""
|
|
34
|
+
LIMIT=""; INCLUDE_GROUPS=""
|
|
35
|
+
|
|
36
|
+
if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
|
|
37
|
+
INPUT_JSON="$1"
|
|
38
|
+
shift || true
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
while [[ $# -gt 0 ]]; do
|
|
42
|
+
case "$1" in
|
|
43
|
+
--limit|-n) [ $# -ge 2 ] || error_exit "--limit requires a value"; LIMIT="$2"; shift 2 ;;
|
|
44
|
+
--include-groups) INCLUDE_GROUPS=1; shift ;;
|
|
45
|
+
--help|-h) print_usage; exit 0 ;;
|
|
46
|
+
*) error_exit "Unknown option: $1" ;;
|
|
47
|
+
esac
|
|
48
|
+
done
|
|
49
|
+
|
|
50
|
+
if [ -n "$INPUT_JSON" ]; then
|
|
51
|
+
INPUT=$(read_json_input "$INPUT_JSON")
|
|
52
|
+
[ -z "$LIMIT" ] && LIMIT=$(printf '%s' "$INPUT" | jq -r '.limit // empty')
|
|
53
|
+
[ -z "$INCLUDE_GROUPS" ] && INCLUDE_GROUPS=$(printf '%s' "$INPUT" | jq -r 'if .includeGroups == true then "1" else "" end')
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
QS=""
|
|
57
|
+
[ -n "$LIMIT" ] && QS="limit=$(jq -rn --arg v "$LIMIT" '$v|@uri')"
|
|
58
|
+
if [ -n "$INCLUDE_GROUPS" ]; then
|
|
59
|
+
QS="${QS:+${QS}&}includeGroups=true"
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
# stderr is kept apart so lib.sh's session warning cannot pollute a good response.
|
|
63
|
+
ERR_FILE=$(mktemp); trap 'rm -f "$ERR_FILE"' EXIT
|
|
64
|
+
RESPONSE=$(api_call GET "/whatsapp/inbox${QS:+?${QS}}" 2>"$ERR_FILE") || {
|
|
65
|
+
ERR=$(tail -n 1 "$ERR_FILE")
|
|
66
|
+
printf '%s' "$ERR" | jq -c '{success: false, reason: (.details.code // .details.error // .details // .error // "unknown"), message: (.details.error // "")}' 2>/dev/null \
|
|
67
|
+
|| jq -n --arg r "$ERR" '{success: false, reason: $r}'
|
|
68
|
+
exit 1
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if printf '%s' "$RESPONSE" | jq -e '.truncated == true' >/dev/null 2>&1; then
|
|
72
|
+
printf '%s\n' "$RESPONSE"; exit 0
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
printf '%s' "$RESPONSE" | jq -c --argjson n "$PREVIEW_CHARS" '{
|
|
76
|
+
count: (.data | length),
|
|
77
|
+
chats: [.data[] | {
|
|
78
|
+
chatId: .chat.id,
|
|
79
|
+
name: (.chat.name // .chat.id),
|
|
80
|
+
isGroup: .chat.isGroup,
|
|
81
|
+
unanswered: .unansweredCount,
|
|
82
|
+
lastKind: .lastKind,
|
|
83
|
+
lastFrom: .lastSenderName,
|
|
84
|
+
lastText: (.lastText | .[0:$n]),
|
|
85
|
+
lastAt: ((.lastMessageAt / 1000) | floor | todate)
|
|
86
|
+
}]
|
|
87
|
+
}'
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Tests for whatsapp-inbox — run with: bash execute.test.sh </dev/null
|
|
3
|
+
# Spins up a python HTTP stub as the backend, asserts the request path the
|
|
4
|
+
# skill sends and the JSON it prints. Exit 0 on pass.
|
|
5
|
+
set -u
|
|
6
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
+
EXEC="$HERE/execute.sh"
|
|
8
|
+
PASS=0; FAIL=0
|
|
9
|
+
check() {
|
|
10
|
+
local name="$1"; local got="$2"; local want="$3"
|
|
11
|
+
if [ "$got" = "$want" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: $name"; echo " want: $want"; echo " got: $got"; fi
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
PORT=18841
|
|
15
|
+
STUB_LOG="$(mktemp)"; export STUB_LOG; export STUB_PORT="$PORT"
|
|
16
|
+
python3 -u <<'PY' >/dev/null 2>&1 &
|
|
17
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
18
|
+
from urllib.parse import urlparse, parse_qs
|
|
19
|
+
import json, os
|
|
20
|
+
LOG = os.environ['STUB_LOG']; PORT = int(os.environ['STUB_PORT'])
|
|
21
|
+
ENTRY = {'chat': {'id': '491@s.whatsapp.net', 'name': 'Ann', 'isGroup': False, 'lastMessageAt': 1760000000000},
|
|
22
|
+
'unansweredCount': 2, 'lastText': 'x' * 300, 'lastKind': 'text', 'lastSenderName': 'Ann', 'lastMessageAt': 1760000000000}
|
|
23
|
+
class H(BaseHTTPRequestHandler):
|
|
24
|
+
def do_GET(self):
|
|
25
|
+
open(LOG, 'w').write(json.dumps({'method': 'GET', 'path': self.path, 'agent': self.headers.get('X-Agent-Session')}))
|
|
26
|
+
qs = parse_qs(urlparse(self.path).query)
|
|
27
|
+
if qs.get('limit', [''])[0] == '999':
|
|
28
|
+
self.send_response(500); self.send_header('Content-Type', 'application/json'); self.end_headers()
|
|
29
|
+
self.wfile.write(json.dumps({'success': False, 'error': 'db locked'}).encode()); return
|
|
30
|
+
self.send_response(200); self.send_header('Content-Type', 'application/json'); self.end_headers()
|
|
31
|
+
self.wfile.write(json.dumps({'success': True, 'data': [ENTRY]}).encode())
|
|
32
|
+
def log_message(self, *a, **k): pass
|
|
33
|
+
HTTPServer(('127.0.0.1', PORT), H).serve_forever()
|
|
34
|
+
PY
|
|
35
|
+
STUB_PID=$!
|
|
36
|
+
disown "$STUB_PID" 2>/dev/null || true
|
|
37
|
+
trap 'kill "$STUB_PID" >/dev/null 2>&1; rm -f "$STUB_LOG"' EXIT
|
|
38
|
+
for i in $(seq 1 30); do curl -s -o /dev/null -m 0.2 "http://127.0.0.1:${PORT}/" 2>/dev/null && break; sleep 0.1; done
|
|
39
|
+
|
|
40
|
+
run() { CREWLY_SESSION_NAME=test-agent CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" "$@" 2>/dev/null; }
|
|
41
|
+
run_err() { CREWLY_SESSION_NAME=test-agent CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" "$@" 2>&1 >/dev/null | jq -r '.error' 2>/dev/null; }
|
|
42
|
+
|
|
43
|
+
X200=$(printf 'x%.0s' $(seq 1 200))
|
|
44
|
+
WANT='{"count":1,"chats":[{"chatId":"491@s.whatsapp.net","name":"Ann","isGroup":false,"unanswered":2,"lastKind":"text","lastFrom":"Ann","lastText":"'"$X200"'","lastAt":"2025-10-09T08:53:20Z"}]}'
|
|
45
|
+
|
|
46
|
+
OUT=$(run)
|
|
47
|
+
check "defaults: output (preview truncated to 200)" "$OUT" "$WANT"
|
|
48
|
+
check "defaults: path" "$(jq -r .path "$STUB_LOG")" "/api/whatsapp/inbox"
|
|
49
|
+
check "sends the agent session header" "$(jq -r .agent "$STUB_LOG")" "test-agent"
|
|
50
|
+
|
|
51
|
+
run --limit 5 --include-groups >/dev/null
|
|
52
|
+
check "flags: path" "$(jq -r .path "$STUB_LOG")" "/api/whatsapp/inbox?limit=5&includeGroups=true"
|
|
53
|
+
|
|
54
|
+
run '{"limit":7,"includeGroups":true}' >/dev/null
|
|
55
|
+
check "json: path" "$(jq -r .path "$STUB_LOG")" "/api/whatsapp/inbox?limit=7&includeGroups=true"
|
|
56
|
+
|
|
57
|
+
run --include-groups >/dev/null
|
|
58
|
+
check "groups only: path" "$(jq -r .path "$STUB_LOG")" "/api/whatsapp/inbox?includeGroups=true"
|
|
59
|
+
|
|
60
|
+
OUT=$(CREWLY_SESSION_NAME=test-agent CREWLY_API_URL="http://127.0.0.1:${PORT}" bash "$EXEC" --limit 999 2>/dev/null); RC=$?
|
|
61
|
+
check "500: exit code" "$RC" "1"
|
|
62
|
+
check "500: reason" "$(printf '%s' "$OUT" | jq -r .reason)" "db locked"
|
|
63
|
+
|
|
64
|
+
check "unknown option errors" "$(run_err --bogus)" "Unknown option: --bogus"
|
|
65
|
+
check "--limit needs a value" "$(run_err --limit)" "--limit requires a value"
|
|
66
|
+
|
|
67
|
+
echo "=== Results: $PASS passed, $FAIL failed ==="
|
|
68
|
+
[ "$FAIL" -eq 0 ]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: WhatsApp Read
|
|
3
|
+
description: Read one of the owner's WhatsApp chats, search their WhatsApp messages, or list recent chats. Read-only. For summarising and preparing replies — never for forwarding private chats elsewhere.
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
category: communication
|
|
6
|
+
skillType: claude-skill
|
|
7
|
+
assignableRoles:
|
|
8
|
+
- orchestrator
|
|
9
|
+
- team-leader
|
|
10
|
+
- generalist
|
|
11
|
+
- operations
|
|
12
|
+
- ops
|
|
13
|
+
- support
|
|
14
|
+
- sales
|
|
15
|
+
triggers:
|
|
16
|
+
- read whatsapp chat
|
|
17
|
+
- search whatsapp
|
|
18
|
+
- what did they say on whatsapp
|
|
19
|
+
- whatsapp conversation
|
|
20
|
+
tags:
|
|
21
|
+
- whatsapp
|
|
22
|
+
- inbox
|
|
23
|
+
execution:
|
|
24
|
+
type: script
|
|
25
|
+
script:
|
|
26
|
+
file: execute.sh
|
|
27
|
+
interpreter: bash
|
|
28
|
+
timeoutMs: 30000
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
# WhatsApp Read
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
bash execute.sh --chat 4915550001@s.whatsapp.net # newest 50 messages, oldest first
|
|
35
|
+
bash execute.sh --chat 4915550001@s.whatsapp.net --before 1760000000000 # page back (use nextBefore)
|
|
36
|
+
bash execute.sh --q "invoice" # search all chats
|
|
37
|
+
bash execute.sh --chats --q "ann" # find a chat by name
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Rules (owner decision — apply to every whatsapp-* skill)
|
|
41
|
+
|
|
42
|
+
- **Never send anything without the owner's explicit 「发 <code>」.** Reading is
|
|
43
|
+
all this skill does; replies go through `whatsapp-draft`, and only the owner
|
|
44
|
+
releases a draft.
|
|
45
|
+
- **Never auto-reply to anyone.**
|
|
46
|
+
- **Summarise; don't paste private chats wholesale** into Slack, team channels,
|
|
47
|
+
the wiki, tasks, memory or any other place. Tell the owner what a chat is
|
|
48
|
+
about and what is being asked; quote only the line you need.
|
|
49
|
+
- **Groups are low priority.** Skim them only when asked or when the owner is
|
|
50
|
+
addressed directly.
|
|
51
|
+
|
|
52
|
+
## Parameters
|
|
53
|
+
|
|
54
|
+
| Name | Required | Meaning |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| `--chat` (`chatId`) | one of three | Chat JID (from `whatsapp-inbox` or `--chats`) |
|
|
57
|
+
| `--q` (`q`) | one of three | Search text; with `--chats`, filters chat names |
|
|
58
|
+
| `--chats` (`chats: true`) | one of three | List recent chats |
|
|
59
|
+
| `--limit` (`limit`) | no | Messages default 50 (max 500); search 20 (max 200); chats 50 (max 500) |
|
|
60
|
+
| `--before` (`before`) | no | Epoch ms; only older messages (paging) |
|
|
61
|
+
|
|
62
|
+
## Output
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{"chat":{"chatId":"4915550001@s.whatsapp.net","name":"Ann","isGroup":false},
|
|
66
|
+
"messages":[{"at":"2026-09-24T18:01:00Z","fromMe":false,"sender":"Ann","kind":"text","text":"at 8?"},
|
|
67
|
+
{"at":"2026-09-24T18:02:00Z","fromMe":true,"sender":"me","kind":"text","text":"yes"}],
|
|
68
|
+
"nextBefore":null}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`fromMe: true` / `sender: "me"` is the owner. Media shows as its `kind`
|
|
72
|
+
(`image`, `document`, `audio`, `video`, `sticker`, `other`) with the caption or
|
|
73
|
+
file name as `text` — files are never downloaded. `nextBefore` is non-null when
|
|
74
|
+
there may be older messages; pass it as `--before`.
|
|
75
|
+
|
|
76
|
+
Search: `{"count":1,"hits":[{"chatId":"…","chatName":"Ann","at":"…","fromMe":false,"sender":"Ann","kind":"text","text":"…"}]}`
|
|
77
|
+
|
|
78
|
+
## Failures
|
|
79
|
+
|
|
80
|
+
`{"success":false,"reason":"chat_not_found","message":"…"}` (exit 1).
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# whatsapp-read — Read one WhatsApp chat, search messages, or list chats
|
|
4
|
+
#
|
|
5
|
+
# Backed by:
|
|
6
|
+
# GET /api/whatsapp/chats/<chatId>/messages?limit=&before= (--chat)
|
|
7
|
+
# GET /api/whatsapp/search?q=&limit= (--q)
|
|
8
|
+
# GET /api/whatsapp/chats?q=&limit= (--chats)
|
|
9
|
+
# Read-only.
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# bash execute.sh --chat <jid> [--limit 50] [--before <epoch ms>]
|
|
13
|
+
# bash execute.sh --q "dinner" [--limit 20]
|
|
14
|
+
# bash execute.sh --chats [--q "ann"] [--limit 50]
|
|
15
|
+
# bash execute.sh '{"chatId":"…","limit":50}' | '{"q":"dinner"}' | '{"chats":true}'
|
|
16
|
+
# =============================================================================
|
|
17
|
+
set -euo pipefail
|
|
18
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
19
|
+
source "${SCRIPT_DIR}/../../_common/lib.sh"
|
|
20
|
+
|
|
21
|
+
print_usage() {
|
|
22
|
+
cat <<'EOF_USAGE'
|
|
23
|
+
Usage:
|
|
24
|
+
bash execute.sh --chat <jid> [--limit 50] [--before <epoch ms>]
|
|
25
|
+
bash execute.sh --q "text" [--limit 20]
|
|
26
|
+
bash execute.sh --chats [--q "name"] [--limit 50]
|
|
27
|
+
|
|
28
|
+
Options:
|
|
29
|
+
--chat Chat JID to read (from whatsapp-inbox / --chats)
|
|
30
|
+
--before Page back: only messages older than this epoch-ms (use nextBefore)
|
|
31
|
+
--q Search text (with --chats: filter chat names)
|
|
32
|
+
--chats List recent chats instead of messages
|
|
33
|
+
--limit Row cap
|
|
34
|
+
--help | -h Show this help
|
|
35
|
+
EOF_USAGE
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
INPUT_JSON=""
|
|
39
|
+
CHAT=""; Q=""; LIMIT=""; BEFORE=""; LIST_CHATS=""
|
|
40
|
+
|
|
41
|
+
if [[ $# -gt 0 && ${1:0:1} == '{' ]]; then
|
|
42
|
+
INPUT_JSON="$1"
|
|
43
|
+
shift || true
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
while [[ $# -gt 0 ]]; do
|
|
47
|
+
case "$1" in
|
|
48
|
+
--chat|-c) [ $# -ge 2 ] || error_exit "--chat requires a value"; CHAT="$2"; shift 2 ;;
|
|
49
|
+
--q|--query) [ $# -ge 2 ] || error_exit "--q requires a value"; Q="$2"; shift 2 ;;
|
|
50
|
+
--limit|-n) [ $# -ge 2 ] || error_exit "--limit requires a value"; LIMIT="$2"; shift 2 ;;
|
|
51
|
+
--before) [ $# -ge 2 ] || error_exit "--before requires a value"; BEFORE="$2"; shift 2 ;;
|
|
52
|
+
--chats) LIST_CHATS=1; shift ;;
|
|
53
|
+
--help|-h) print_usage; exit 0 ;;
|
|
54
|
+
*) error_exit "Unknown option: $1" ;;
|
|
55
|
+
esac
|
|
56
|
+
done
|
|
57
|
+
|
|
58
|
+
if [ -n "$INPUT_JSON" ]; then
|
|
59
|
+
INPUT=$(read_json_input "$INPUT_JSON")
|
|
60
|
+
[ -z "$CHAT" ] && CHAT=$(printf '%s' "$INPUT" | jq -r '.chatId // .chat // empty')
|
|
61
|
+
[ -z "$Q" ] && Q=$(printf '%s' "$INPUT" | jq -r '.q // .query // empty')
|
|
62
|
+
[ -z "$LIMIT" ] && LIMIT=$(printf '%s' "$INPUT" | jq -r '.limit // empty')
|
|
63
|
+
[ -z "$BEFORE" ] && BEFORE=$(printf '%s' "$INPUT" | jq -r '.before // empty')
|
|
64
|
+
[ -z "$LIST_CHATS" ] && LIST_CHATS=$(printf '%s' "$INPUT" | jq -r 'if .chats == true then "1" else "" end')
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
uri() { jq -rn --arg v "$1" '$v|@uri'; }
|
|
68
|
+
|
|
69
|
+
if [ -n "$LIST_CHATS" ]; then
|
|
70
|
+
MODE=chats
|
|
71
|
+
QS=""
|
|
72
|
+
[ -n "$Q" ] && QS="q=$(uri "$Q")"
|
|
73
|
+
[ -n "$LIMIT" ] && QS="${QS:+${QS}&}limit=$(uri "$LIMIT")"
|
|
74
|
+
ENDPOINT="/whatsapp/chats${QS:+?${QS}}"
|
|
75
|
+
elif [ -n "$CHAT" ]; then
|
|
76
|
+
MODE=chat
|
|
77
|
+
QS=""
|
|
78
|
+
[ -n "$LIMIT" ] && QS="limit=$(uri "$LIMIT")"
|
|
79
|
+
[ -n "$BEFORE" ] && QS="${QS:+${QS}&}before=$(uri "$BEFORE")"
|
|
80
|
+
ENDPOINT="/whatsapp/chats/$(uri "$CHAT")/messages${QS:+?${QS}}"
|
|
81
|
+
elif [ -n "$Q" ]; then
|
|
82
|
+
MODE=search
|
|
83
|
+
QS="q=$(uri "$Q")"
|
|
84
|
+
[ -n "$LIMIT" ] && QS="${QS}&limit=$(uri "$LIMIT")"
|
|
85
|
+
ENDPOINT="/whatsapp/search?${QS}"
|
|
86
|
+
else
|
|
87
|
+
error_exit "Pass --chat <jid>, --q <text>, or --chats"
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
# stderr is kept apart so lib.sh's session warning cannot pollute a good response.
|
|
91
|
+
ERR_FILE=$(mktemp); trap 'rm -f "$ERR_FILE"' EXIT
|
|
92
|
+
RESPONSE=$(api_call GET "$ENDPOINT" 2>"$ERR_FILE") || {
|
|
93
|
+
ERR=$(tail -n 1 "$ERR_FILE")
|
|
94
|
+
printf '%s' "$ERR" | jq -c '{success: false, reason: (.details.code // .details.error // .details // .error // "unknown"), message: (.details.error // "")}' 2>/dev/null \
|
|
95
|
+
|| jq -n --arg r "$ERR" '{success: false, reason: $r}'
|
|
96
|
+
exit 1
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if printf '%s' "$RESPONSE" | jq -e '.truncated == true' >/dev/null 2>&1; then
|
|
100
|
+
printf '%s\n' "$RESPONSE"; exit 0
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
# One compact line per message: who, when, what.
|
|
104
|
+
MSG_SHAPE='{at: ((.ts / 1000) | floor | todate), fromMe: .fromMe, sender: (if .fromMe then "me" else (.senderName // .senderJid) end), kind: .kind, text: .text}'
|
|
105
|
+
|
|
106
|
+
case "$MODE" in
|
|
107
|
+
chats)
|
|
108
|
+
printf '%s' "$RESPONSE" | jq -c '{count: (.data | length), chats: [.data[] | {chatId: .id, name: (.name // .id), isGroup: .isGroup, lastAt: (if .lastMessageAt then ((.lastMessageAt / 1000) | floor | todate) else null end)}]}'
|
|
109
|
+
;;
|
|
110
|
+
chat)
|
|
111
|
+
printf '%s' "$RESPONSE" | jq -c "{chat: {chatId: .data.chat.id, name: (.data.chat.name // .data.chat.id), isGroup: .data.chat.isGroup}, messages: [.data.messages[] | ${MSG_SHAPE}], nextBefore: .data.nextBefore}"
|
|
112
|
+
;;
|
|
113
|
+
search)
|
|
114
|
+
printf '%s' "$RESPONSE" | jq -c "{count: (.data | length), hits: [.data[] | ({chatId: .chatId, chatName: (.chatName // .chatId)} + ${MSG_SHAPE})]}"
|
|
115
|
+
;;
|
|
116
|
+
esac
|