openclaw-threema 0.5.2 → 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 +129 -16
- 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
|
@@ -1622,7 +1622,7 @@ const threemaChannel = {
|
|
|
1622
1622
|
|
|
1623
1623
|
export const id = "threema";
|
|
1624
1624
|
export const name = "Threema Gateway";
|
|
1625
|
-
export const version = "0.
|
|
1625
|
+
export const version = "0.6.0";
|
|
1626
1626
|
export const description =
|
|
1627
1627
|
"Threema messaging channel via Threema Gateway API (E2E encrypted, with media support)";
|
|
1628
1628
|
|
|
@@ -1782,25 +1782,138 @@ export default function register(api: any) {
|
|
|
1782
1782
|
api.logger?.info?.("Threema text message received");
|
|
1783
1783
|
api.logger?.debug?.(`Threema text from=${from} messageId=${messageId}`);
|
|
1784
1784
|
|
|
1785
|
-
// Dispatch
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
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,
|
|
1792
1795
|
channel: "threema",
|
|
1793
|
-
to: from,
|
|
1794
|
-
from: from,
|
|
1795
1796
|
accountId: "default",
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
api.logger?.info?.("Threema message dispatched via enqueueSystemEvent");
|
|
1797
|
+
peer: { kind: "direct", id: from },
|
|
1798
|
+
});
|
|
1799
1799
|
|
|
1800
|
-
|
|
1801
|
-
|
|
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
|
+
}
|
|
1802
1897
|
} else {
|
|
1803
|
-
|
|
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
|
+
}
|
|
1804
1917
|
}
|
|
1805
1918
|
} else if (decrypted?.type === 0x17 && decrypted?.fileMessage) {
|
|
1806
1919
|
// File message - PII only on debug level
|
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",
|