openclaw-threema 0.5.0 → 0.5.2
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/index.ts +102 -15
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
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.5.
|
|
1625
|
+
export const version = "0.5.2";
|
|
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(
|
|
@@ -1834,9 +1882,37 @@ export default function register(api: any) {
|
|
|
1834
1882
|
res.end("Internal Server Error");
|
|
1835
1883
|
}
|
|
1836
1884
|
}
|
|
1885
|
+
return;
|
|
1886
|
+
};
|
|
1887
|
+
|
|
1888
|
+
// Register the webhook — Threema's callback server POSTs without our
|
|
1889
|
+
// gateway auth token, so we need an unauthenticated route.
|
|
1890
|
+
//
|
|
1891
|
+
// Strategy: prefer registerHttpHandler (available in 2026.3.1, removed in 2026.3.2)
|
|
1892
|
+
// because it bypasses gateway auth. Fall back to registerHttpRoute with auth:"none"
|
|
1893
|
+
// (supported from 2026.3.2+). The order matters: registerHttpRoute in 2026.3.1
|
|
1894
|
+
// silently ignores auth:"none" and applies gateway auth, breaking external webhooks.
|
|
1895
|
+
if (api.registerHttpHandler) {
|
|
1896
|
+
// 2026.3.1: registerHttpHandler with manual path matching (no gateway auth)
|
|
1897
|
+
api.registerHttpHandler(async (req: any, res: any): Promise<boolean> => {
|
|
1898
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
1899
|
+
if (url.pathname !== webhookPath) return false;
|
|
1900
|
+
await webhookHandler(req, res);
|
|
1837
1901
|
return true;
|
|
1838
|
-
|
|
1839
|
-
|
|
1902
|
+
});
|
|
1903
|
+
api.logger?.info?.(`Threema webhook registered via registerHttpHandler at ${webhookPath}`);
|
|
1904
|
+
} else if (api.registerHttpRoute) {
|
|
1905
|
+
// 2026.3.2+: registerHttpHandler removed, use registerHttpRoute with auth:"plugin"
|
|
1906
|
+
// auth:"plugin" = no gateway auth, plugin handles its own auth (MAC verification)
|
|
1907
|
+
api.registerHttpRoute({
|
|
1908
|
+
path: webhookPath,
|
|
1909
|
+
auth: "plugin",
|
|
1910
|
+
handler: webhookHandler,
|
|
1911
|
+
});
|
|
1912
|
+
api.logger?.info?.(`Threema webhook registered via registerHttpRoute (auth:plugin) at ${webhookPath}`);
|
|
1913
|
+
} else {
|
|
1914
|
+
api.logger?.error?.("No HTTP registration method available — Threema webhook NOT registered!");
|
|
1915
|
+
}
|
|
1840
1916
|
}
|
|
1841
1917
|
|
|
1842
1918
|
// Register CLI commands
|
|
@@ -1962,10 +2038,21 @@ export default function register(api: any) {
|
|
|
1962
2038
|
/**
|
|
1963
2039
|
* Wake the agent to process new messages
|
|
1964
2040
|
*/
|
|
2041
|
+
// Shared reference to runtime API — set during register(), used by wakeAgent()
|
|
2042
|
+
let _runtimeApi: any = null;
|
|
2043
|
+
|
|
1965
2044
|
function wakeAgent(config: any) {
|
|
2045
|
+
// Prefer the new requestHeartbeatNow API (2026.3.2+) — direct, no HTTP roundtrip
|
|
2046
|
+
if (_runtimeApi?.system?.requestHeartbeatNow) {
|
|
2047
|
+
try {
|
|
2048
|
+
_runtimeApi.system.requestHeartbeatNow({ sessionKey: "agent:main:main" });
|
|
2049
|
+
return;
|
|
2050
|
+
} catch {}
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
// Fallback: HTTP wake call for older versions
|
|
1966
2054
|
const gatewayPort = config?.gateway?.port || 18789;
|
|
1967
2055
|
|
|
1968
|
-
// Try ENV first, fallback to config file (security: ENV is preferred)
|
|
1969
2056
|
let hooksToken: string | undefined = process.env.OPENCLAW_HOOKS_TOKEN;
|
|
1970
2057
|
|
|
1971
2058
|
if (!hooksToken) {
|
package/openclaw.plugin.json
CHANGED