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 CHANGED
@@ -1,4 +1,4 @@
1
- # @openclaw/threema
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 v0.30+ with channel plugin support
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
- Please keep in mind that the use of the Threema Gateway is not for free.
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 (when published)
28
- npm install @openclaw/threema
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., via `pip install openai-whisper` or Homebrew).
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.5.2";
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 to OpenClaw via enqueueSystemEvent
1786
- const enqueue = runtime?.system?.enqueueSystemEvent;
1787
- if (enqueue) {
1788
- const envelope = `[Threema message from ${senderLabel} (${from})]\n${decrypted.text}`;
1789
- enqueue(envelope, {
1790
- sessionKey: "agent:main:main",
1791
- deliveryContext: {
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
- // Wake the agent
1801
- wakeAgent(config);
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
- api.logger?.warn?.("enqueueSystemEvent not available");
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
@@ -2,7 +2,7 @@
2
2
  "id": "threema",
3
3
  "name": "Threema Gateway",
4
4
  "description": "Threema messaging channel via Threema Gateway API (E2E encrypted)",
5
- "version": "0.5.1",
5
+ "version": "0.6.0",
6
6
  "channels": [
7
7
  "threema"
8
8
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-threema",
3
- "version": "0.5.2",
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",