openclaw-threema 0.5.1 → 0.6.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 +31 -12
- package/index.ts +230 -30
- package/openclaw.plugin.json +1 -1
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# openclaw-threema
|
|
2
2
|
|
|
3
3
|
Threema Gateway channel plugin for [OpenClaw](https://github.com/openclaw/openclaw) — privacy-focused E2E encrypted messaging via the [Threema Gateway API](https://gateway.threema.ch/).
|
|
4
4
|
|
|
@@ -8,24 +8,25 @@ Threema Gateway channel plugin for [OpenClaw](https://github.com/openclaw/opencl
|
|
|
8
8
|
- **Media send/receive** — images, files, audio (E2E encrypted blobs)
|
|
9
9
|
- **Voice transcription** — automatic speech-to-text via local Whisper
|
|
10
10
|
- **Instant wake** — webhook-based message delivery (no polling)
|
|
11
|
+
- **Slash commands** — `/status`, `/compact`, etc. work natively via Threema (v0.6.0+)
|
|
11
12
|
- **CLI tools** — `openclaw threema send|send-file|status|keygen`
|
|
12
13
|
|
|
13
14
|
## Requirements
|
|
14
15
|
|
|
15
16
|
- A [Threema Gateway](https://gateway.threema.ch/) account (E2E mode)
|
|
16
|
-
- OpenClaw
|
|
17
|
+
- OpenClaw 2026.3.2+ with channel plugin support
|
|
17
18
|
- For voice transcription: [OpenAI Whisper](https://github.com/openai/whisper) installed locally
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
At the time of writing these lines you have to pay 1.600 "Credits" to get an ID.
|
|
21
|
-
Every Message costs another Credit (roughly EUR 0,02).
|
|
22
|
-
2.500 Credits are about EUR 55,00
|
|
20
|
+
> **Note:** Threema Gateway usage is not free. At the time of writing, you need ~1,600 credits to register a Gateway ID, and each message costs 1 credit (~€0.02). 2,500 credits cost approximately €55.
|
|
23
21
|
|
|
24
22
|
## Installation
|
|
25
23
|
|
|
26
24
|
```bash
|
|
27
|
-
# From npm
|
|
28
|
-
npm install
|
|
25
|
+
# From npm
|
|
26
|
+
npm install openclaw-threema
|
|
27
|
+
|
|
28
|
+
# From ClawHub
|
|
29
|
+
openclaw plugins install clawhub:threema
|
|
29
30
|
|
|
30
31
|
# Or as a local extension
|
|
31
32
|
cp -r . ~/.openclaw/extensions/threema/
|
|
@@ -77,7 +78,7 @@ https://your-host:18789/threema/webhook
|
|
|
77
78
|
|
|
78
79
|
The default port is `18789` (OpenClaw Gateway). The path matches `webhookPath` in your config (default: `/threema/webhook`).
|
|
79
80
|
|
|
80
|
-
**Note:** If you're behind a reverse proxy, adjust the URL accordingly. The plugin registers the endpoint at the configured `webhookPath`.
|
|
81
|
+
**Note:** If you're behind a reverse proxy (Cloudflare Tunnel, nginx, etc.), adjust the URL accordingly. The plugin registers the endpoint at the configured `webhookPath`.
|
|
81
82
|
|
|
82
83
|
### 3. Restart OpenClaw
|
|
83
84
|
|
|
@@ -96,7 +97,7 @@ openclaw threema send ABCD1234 "Hello from OpenClaw!"
|
|
|
96
97
|
|
|
97
98
|
| Policy | Description |
|
|
98
99
|
|--------|-------------|
|
|
99
|
-
| `allowlist` | Only IDs in `allowFrom` array (default) |
|
|
100
|
+
| `allowlist` | Only IDs in `allowFrom` array (default, recommended) |
|
|
100
101
|
| `open` | Accept from anyone |
|
|
101
102
|
| `disabled` | Reject all DMs |
|
|
102
103
|
|
|
@@ -104,7 +105,7 @@ openclaw threema send ABCD1234 "Hello from OpenClaw!"
|
|
|
104
105
|
|
|
105
106
|
When a voice message is received, the plugin automatically transcribes it using local Whisper (no API key needed). The transcription is included in the message delivered to the agent.
|
|
106
107
|
|
|
107
|
-
Whisper must be installed and accessible in PATH (e.g.,
|
|
108
|
+
Whisper must be installed and accessible in PATH (e.g., `pip install openai-whisper`).
|
|
108
109
|
|
|
109
110
|
## Message Types Supported
|
|
110
111
|
|
|
@@ -119,8 +120,26 @@ Whisper must be installed and accessible in PATH (e.g., via `pip install openai-
|
|
|
119
120
|
- Private keys never leave the host
|
|
120
121
|
- Webhook verification via HMAC-SHA256 (mandatory, verified before decryption)
|
|
121
122
|
- SSRF protection: redirect blocking, DNS rebinding checks, private IP filtering
|
|
123
|
+
- Secrets are redacted in all log output
|
|
124
|
+
|
|
125
|
+
## Changelog
|
|
126
|
+
|
|
127
|
+
### v0.6.0 (2026-04-17)
|
|
128
|
+
- **Channel Inbound Pipeline** — messages now go through OpenClaw's native channel pipeline instead of raw `enqueueSystemEvent`. This enables slash commands (`/status`, `/compact`, etc.) directly from Threema.
|
|
129
|
+
- **Graceful fallback** — automatically falls back to `enqueueSystemEvent` if the channel pipeline is unavailable (e.g., older OpenClaw versions).
|
|
130
|
+
- **Long message chunking** — replies exceeding 3,500 chars are split at newline boundaries.
|
|
131
|
+
|
|
132
|
+
### v0.5.2 (2026-03-30)
|
|
133
|
+
- OpenClaw 2026.3.2 compatibility fixes
|
|
134
|
+
- Health monitor improvements
|
|
135
|
+
- ClawHub publishing support (`compat.pluginApi`, `build.openclawVersion`)
|
|
136
|
+
|
|
137
|
+
### v0.4.5 (2026-02-17)
|
|
138
|
+
- Initial npm release
|
|
139
|
+
- Full E2E text + media support
|
|
140
|
+
- Voice transcription via Whisper
|
|
141
|
+
- Security hardening (SSRF protection, path restrictions, PII log reduction)
|
|
122
142
|
|
|
123
143
|
## License
|
|
124
144
|
|
|
125
145
|
MIT
|
|
126
|
-
|
package/index.ts
CHANGED
|
@@ -1109,6 +1109,28 @@ function getMimeFromPath(filePath: string): string {
|
|
|
1109
1109
|
// Channel Plugin Definition
|
|
1110
1110
|
// ============================================================================
|
|
1111
1111
|
|
|
1112
|
+
// Shared status updater — allows the webhook handler (registered outside the
|
|
1113
|
+
// channel adapter) to update the channel's health-monitor status on inbound events.
|
|
1114
|
+
// startAccount populates this; the webhook handler calls updateActivity().
|
|
1115
|
+
const channelStatus = {
|
|
1116
|
+
_getStatus: null as (() => ChannelAccountSnapshot) | null,
|
|
1117
|
+
_setStatus: null as ((s: ChannelAccountSnapshot) => void) | null,
|
|
1118
|
+
bind(getStatus: () => ChannelAccountSnapshot, setStatus: (s: ChannelAccountSnapshot) => void) {
|
|
1119
|
+
this._getStatus = getStatus;
|
|
1120
|
+
this._setStatus = setStatus;
|
|
1121
|
+
},
|
|
1122
|
+
updateActivity() {
|
|
1123
|
+
if (this._getStatus && this._setStatus) {
|
|
1124
|
+
const now = Date.now();
|
|
1125
|
+
this._setStatus({
|
|
1126
|
+
...this._getStatus(),
|
|
1127
|
+
lastEventAt: now,
|
|
1128
|
+
lastInboundAt: now,
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
},
|
|
1132
|
+
};
|
|
1133
|
+
|
|
1112
1134
|
const threemaChannel = {
|
|
1113
1135
|
id: "threema" as const,
|
|
1114
1136
|
|
|
@@ -1549,18 +1571,34 @@ const threemaChannel = {
|
|
|
1549
1571
|
log?.info?.(`E2E public key: ${client.ownPublicKey}`);
|
|
1550
1572
|
}
|
|
1551
1573
|
|
|
1574
|
+
// Bind shared status so the webhook handler can report activity
|
|
1575
|
+
channelStatus.bind(getStatus, setStatus);
|
|
1576
|
+
|
|
1552
1577
|
setStatus({
|
|
1553
1578
|
...getStatus(),
|
|
1554
1579
|
running: true,
|
|
1555
1580
|
connected: true,
|
|
1556
1581
|
lastConnectedAt: Date.now(),
|
|
1582
|
+
lastEventAt: Date.now(),
|
|
1557
1583
|
});
|
|
1558
1584
|
|
|
1585
|
+
// Periodic health heartbeat — update lastEventAt every 15 min so the
|
|
1586
|
+
// health-monitor doesn't think we're stuck (webhook channels are passive).
|
|
1587
|
+
const heartbeatInterval = setInterval(() => {
|
|
1588
|
+
setStatus({
|
|
1589
|
+
...getStatus(),
|
|
1590
|
+
lastEventAt: Date.now(),
|
|
1591
|
+
});
|
|
1592
|
+
}, 15 * 60 * 1000);
|
|
1593
|
+
|
|
1559
1594
|
// Keep the promise alive until abortSignal fires — resolving immediately
|
|
1560
1595
|
// causes the gateway to treat the channel as "exited" and restart it.
|
|
1561
1596
|
await new Promise<void>((resolve) => {
|
|
1562
1597
|
if (abortSignal.aborted) return resolve();
|
|
1563
|
-
abortSignal.addEventListener("abort", () =>
|
|
1598
|
+
abortSignal.addEventListener("abort", () => {
|
|
1599
|
+
clearInterval(heartbeatInterval);
|
|
1600
|
+
resolve();
|
|
1601
|
+
}, { once: true });
|
|
1564
1602
|
});
|
|
1565
1603
|
|
|
1566
1604
|
log?.info?.("Threema Gateway stopped via abort signal");
|
|
@@ -1584,7 +1622,7 @@ const threemaChannel = {
|
|
|
1584
1622
|
|
|
1585
1623
|
export const id = "threema";
|
|
1586
1624
|
export const name = "Threema Gateway";
|
|
1587
|
-
export const version = "0.
|
|
1625
|
+
export const version = "0.6.0";
|
|
1588
1626
|
export const description =
|
|
1589
1627
|
"Threema messaging channel via Threema Gateway API (E2E encrypted, with media support)";
|
|
1590
1628
|
|
|
@@ -1594,6 +1632,12 @@ export default function register(api: any) {
|
|
|
1594
1632
|
const threemaCfg = getThreemaConfig(config);
|
|
1595
1633
|
const runtime = api.runtime;
|
|
1596
1634
|
|
|
1635
|
+
// Store runtime reference for wakeAgent() to use requestHeartbeatNow
|
|
1636
|
+
_runtimeApi = runtime;
|
|
1637
|
+
|
|
1638
|
+
// Debug: check if requestHeartbeatNow is available
|
|
1639
|
+
api.logger?.info?.(`Threema wake API: runtime.system.requestHeartbeatNow=${typeof runtime?.system?.requestHeartbeatNow}`);
|
|
1640
|
+
|
|
1597
1641
|
// Register the channel plugin
|
|
1598
1642
|
api.registerChannel({ plugin: threemaChannel });
|
|
1599
1643
|
api.logger?.info?.("Threema channel plugin registered");
|
|
@@ -1606,17 +1650,16 @@ export default function register(api: any) {
|
|
|
1606
1650
|
const dmPolicy = threemaCfg.dmPolicy ?? "allowlist";
|
|
1607
1651
|
const allowFrom = threemaCfg.allowFrom ?? [];
|
|
1608
1652
|
|
|
1609
|
-
//
|
|
1610
|
-
//
|
|
1611
|
-
//
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1653
|
+
// Webhook handler — Threema's API POSTs without our gateway token,
|
|
1654
|
+
// so we need an unauthenticated route. The webhook has its own MAC-based auth.
|
|
1655
|
+
//
|
|
1656
|
+
// Forward-compatible: use registerHttpRoute with auth:"none" (2026.3.2+),
|
|
1657
|
+
// fall back to registerHttpHandler for 2026.3.1.
|
|
1658
|
+
const webhookHandler = async (req: any, res: any): Promise<void> => {
|
|
1616
1659
|
if (req.method !== "POST") {
|
|
1617
1660
|
res.writeHead(405, { "Content-Type": "text/plain" });
|
|
1618
1661
|
res.end("Method Not Allowed");
|
|
1619
|
-
return
|
|
1662
|
+
return;
|
|
1620
1663
|
}
|
|
1621
1664
|
|
|
1622
1665
|
try {
|
|
@@ -1669,11 +1712,13 @@ export default function register(api: any) {
|
|
|
1669
1712
|
return;
|
|
1670
1713
|
}
|
|
1671
1714
|
|
|
1672
|
-
// 4. Validate date (not older than
|
|
1715
|
+
// 4. Validate date (not older than 60 minutes)
|
|
1716
|
+
// Threema retries delivery on webhook errors, so messages can arrive late.
|
|
1717
|
+
// 60 min is generous enough for retries while still rejecting stale replays.
|
|
1673
1718
|
if (date) {
|
|
1674
1719
|
const msgTimestamp = parseInt(date, 10) * 1000; // Convert to ms
|
|
1675
1720
|
const now = Date.now();
|
|
1676
|
-
const maxAge =
|
|
1721
|
+
const maxAge = 60 * 60 * 1000; // 60 minutes in ms
|
|
1677
1722
|
if (now - msgTimestamp > maxAge) {
|
|
1678
1723
|
api.logger?.warn?.(`Threema webhook: message too old (date=${date})`);
|
|
1679
1724
|
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
@@ -1719,6 +1764,9 @@ export default function register(api: any) {
|
|
|
1719
1764
|
|
|
1720
1765
|
// === PROCESSING PHASE ===
|
|
1721
1766
|
|
|
1767
|
+
// Report activity to health-monitor (prevents "stuck" restarts)
|
|
1768
|
+
channelStatus.updateActivity();
|
|
1769
|
+
|
|
1722
1770
|
// Decrypt the message
|
|
1723
1771
|
const senderPubKey = await client.getPublicKey(from);
|
|
1724
1772
|
const decrypted = client.decryptMessage(
|
|
@@ -1734,25 +1782,138 @@ export default function register(api: any) {
|
|
|
1734
1782
|
api.logger?.info?.("Threema text message received");
|
|
1735
1783
|
api.logger?.debug?.(`Threema text from=${from} messageId=${messageId}`);
|
|
1736
1784
|
|
|
1737
|
-
// Dispatch
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1785
|
+
// Dispatch through the proper Channel Inbound Pipeline
|
|
1786
|
+
// This enables slash-command parsing (/status, /compact, etc.)
|
|
1787
|
+
const channelRuntime = runtime?.channel;
|
|
1788
|
+
if (channelRuntime?.routing?.resolveAgentRoute && channelRuntime?.reply?.finalizeInboundContext && channelRuntime?.reply?.dispatchReplyWithBufferedBlockDispatcher) {
|
|
1789
|
+
try {
|
|
1790
|
+
const currentCfg = runtime.config.loadConfig();
|
|
1791
|
+
|
|
1792
|
+
// 1. Resolve the agent route and session key
|
|
1793
|
+
const route = channelRuntime.routing.resolveAgentRoute({
|
|
1794
|
+
cfg: currentCfg,
|
|
1744
1795
|
channel: "threema",
|
|
1745
|
-
to: from,
|
|
1746
|
-
from: from,
|
|
1747
1796
|
accountId: "default",
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
api.logger?.info?.("Threema message dispatched via enqueueSystemEvent");
|
|
1797
|
+
peer: { kind: "direct", id: from },
|
|
1798
|
+
});
|
|
1751
1799
|
|
|
1752
|
-
|
|
1753
|
-
|
|
1800
|
+
const sessionKey = channelRuntime.routing.buildAgentSessionKey({
|
|
1801
|
+
agentId: route.agentId,
|
|
1802
|
+
channel: "threema",
|
|
1803
|
+
accountId: "default",
|
|
1804
|
+
peer: { kind: "direct", id: from },
|
|
1805
|
+
dmScope: "per-account-channel-peer",
|
|
1806
|
+
});
|
|
1807
|
+
|
|
1808
|
+
// 2. Check if sender is command-authorized (in allowFrom list)
|
|
1809
|
+
const senderAllowed = allowFrom.length === 0 || allowFrom.includes(from);
|
|
1810
|
+
|
|
1811
|
+
// 3. Build the finalized inbound context
|
|
1812
|
+
const msgCtx = channelRuntime.reply.finalizeInboundContext({
|
|
1813
|
+
Body: decrypted.text,
|
|
1814
|
+
RawBody: decrypted.text,
|
|
1815
|
+
CommandBody: decrypted.text,
|
|
1816
|
+
From: `threema:${from}`,
|
|
1817
|
+
To: `threema:${ownGatewayId}`,
|
|
1818
|
+
SessionKey: sessionKey,
|
|
1819
|
+
AccountId: "default",
|
|
1820
|
+
OriginatingChannel: "threema",
|
|
1821
|
+
OriginatingTo: `threema:${from}`,
|
|
1822
|
+
ChatType: "direct" as const,
|
|
1823
|
+
SenderName: senderLabel,
|
|
1824
|
+
SenderId: from,
|
|
1825
|
+
Provider: "threema",
|
|
1826
|
+
Surface: "threema",
|
|
1827
|
+
ConversationLabel: senderLabel || from,
|
|
1828
|
+
Timestamp: Date.now(),
|
|
1829
|
+
CommandAuthorized: senderAllowed,
|
|
1830
|
+
});
|
|
1831
|
+
|
|
1832
|
+
// 4. Dispatch through the full pipeline (command parsing + agent)
|
|
1833
|
+
const replyClient = new ThreemaClient(getThreemaConfig(currentCfg)!);
|
|
1834
|
+
await channelRuntime.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
1835
|
+
ctx: msgCtx,
|
|
1836
|
+
cfg: currentCfg,
|
|
1837
|
+
dispatcherOptions: {
|
|
1838
|
+
deliver: async (payload: any) => {
|
|
1839
|
+
const text = payload.text ?? payload.body;
|
|
1840
|
+
if (!text) return;
|
|
1841
|
+
// Chunk long replies if needed
|
|
1842
|
+
const limit = getThreemaConfig(currentCfg)?.textChunkLimit ?? 3500;
|
|
1843
|
+
if (text.length <= limit) {
|
|
1844
|
+
if (replyClient.isE2EEnabled) {
|
|
1845
|
+
await replyClient.sendE2E(from, text);
|
|
1846
|
+
} else {
|
|
1847
|
+
await replyClient.sendSimple(from, text);
|
|
1848
|
+
}
|
|
1849
|
+
} else {
|
|
1850
|
+
// Split into chunks at newline boundaries
|
|
1851
|
+
const chunks: string[] = [];
|
|
1852
|
+
let remaining = text;
|
|
1853
|
+
while (remaining.length > 0) {
|
|
1854
|
+
if (remaining.length <= limit) {
|
|
1855
|
+
chunks.push(remaining);
|
|
1856
|
+
break;
|
|
1857
|
+
}
|
|
1858
|
+
let splitIdx = remaining.lastIndexOf("\n", limit);
|
|
1859
|
+
if (splitIdx <= 0) splitIdx = limit;
|
|
1860
|
+
chunks.push(remaining.slice(0, splitIdx));
|
|
1861
|
+
remaining = remaining.slice(splitIdx).replace(/^\n/, "");
|
|
1862
|
+
}
|
|
1863
|
+
for (const chunk of chunks) {
|
|
1864
|
+
if (replyClient.isE2EEnabled) {
|
|
1865
|
+
await replyClient.sendE2E(from, chunk);
|
|
1866
|
+
} else {
|
|
1867
|
+
await replyClient.sendSimple(from, chunk);
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
},
|
|
1872
|
+
onReplyStart: () => {
|
|
1873
|
+
api.logger?.info?.(`Threema: agent reply started for ${from}`);
|
|
1874
|
+
},
|
|
1875
|
+
},
|
|
1876
|
+
});
|
|
1877
|
+
api.logger?.info?.("Threema message dispatched via Channel Inbound Pipeline");
|
|
1878
|
+
} catch (pipelineErr: any) {
|
|
1879
|
+
api.logger?.error?.(`Threema inbound pipeline error: ${pipelineErr.message}`);
|
|
1880
|
+
// Fallback to enqueueSystemEvent if pipeline fails
|
|
1881
|
+
const enqueue = runtime?.system?.enqueueSystemEvent;
|
|
1882
|
+
if (enqueue) {
|
|
1883
|
+
const envelope = `[Threema message from ${senderLabel} (${from})]\n${decrypted.text}`;
|
|
1884
|
+
enqueue(envelope, {
|
|
1885
|
+
sessionKey: "agent:main:main",
|
|
1886
|
+
deliveryContext: {
|
|
1887
|
+
channel: "threema",
|
|
1888
|
+
to: from,
|
|
1889
|
+
from: from,
|
|
1890
|
+
accountId: "default",
|
|
1891
|
+
},
|
|
1892
|
+
});
|
|
1893
|
+
api.logger?.info?.("Threema message dispatched via enqueueSystemEvent (fallback)");
|
|
1894
|
+
wakeAgent(config);
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1754
1897
|
} else {
|
|
1755
|
-
|
|
1898
|
+
// Fallback: use enqueueSystemEvent if channel runtime not available
|
|
1899
|
+
api.logger?.warn?.("Channel inbound pipeline not available, falling back to enqueueSystemEvent");
|
|
1900
|
+
const enqueue = runtime?.system?.enqueueSystemEvent;
|
|
1901
|
+
if (enqueue) {
|
|
1902
|
+
const envelope = `[Threema message from ${senderLabel} (${from})]\n${decrypted.text}`;
|
|
1903
|
+
enqueue(envelope, {
|
|
1904
|
+
sessionKey: "agent:main:main",
|
|
1905
|
+
deliveryContext: {
|
|
1906
|
+
channel: "threema",
|
|
1907
|
+
to: from,
|
|
1908
|
+
from: from,
|
|
1909
|
+
accountId: "default",
|
|
1910
|
+
},
|
|
1911
|
+
});
|
|
1912
|
+
api.logger?.info?.("Threema message dispatched via enqueueSystemEvent (legacy)");
|
|
1913
|
+
wakeAgent(config);
|
|
1914
|
+
} else {
|
|
1915
|
+
api.logger?.warn?.("Neither channel pipeline nor enqueueSystemEvent available");
|
|
1916
|
+
}
|
|
1756
1917
|
}
|
|
1757
1918
|
} else if (decrypted?.type === 0x17 && decrypted?.fileMessage) {
|
|
1758
1919
|
// File message - PII only on debug level
|
|
@@ -1834,9 +1995,37 @@ export default function register(api: any) {
|
|
|
1834
1995
|
res.end("Internal Server Error");
|
|
1835
1996
|
}
|
|
1836
1997
|
}
|
|
1998
|
+
return;
|
|
1999
|
+
};
|
|
2000
|
+
|
|
2001
|
+
// Register the webhook — Threema's callback server POSTs without our
|
|
2002
|
+
// gateway auth token, so we need an unauthenticated route.
|
|
2003
|
+
//
|
|
2004
|
+
// Strategy: prefer registerHttpHandler (available in 2026.3.1, removed in 2026.3.2)
|
|
2005
|
+
// because it bypasses gateway auth. Fall back to registerHttpRoute with auth:"none"
|
|
2006
|
+
// (supported from 2026.3.2+). The order matters: registerHttpRoute in 2026.3.1
|
|
2007
|
+
// silently ignores auth:"none" and applies gateway auth, breaking external webhooks.
|
|
2008
|
+
if (api.registerHttpHandler) {
|
|
2009
|
+
// 2026.3.1: registerHttpHandler with manual path matching (no gateway auth)
|
|
2010
|
+
api.registerHttpHandler(async (req: any, res: any): Promise<boolean> => {
|
|
2011
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
2012
|
+
if (url.pathname !== webhookPath) return false;
|
|
2013
|
+
await webhookHandler(req, res);
|
|
1837
2014
|
return true;
|
|
1838
|
-
|
|
1839
|
-
|
|
2015
|
+
});
|
|
2016
|
+
api.logger?.info?.(`Threema webhook registered via registerHttpHandler at ${webhookPath}`);
|
|
2017
|
+
} else if (api.registerHttpRoute) {
|
|
2018
|
+
// 2026.3.2+: registerHttpHandler removed, use registerHttpRoute with auth:"plugin"
|
|
2019
|
+
// auth:"plugin" = no gateway auth, plugin handles its own auth (MAC verification)
|
|
2020
|
+
api.registerHttpRoute({
|
|
2021
|
+
path: webhookPath,
|
|
2022
|
+
auth: "plugin",
|
|
2023
|
+
handler: webhookHandler,
|
|
2024
|
+
});
|
|
2025
|
+
api.logger?.info?.(`Threema webhook registered via registerHttpRoute (auth:plugin) at ${webhookPath}`);
|
|
2026
|
+
} else {
|
|
2027
|
+
api.logger?.error?.("No HTTP registration method available — Threema webhook NOT registered!");
|
|
2028
|
+
}
|
|
1840
2029
|
}
|
|
1841
2030
|
|
|
1842
2031
|
// Register CLI commands
|
|
@@ -1962,10 +2151,21 @@ export default function register(api: any) {
|
|
|
1962
2151
|
/**
|
|
1963
2152
|
* Wake the agent to process new messages
|
|
1964
2153
|
*/
|
|
2154
|
+
// Shared reference to runtime API — set during register(), used by wakeAgent()
|
|
2155
|
+
let _runtimeApi: any = null;
|
|
2156
|
+
|
|
1965
2157
|
function wakeAgent(config: any) {
|
|
2158
|
+
// Prefer the new requestHeartbeatNow API (2026.3.2+) — direct, no HTTP roundtrip
|
|
2159
|
+
if (_runtimeApi?.system?.requestHeartbeatNow) {
|
|
2160
|
+
try {
|
|
2161
|
+
_runtimeApi.system.requestHeartbeatNow({ sessionKey: "agent:main:main" });
|
|
2162
|
+
return;
|
|
2163
|
+
} catch {}
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
// Fallback: HTTP wake call for older versions
|
|
1966
2167
|
const gatewayPort = config?.gateway?.port || 18789;
|
|
1967
2168
|
|
|
1968
|
-
// Try ENV first, fallback to config file (security: ENV is preferred)
|
|
1969
2169
|
let hooksToken: string | undefined = process.env.OPENCLAW_HOOKS_TOKEN;
|
|
1970
2170
|
|
|
1971
2171
|
if (!hooksToken) {
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-threema",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Threema Gateway channel plugin for OpenClaw",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -19,7 +19,13 @@
|
|
|
19
19
|
"threema-gateway"
|
|
20
20
|
]
|
|
21
21
|
},
|
|
22
|
-
"id": "threema"
|
|
22
|
+
"id": "threema",
|
|
23
|
+
"compat": {
|
|
24
|
+
"pluginApi": ">=1.0.0"
|
|
25
|
+
},
|
|
26
|
+
"build": {
|
|
27
|
+
"openclawVersion": "2026.3.28"
|
|
28
|
+
}
|
|
23
29
|
},
|
|
24
30
|
"dependencies": {
|
|
25
31
|
"tweetnacl": "^1.0.3",
|