@laburen/openclaw-plugin-whatsapp-api 0.2.2 → 0.3.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 +8 -0
- package/index.d.ts +8 -3
- package/index.js +135 -7
- package/package.json +2 -1
- package/src/scripts/check.sh +46 -0
- package/src/scripts/setup.sh +49 -0
package/README.md
CHANGED
|
@@ -10,6 +10,14 @@ openclaw plugins install @laburen/openclaw-plugin-whatsapp-api
|
|
|
10
10
|
|
|
11
11
|
If you install from a local folder instead, copy the package into your OpenClaw extensions root and enable it in config (same plugin id: `whatsapp-api`).
|
|
12
12
|
|
|
13
|
+
## Context window reminder (cron scripts)
|
|
14
|
+
|
|
15
|
+
The plugin stores the last inbound message so **`check.sh`** can remind the user before Meta’s 24-hour window ends. Run **`setup.sh`** by path (no `cd` needed—the script resolves `check.sh` next to itself):
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bash ~/.openclaw/extensions/whatsapp-api/src/scripts/setup.sh
|
|
19
|
+
```
|
|
20
|
+
|
|
13
21
|
## Setup
|
|
14
22
|
|
|
15
23
|
1. In [Meta for Developers](https://developers.facebook.com/), create or open an app with **WhatsApp** product enabled and note your **Phone number ID** and a long-lived **System User** or **Temporary** access token with `whatsapp_business_messaging` (and webhook permissions as required by your setup).
|
package/index.d.ts
CHANGED
|
@@ -2,16 +2,21 @@ import { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
|
2
2
|
|
|
3
3
|
//#region index.d.ts
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* OpenClaw plugin metadata and registration hook. Default export of this package.
|
|
6
|
+
*
|
|
7
|
+
* Channel behaviour is implemented by {@link createWhatsAppApiChannel}; hooks and
|
|
8
|
+
* services are aggregated via {@link registerAllPluginHooks} and
|
|
9
|
+
* {@link registerAllPluginServices}.
|
|
6
10
|
*/
|
|
7
11
|
declare const plugin: {
|
|
8
12
|
id: string;
|
|
9
13
|
name: string;
|
|
10
14
|
description: string;
|
|
11
15
|
/**
|
|
12
|
-
*
|
|
16
|
+
* Called by OpenClaw when the plugin loads: publishes the channel, wires the
|
|
17
|
+
* global plugin API holder, then registers pipeline hooks and plugin services.
|
|
13
18
|
*
|
|
14
|
-
* @param api -
|
|
19
|
+
* @param api - Plugin API injected by the host (config, runtime, registration helpers)
|
|
15
20
|
*/
|
|
16
21
|
register(api: OpenClawPluginApi): void;
|
|
17
22
|
};
|
package/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
1
|
+
import fs, { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
//#region src/core/accounts.ts
|
|
@@ -1079,7 +1079,7 @@ async function handleWhatsAppApiWebhook(params) {
|
|
|
1079
1079
|
//#endregion
|
|
1080
1080
|
//#region src/channel/plugin.ts
|
|
1081
1081
|
const CHANNEL_ID = "whatsapp-api";
|
|
1082
|
-
const log$
|
|
1082
|
+
const log$3 = waLogger("channel");
|
|
1083
1083
|
const DEFAULT_ACCOUNT_ID = "default";
|
|
1084
1084
|
/**
|
|
1085
1085
|
* Resolves when `signal` aborts, or never if `signal` is undefined.
|
|
@@ -1148,7 +1148,7 @@ async function dispatchInboundToAgent(params) {
|
|
|
1148
1148
|
const mediaAttachment = await downloadInboundMediaAttachment({
|
|
1149
1149
|
account,
|
|
1150
1150
|
message,
|
|
1151
|
-
log: { warn: (msg) => log$
|
|
1151
|
+
log: { warn: (msg) => log$3.warn(msg) }
|
|
1152
1152
|
});
|
|
1153
1153
|
const replyApi = rt.channel.reply;
|
|
1154
1154
|
const ctxPayload = replyApi.finalizeInboundContext({
|
|
@@ -1188,17 +1188,17 @@ async function dispatchInboundToAgent(params) {
|
|
|
1188
1188
|
account
|
|
1189
1189
|
});
|
|
1190
1190
|
if (sent.length === 0) {
|
|
1191
|
-
log$
|
|
1191
|
+
log$3.info("skipping outbound: reply payload empty", { messageId: message.messageId });
|
|
1192
1192
|
return;
|
|
1193
1193
|
}
|
|
1194
|
-
log$
|
|
1194
|
+
log$3.info(`sent ${sent.length} outbound message(s)`, {
|
|
1195
1195
|
to: message.from,
|
|
1196
1196
|
messageId: message.messageId
|
|
1197
1197
|
});
|
|
1198
1198
|
onOutbound?.();
|
|
1199
1199
|
},
|
|
1200
1200
|
onError: (err, info) => {
|
|
1201
|
-
log$
|
|
1201
|
+
log$3.error(`${info?.kind ?? "reply"} dispatch failed`, {
|
|
1202
1202
|
accountId,
|
|
1203
1203
|
messageId: message.messageId,
|
|
1204
1204
|
error: String(err)
|
|
@@ -1335,10 +1335,136 @@ function createWhatsAppApiChannel(api) {
|
|
|
1335
1335
|
};
|
|
1336
1336
|
}
|
|
1337
1337
|
//#endregion
|
|
1338
|
+
//#region src/plugin/plugin-state-dir.ts
|
|
1339
|
+
/**
|
|
1340
|
+
* SPDX-License-Identifier: MIT
|
|
1341
|
+
*
|
|
1342
|
+
* OpenClaw provides `ctx.stateDir` only inside a registered service's `start`.
|
|
1343
|
+
* Pipeline hooks (e.g. `message_received`) do not receive that context, so any
|
|
1344
|
+
* hook that must read/write under the plugin state tree needs the same path.
|
|
1345
|
+
*
|
|
1346
|
+
* Only code that actually persists or reads plugin state should import this module.
|
|
1347
|
+
* Other hooks/services stay unaware of it.
|
|
1348
|
+
*/
|
|
1349
|
+
let serviceStateDir = null;
|
|
1350
|
+
/**
|
|
1351
|
+
* @returns The last `stateDir` set by a plugin service `start`, or `null` if
|
|
1352
|
+
* no service has started or after `stop` cleared it.
|
|
1353
|
+
*/
|
|
1354
|
+
function getPluginServiceStateDir() {
|
|
1355
|
+
return serviceStateDir;
|
|
1356
|
+
}
|
|
1357
|
+
/**
|
|
1358
|
+
* Updates the path exposed to hooks. Called from service lifecycle (typically
|
|
1359
|
+
* `start` / `stop` of whichever service owns the OpenClaw service context).
|
|
1360
|
+
*/
|
|
1361
|
+
function setPluginServiceStateDir(dir) {
|
|
1362
|
+
serviceStateDir = dir;
|
|
1363
|
+
}
|
|
1364
|
+
//#endregion
|
|
1365
|
+
//#region src/utils/last-inbound-persistence/store.ts
|
|
1366
|
+
/**
|
|
1367
|
+
* SPDX-License-Identifier: MIT
|
|
1368
|
+
*
|
|
1369
|
+
* Atomic JSON persistence for {@link LastInboundMessageSnapshot} under the
|
|
1370
|
+
* plugin state directory. Shared util for hooks and services.
|
|
1371
|
+
*/
|
|
1372
|
+
function getLastInboundMessagePath(stateDir) {
|
|
1373
|
+
return path.join(stateDir, "whatsapp-api", "last-inbound-message.json");
|
|
1374
|
+
}
|
|
1375
|
+
/**
|
|
1376
|
+
* Persists the last inbound message snapshot (write-to-temp + rename).
|
|
1377
|
+
*/
|
|
1378
|
+
async function writeLastInboundMessage(stateDir, data) {
|
|
1379
|
+
const filePath = getLastInboundMessagePath(stateDir);
|
|
1380
|
+
const dir = path.dirname(filePath);
|
|
1381
|
+
await fs.mkdir(dir, { recursive: true });
|
|
1382
|
+
const tmpPath = `${filePath}.tmp`;
|
|
1383
|
+
await fs.writeFile(tmpPath, JSON.stringify(data, null, 2), "utf8");
|
|
1384
|
+
await fs.rename(tmpPath, filePath);
|
|
1385
|
+
}
|
|
1386
|
+
//#endregion
|
|
1387
|
+
//#region src/hooks/message-received/handlers/persist-last-inbound.ts
|
|
1388
|
+
/**
|
|
1389
|
+
* SPDX-License-Identifier: MIT
|
|
1390
|
+
*
|
|
1391
|
+
* Persist last inbound message snapshot (used by background services).
|
|
1392
|
+
*/
|
|
1393
|
+
const log$2 = waLogger("message-received/persist-last-inbound");
|
|
1394
|
+
async function handlePersistLastInbound(event, ctx) {
|
|
1395
|
+
const stateDir = getPluginServiceStateDir();
|
|
1396
|
+
if (!stateDir) return;
|
|
1397
|
+
const now = Date.now();
|
|
1398
|
+
const inboundTimestamp = typeof event.timestamp === "number" && Number.isFinite(event.timestamp) ? event.timestamp : now;
|
|
1399
|
+
const payload = {
|
|
1400
|
+
from: event.from,
|
|
1401
|
+
content: event.content,
|
|
1402
|
+
timestamp: inboundTimestamp,
|
|
1403
|
+
channelId: ctx.channelId,
|
|
1404
|
+
accountId: ctx.accountId,
|
|
1405
|
+
conversationId: ctx.conversationId,
|
|
1406
|
+
notified: false
|
|
1407
|
+
};
|
|
1408
|
+
try {
|
|
1409
|
+
await writeLastInboundMessage(stateDir, payload);
|
|
1410
|
+
log$2.debug("last inbound message persisted");
|
|
1411
|
+
} catch (error) {
|
|
1412
|
+
log$2.warn(`failed to persist last inbound message: ${String(error)}`);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
//#endregion
|
|
1416
|
+
//#region src/hooks/message-received/register.ts
|
|
1417
|
+
function registerMessageReceivedHook(api) {
|
|
1418
|
+
api.on("message_received", async (event, ctx) => {
|
|
1419
|
+
await handlePersistLastInbound(event, ctx);
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
1422
|
+
//#endregion
|
|
1423
|
+
//#region src/hooks/register.ts
|
|
1424
|
+
/**
|
|
1425
|
+
* Registers all plugin-managed OpenClaw hooks for this extension.
|
|
1426
|
+
*/
|
|
1427
|
+
function registerAllPluginHooks(api) {
|
|
1428
|
+
registerMessageReceivedHook(api);
|
|
1429
|
+
}
|
|
1430
|
+
//#endregion
|
|
1431
|
+
//#region src/services/context-window-check/register.ts
|
|
1432
|
+
const log$1 = waLogger("context-window-check");
|
|
1433
|
+
/**
|
|
1434
|
+
* Registers the `whatsapp-api` plugin service (state dir wiring; see `start` for
|
|
1435
|
+
* the commented in-process poll vs OpenClaw cron + scripts, described in the
|
|
1436
|
+
* module comment above).
|
|
1437
|
+
*/
|
|
1438
|
+
function registerContextWindowCheckService(api) {
|
|
1439
|
+
api.registerService({
|
|
1440
|
+
id: "whatsapp-api",
|
|
1441
|
+
start: async (ctx) => {
|
|
1442
|
+
setPluginServiceStateDir(ctx.stateDir);
|
|
1443
|
+
log$1.info("service started");
|
|
1444
|
+
},
|
|
1445
|
+
stop: async (_ctx) => {
|
|
1446
|
+
setPluginServiceStateDir(null);
|
|
1447
|
+
log$1.info("service stopped");
|
|
1448
|
+
}
|
|
1449
|
+
});
|
|
1450
|
+
}
|
|
1451
|
+
//#endregion
|
|
1452
|
+
//#region src/services/register.ts
|
|
1453
|
+
/**
|
|
1454
|
+
* Registers all plugin-managed OpenClaw services for this extension.
|
|
1455
|
+
*/
|
|
1456
|
+
function registerAllPluginServices(api) {
|
|
1457
|
+
registerContextWindowCheckService(api);
|
|
1458
|
+
}
|
|
1459
|
+
//#endregion
|
|
1338
1460
|
//#region index.ts
|
|
1339
1461
|
const log = waLogger("plugin");
|
|
1340
1462
|
/**
|
|
1341
|
-
*
|
|
1463
|
+
* OpenClaw plugin metadata and registration hook. Default export of this package.
|
|
1464
|
+
*
|
|
1465
|
+
* Channel behaviour is implemented by {@link createWhatsAppApiChannel}; hooks and
|
|
1466
|
+
* services are aggregated via {@link registerAllPluginHooks} and
|
|
1467
|
+
* {@link registerAllPluginServices}.
|
|
1342
1468
|
*/
|
|
1343
1469
|
const plugin = {
|
|
1344
1470
|
id: "whatsapp-api",
|
|
@@ -1348,6 +1474,8 @@ const plugin = {
|
|
|
1348
1474
|
setPluginApi(api);
|
|
1349
1475
|
api.registerChannel({ plugin: createWhatsAppApiChannel(api) });
|
|
1350
1476
|
log.info("plugin registered");
|
|
1477
|
+
registerAllPluginHooks(api);
|
|
1478
|
+
registerAllPluginServices(api);
|
|
1351
1479
|
}
|
|
1352
1480
|
};
|
|
1353
1481
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@laburen/openclaw-plugin-whatsapp-api",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "WhatsApp API channel plugin for OpenClaw",
|
|
6
6
|
"main": "index.js",
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"index.js",
|
|
9
9
|
"index.d.ts",
|
|
10
10
|
"openclaw.plugin.json",
|
|
11
|
+
"src/scripts",
|
|
11
12
|
"README.md",
|
|
12
13
|
"LICENSE"
|
|
13
14
|
],
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# check.sh - WhatsApp context window notifier
|
|
3
|
+
# Reads last-inbound-message.json and sends a warning if the user hasn't
|
|
4
|
+
# written in more than THRESHOLD_MINUTES and notified=false.
|
|
5
|
+
|
|
6
|
+
OPENCLAW_DIR="${OPENCLAW_DIR:-/home/$(whoami)/.openclaw}"
|
|
7
|
+
LAST_MSG_FILE="$OPENCLAW_DIR/whatsapp-api/last-inbound-message.json"
|
|
8
|
+
# 23 hours in minutes
|
|
9
|
+
THRESHOLD_MINUTES="${THRESHOLD_MINUTES:-$((23 * 60))}"
|
|
10
|
+
WARNING_MESSAGE="${WARNING_MESSAGE:-Aviso: la ventana de contexto de WhatsApp esta por vencer. Si quieres seguir recibiendo mensajes, responde este chat para reabrir la ventana de 24 horas.}"
|
|
11
|
+
|
|
12
|
+
if [ ! -f "$LAST_MSG_FILE" ]; then
|
|
13
|
+
echo "[whatsapp-window-notifier] File not found: $LAST_MSG_FILE — skipping."
|
|
14
|
+
exit 0
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
TIMESTAMP=$(jq -r '.timestamp' "$LAST_MSG_FILE")
|
|
18
|
+
NOTIFIED=$(jq -r '.notified' "$LAST_MSG_FILE")
|
|
19
|
+
FROM=$(jq -r '.from' "$LAST_MSG_FILE")
|
|
20
|
+
|
|
21
|
+
# Extract phone number: "whatsapp-api:5493496460785" → "5493496460785"
|
|
22
|
+
PHONE=$(echo "$FROM" | sed 's/.*://')
|
|
23
|
+
|
|
24
|
+
if [ "$NOTIFIED" = "true" ]; then
|
|
25
|
+
echo "[whatsapp-window-notifier] Already notified, skipping."
|
|
26
|
+
exit 0
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
NOW_MS=$(date +%s%3N)
|
|
30
|
+
ELAPSED_MS=$((NOW_MS - TIMESTAMP))
|
|
31
|
+
ELAPSED_MIN=$((ELAPSED_MS / 60000))
|
|
32
|
+
|
|
33
|
+
echo "[whatsapp-window-notifier] Elapsed: ${ELAPSED_MIN}m / threshold: ${THRESHOLD_MINUTES}m / target: $PHONE"
|
|
34
|
+
|
|
35
|
+
if [ "$ELAPSED_MIN" -ge "$THRESHOLD_MINUTES" ]; then
|
|
36
|
+
openclaw message send \
|
|
37
|
+
--channel whatsapp-api \
|
|
38
|
+
--target "$PHONE" \
|
|
39
|
+
--message "$WARNING_MESSAGE"
|
|
40
|
+
|
|
41
|
+
TMP=$(mktemp)
|
|
42
|
+
jq '.notified = true' "$LAST_MSG_FILE" > "$TMP" && mv "$TMP" "$LAST_MSG_FILE"
|
|
43
|
+
echo "[whatsapp-window-notifier] Notification sent to $PHONE, notified=true set."
|
|
44
|
+
else
|
|
45
|
+
echo "[whatsapp-window-notifier] Not enough time elapsed, no action."
|
|
46
|
+
fi
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# setup.sh - Installs/uninstalls the whatsapp-window-notifier cron job.
|
|
3
|
+
# Run once per OpenClaw instance.
|
|
4
|
+
#
|
|
5
|
+
# Usage:
|
|
6
|
+
# bash setup.sh → installs the cron
|
|
7
|
+
# bash setup.sh --uninstall → removes the cron
|
|
8
|
+
|
|
9
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
10
|
+
CHECK_SCRIPT="$SCRIPT_DIR/check.sh"
|
|
11
|
+
JOB_NAME="whatsapp-window-notifier"
|
|
12
|
+
EVERY="${EVERY:-30m}"
|
|
13
|
+
UNINSTALL=false
|
|
14
|
+
|
|
15
|
+
for arg in "$@"; do
|
|
16
|
+
case $arg in
|
|
17
|
+
--uninstall) UNINSTALL=true ;;
|
|
18
|
+
esac
|
|
19
|
+
done
|
|
20
|
+
|
|
21
|
+
if [ "$UNINSTALL" = "true" ]; then
|
|
22
|
+
JOB_ID=$(openclaw cron list --json 2>/dev/null | jq -r ".jobs[] | select(.name == \"$JOB_NAME\") | .id")
|
|
23
|
+
if [ -n "$JOB_ID" ]; then
|
|
24
|
+
openclaw cron rm "$JOB_ID"
|
|
25
|
+
echo "[$JOB_NAME] Cron job removed (id: $JOB_ID)."
|
|
26
|
+
else
|
|
27
|
+
echo "[$JOB_NAME] No cron job found to remove."
|
|
28
|
+
fi
|
|
29
|
+
exit 0
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
# Check if already installed
|
|
33
|
+
EXISTING=$(openclaw cron list --json 2>/dev/null | jq -r ".jobs[] | select(.name == \"$JOB_NAME\") | .id")
|
|
34
|
+
if [ -n "$EXISTING" ]; then
|
|
35
|
+
echo "[$JOB_NAME] Already installed (id: $EXISTING). Run with --uninstall first to reinstall."
|
|
36
|
+
exit 0
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
chmod +x "$CHECK_SCRIPT"
|
|
40
|
+
|
|
41
|
+
openclaw cron add \
|
|
42
|
+
--name "$JOB_NAME" \
|
|
43
|
+
--every "$EVERY" \
|
|
44
|
+
--session isolated \
|
|
45
|
+
--no-deliver \
|
|
46
|
+
--description "Notifies user when WhatsApp 24h context window is about to expire" \
|
|
47
|
+
--message "Run this bash script using the exec tool and report nothing to the user: $CHECK_SCRIPT"
|
|
48
|
+
|
|
49
|
+
echo "[$JOB_NAME] Cron job installed. Runs every $EVERY."
|