@cloudrise/openclaw-channel-rocketchat 0.1.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/LICENSE +21 -0
- package/README.md +20 -0
- package/index.ts +25 -0
- package/openclaw.plugin.json +12 -0
- package/package.json +31 -0
- package/src/channel.ts +231 -0
- package/src/rocketchat/accounts.ts +128 -0
- package/src/rocketchat/client.ts +227 -0
- package/src/rocketchat/index.ts +5 -0
- package/src/rocketchat/monitor.ts +511 -0
- package/src/rocketchat/realtime.ts +389 -0
- package/src/rocketchat/send.ts +195 -0
- package/src/runtime.ts +25 -0
- package/test-chad.mjs +79 -0
- package/test-chad2.mjs +111 -0
- package/test-realtime.mjs +68 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 cloudrise network
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# @cloudrise/openclaw-channel-rocketchat
|
|
2
|
+
|
|
3
|
+
Rocket.Chat channel plugin for **OpenClaw** (Cloudrise-maintained).
|
|
4
|
+
|
|
5
|
+
- **Inbound:** Rocket.Chat Realtime (DDP/WebSocket) subscribe to `stream-room-messages`
|
|
6
|
+
- **Outbound:** Rocket.Chat REST `chat.postMessage`
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm i @cloudrise/openclaw-channel-rocketchat
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Upgrade / rename notice
|
|
15
|
+
|
|
16
|
+
If you were using the old Clawdbot-era package:
|
|
17
|
+
|
|
18
|
+
- Old: `@cloudrise/clawdbot-channel-rocketchat`
|
|
19
|
+
- New: `@cloudrise/openclaw-channel-rocketchat`
|
|
20
|
+
|
package/index.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rocket.Chat channel plugin for OpenClaw
|
|
3
|
+
*
|
|
4
|
+
* Provides integration with self-hosted Rocket.Chat instances via REST API
|
|
5
|
+
* for sending messages and the Realtime/DDP API for receiving messages.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { OpenclawPluginApi } from "openclaw/plugin-sdk";
|
|
9
|
+
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
10
|
+
|
|
11
|
+
import { rocketChatPlugin } from "./src/channel.js";
|
|
12
|
+
import { setRocketChatRuntime } from "./src/runtime.js";
|
|
13
|
+
|
|
14
|
+
const plugin = {
|
|
15
|
+
id: "rocketchat",
|
|
16
|
+
name: "Rocket.Chat",
|
|
17
|
+
description: "Rocket.Chat channel plugin for OpenClaw",
|
|
18
|
+
configSchema: emptyPluginConfigSchema(),
|
|
19
|
+
register(api: OpenclawPluginApi) {
|
|
20
|
+
setRocketChatRuntime(api.runtime);
|
|
21
|
+
api.registerChannel({ plugin: rocketChatPlugin });
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default plugin;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "rocketchat",
|
|
3
|
+
"name": "Rocket.Chat",
|
|
4
|
+
"description": "Rocket.Chat channel plugin for OpenClaw",
|
|
5
|
+
"version": "0.1.0",
|
|
6
|
+
"channels": ["rocketchat"],
|
|
7
|
+
"configSchema": {
|
|
8
|
+
"type": "object",
|
|
9
|
+
"additionalProperties": false,
|
|
10
|
+
"properties": {}
|
|
11
|
+
}
|
|
12
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cloudrise/openclaw-channel-rocketchat",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Rocket.Chat channel plugin for OpenClaw (Cloudrise)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.ts",
|
|
7
|
+
"openclaw": {
|
|
8
|
+
"extensions": [
|
|
9
|
+
"./index.ts"
|
|
10
|
+
],
|
|
11
|
+
"channel": {
|
|
12
|
+
"id": "rocketchat",
|
|
13
|
+
"label": "Rocket.Chat",
|
|
14
|
+
"selectionLabel": "Rocket.Chat (self-hosted)",
|
|
15
|
+
"docsPath": "/channels/rocketchat",
|
|
16
|
+
"docsLabel": "rocketchat",
|
|
17
|
+
"blurb": "Self-hosted team chat via Rocket.Chat REST + Realtime API.",
|
|
18
|
+
"order": 66,
|
|
19
|
+
"aliases": [
|
|
20
|
+
"rc",
|
|
21
|
+
"rocket"
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"ws": "^8.14.0"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"openclaw": "*"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/channel.ts
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rocket.Chat channel plugin for OpenClaw
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_ACCOUNT_ID,
|
|
7
|
+
type ChannelPlugin,
|
|
8
|
+
} from "openclaw/plugin-sdk";
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
listRocketChatAccountIds,
|
|
12
|
+
resolveDefaultRocketChatAccountId,
|
|
13
|
+
resolveRocketChatAccount,
|
|
14
|
+
type ResolvedRocketChatAccount,
|
|
15
|
+
} from "./rocketchat/accounts.js";
|
|
16
|
+
import { normalizeRocketChatBaseUrl } from "./rocketchat/client.js";
|
|
17
|
+
import { monitorRocketChatProvider } from "./rocketchat/monitor.js";
|
|
18
|
+
import { sendMessageRocketChat } from "./rocketchat/send.js";
|
|
19
|
+
import { getRocketChatRuntime } from "./runtime.js";
|
|
20
|
+
|
|
21
|
+
const meta = {
|
|
22
|
+
id: "rocketchat",
|
|
23
|
+
label: "Rocket.Chat",
|
|
24
|
+
selectionLabel: "Rocket.Chat (plugin)",
|
|
25
|
+
detailLabel: "Rocket.Chat Bot",
|
|
26
|
+
docsPath: "/channels/rocketchat",
|
|
27
|
+
docsLabel: "rocketchat",
|
|
28
|
+
blurb: "Self-hosted team chat via Rocket.Chat REST + Realtime API.",
|
|
29
|
+
systemImage: "bubble.left.and.bubble.right",
|
|
30
|
+
order: 66,
|
|
31
|
+
aliases: ["rc", "rocket"],
|
|
32
|
+
quickstartAllowFrom: true,
|
|
33
|
+
} as const;
|
|
34
|
+
|
|
35
|
+
function normalizeAllowEntry(entry: string): string {
|
|
36
|
+
return entry
|
|
37
|
+
.trim()
|
|
38
|
+
.replace(/^(rocketchat|user):/i, "")
|
|
39
|
+
.replace(/^@/, "")
|
|
40
|
+
.toLowerCase();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function formatAllowEntry(entry: string): string {
|
|
44
|
+
const trimmed = entry.trim();
|
|
45
|
+
if (!trimmed) return "";
|
|
46
|
+
if (trimmed.startsWith("@")) {
|
|
47
|
+
const username = trimmed.slice(1).trim();
|
|
48
|
+
return username ? `@${username.toLowerCase()}` : "";
|
|
49
|
+
}
|
|
50
|
+
return trimmed.replace(/^(rocketchat|user):/i, "").toLowerCase();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function looksLikeRocketChatTargetId(value: string): boolean {
|
|
54
|
+
const trimmed = value.trim();
|
|
55
|
+
// Rocket.Chat room IDs are typically 17-character alphanumeric
|
|
56
|
+
if (/^[A-Za-z0-9]{17}$/.test(trimmed)) return true;
|
|
57
|
+
if (trimmed.startsWith("#") || trimmed.startsWith("@")) return true;
|
|
58
|
+
if (trimmed.toLowerCase().startsWith("room:")) return true;
|
|
59
|
+
if (trimmed.toLowerCase().startsWith("user:")) return true;
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function normalizeRocketChatMessagingTarget(raw: string): string {
|
|
64
|
+
const trimmed = raw.trim();
|
|
65
|
+
if (!trimmed) return "";
|
|
66
|
+
return trimmed;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const rocketChatPlugin: ChannelPlugin<ResolvedRocketChatAccount> = {
|
|
70
|
+
id: "rocketchat",
|
|
71
|
+
meta: {
|
|
72
|
+
...meta,
|
|
73
|
+
},
|
|
74
|
+
pairing: {
|
|
75
|
+
idLabel: "rocketchatUserId",
|
|
76
|
+
normalizeAllowEntry,
|
|
77
|
+
notifyApproval: async ({ id }) => {
|
|
78
|
+
console.log(`[rocketchat] User ${id} approved for pairing`);
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
capabilities: {
|
|
82
|
+
chatTypes: ["direct", "channel", "group", "thread"],
|
|
83
|
+
threads: true,
|
|
84
|
+
media: true,
|
|
85
|
+
},
|
|
86
|
+
streaming: {
|
|
87
|
+
blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1000 },
|
|
88
|
+
},
|
|
89
|
+
reload: { configPrefixes: ["channels.rocketchat"] },
|
|
90
|
+
config: {
|
|
91
|
+
listAccountIds: (cfg) => listRocketChatAccountIds(cfg),
|
|
92
|
+
resolveAccount: (cfg, accountId) => resolveRocketChatAccount({ cfg, accountId }),
|
|
93
|
+
defaultAccountId: (cfg) => resolveDefaultRocketChatAccountId(cfg),
|
|
94
|
+
isConfigured: (account) =>
|
|
95
|
+
Boolean(account.authToken && account.userId && account.baseUrl),
|
|
96
|
+
describeAccount: (account) => ({
|
|
97
|
+
accountId: account.accountId,
|
|
98
|
+
name: account.name,
|
|
99
|
+
enabled: account.enabled,
|
|
100
|
+
configured: Boolean(account.authToken && account.userId && account.baseUrl),
|
|
101
|
+
authTokenSource: account.authTokenSource,
|
|
102
|
+
baseUrl: account.baseUrl,
|
|
103
|
+
}),
|
|
104
|
+
resolveAllowFrom: ({ cfg, accountId }) =>
|
|
105
|
+
(resolveRocketChatAccount({ cfg, accountId }).config.allowFrom ?? []).map(String),
|
|
106
|
+
formatAllowFrom: ({ allowFrom }) =>
|
|
107
|
+
allowFrom.map((entry) => formatAllowEntry(String(entry))).filter(Boolean),
|
|
108
|
+
},
|
|
109
|
+
security: {
|
|
110
|
+
resolveDmPolicy: ({ cfg, accountId, account }) => {
|
|
111
|
+
const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
|
|
112
|
+
const rcConfig = (cfg as Record<string, unknown>).channels?.rocketchat;
|
|
113
|
+
const useAccountPath = Boolean(rcConfig?.accounts?.[resolvedAccountId]);
|
|
114
|
+
const basePath = useAccountPath
|
|
115
|
+
? `channels.rocketchat.accounts.${resolvedAccountId}.`
|
|
116
|
+
: "channels.rocketchat.";
|
|
117
|
+
return {
|
|
118
|
+
policy: account.config.dmPolicy ?? "pairing",
|
|
119
|
+
allowFrom: account.config.allowFrom ?? [],
|
|
120
|
+
policyPath: `${basePath}dmPolicy`,
|
|
121
|
+
allowFromPath: basePath,
|
|
122
|
+
approveHint: `openclaw pairing approve rocketchat <code>`,
|
|
123
|
+
normalizeEntry: normalizeAllowEntry,
|
|
124
|
+
};
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
messaging: {
|
|
128
|
+
normalizeTarget: normalizeRocketChatMessagingTarget,
|
|
129
|
+
targetResolver: {
|
|
130
|
+
looksLikeId: looksLikeRocketChatTargetId,
|
|
131
|
+
hint: "<roomId|#channel|@username|room:ID|user:USERNAME>",
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
outbound: {
|
|
135
|
+
deliveryMode: "direct",
|
|
136
|
+
chunker: (text, limit) =>
|
|
137
|
+
getRocketChatRuntime().channel?.text?.chunkMarkdownText?.(text, limit) ?? [text],
|
|
138
|
+
chunkerMode: "markdown",
|
|
139
|
+
textChunkLimit: 4000,
|
|
140
|
+
resolveTarget: ({ to }) => {
|
|
141
|
+
const trimmed = to?.trim();
|
|
142
|
+
if (!trimmed) {
|
|
143
|
+
return {
|
|
144
|
+
ok: false,
|
|
145
|
+
error: new Error(
|
|
146
|
+
"Delivering to Rocket.Chat requires --to <roomId|#channel|@username>"
|
|
147
|
+
),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return { ok: true, to: trimmed };
|
|
151
|
+
},
|
|
152
|
+
sendText: async ({ to, text, accountId, replyToId }) => {
|
|
153
|
+
const result = await sendMessageRocketChat(to, text, {
|
|
154
|
+
accountId: accountId ?? undefined,
|
|
155
|
+
replyToId: replyToId ?? undefined,
|
|
156
|
+
});
|
|
157
|
+
return { channel: "rocketchat", ...result };
|
|
158
|
+
},
|
|
159
|
+
sendMedia: async ({ to, text, mediaUrl, accountId, replyToId }) => {
|
|
160
|
+
const result = await sendMessageRocketChat(to, text, {
|
|
161
|
+
accountId: accountId ?? undefined,
|
|
162
|
+
mediaUrl,
|
|
163
|
+
replyToId: replyToId ?? undefined,
|
|
164
|
+
});
|
|
165
|
+
return { channel: "rocketchat", ...result };
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
status: {
|
|
169
|
+
defaultRuntime: {
|
|
170
|
+
accountId: DEFAULT_ACCOUNT_ID,
|
|
171
|
+
running: false,
|
|
172
|
+
connected: false,
|
|
173
|
+
lastConnectedAt: null,
|
|
174
|
+
lastDisconnect: null,
|
|
175
|
+
lastStartAt: null,
|
|
176
|
+
lastStopAt: null,
|
|
177
|
+
lastError: null,
|
|
178
|
+
},
|
|
179
|
+
buildChannelSummary: ({ snapshot }) => ({
|
|
180
|
+
configured: snapshot.configured ?? false,
|
|
181
|
+
authTokenSource: snapshot.authTokenSource ?? "none",
|
|
182
|
+
running: snapshot.running ?? false,
|
|
183
|
+
connected: snapshot.connected ?? false,
|
|
184
|
+
lastStartAt: snapshot.lastStartAt ?? null,
|
|
185
|
+
lastStopAt: snapshot.lastStopAt ?? null,
|
|
186
|
+
lastError: snapshot.lastError ?? null,
|
|
187
|
+
baseUrl: snapshot.baseUrl ?? null,
|
|
188
|
+
probe: snapshot.probe,
|
|
189
|
+
lastProbeAt: snapshot.lastProbeAt ?? null,
|
|
190
|
+
}),
|
|
191
|
+
buildAccountSnapshot: ({ account, runtime, probe }) => ({
|
|
192
|
+
accountId: account.accountId,
|
|
193
|
+
name: account.name,
|
|
194
|
+
enabled: account.enabled,
|
|
195
|
+
configured: Boolean(account.authToken && account.userId && account.baseUrl),
|
|
196
|
+
authTokenSource: account.authTokenSource,
|
|
197
|
+
baseUrl: account.baseUrl,
|
|
198
|
+
running: runtime?.running ?? false,
|
|
199
|
+
connected: runtime?.connected ?? false,
|
|
200
|
+
lastConnectedAt: runtime?.lastConnectedAt ?? null,
|
|
201
|
+
lastDisconnect: runtime?.lastDisconnect ?? null,
|
|
202
|
+
lastStartAt: runtime?.lastStartAt ?? null,
|
|
203
|
+
lastStopAt: runtime?.lastStopAt ?? null,
|
|
204
|
+
lastError: runtime?.lastError ?? null,
|
|
205
|
+
probe,
|
|
206
|
+
lastInboundAt: runtime?.lastInboundAt ?? null,
|
|
207
|
+
lastOutboundAt: runtime?.lastOutboundAt ?? null,
|
|
208
|
+
}),
|
|
209
|
+
},
|
|
210
|
+
gateway: {
|
|
211
|
+
startAccount: async (ctx) => {
|
|
212
|
+
const account = ctx.account;
|
|
213
|
+
ctx.setStatus({
|
|
214
|
+
accountId: account.accountId,
|
|
215
|
+
baseUrl: account.baseUrl,
|
|
216
|
+
authTokenSource: account.authTokenSource,
|
|
217
|
+
});
|
|
218
|
+
ctx.log?.info(`[${account.accountId}] starting Rocket.Chat channel`);
|
|
219
|
+
return monitorRocketChatProvider({
|
|
220
|
+
authToken: account.authToken ?? undefined,
|
|
221
|
+
userId: account.userId ?? undefined,
|
|
222
|
+
baseUrl: account.baseUrl ?? undefined,
|
|
223
|
+
accountId: account.accountId,
|
|
224
|
+
config: ctx.cfg,
|
|
225
|
+
runtime: ctx.runtime,
|
|
226
|
+
abortSignal: ctx.abortSignal,
|
|
227
|
+
statusSink: (patch) => ctx.setStatus({ accountId: ctx.accountId, ...patch }),
|
|
228
|
+
});
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rocket.Chat account resolution
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { OpenclawConfig } from "openclaw/plugin-sdk";
|
|
6
|
+
|
|
7
|
+
export type RocketChatReplyMode = "thread" | "channel" | "auto";
|
|
8
|
+
|
|
9
|
+
type RocketChatRoomConfig = {
|
|
10
|
+
requireMention?: boolean;
|
|
11
|
+
/** Optional per-room override. Use the room rid (e.g. GENERAL), not the channel name. */
|
|
12
|
+
replyMode?: RocketChatReplyMode;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type RocketChatAccountConfig = {
|
|
16
|
+
enabled?: boolean;
|
|
17
|
+
name?: string;
|
|
18
|
+
baseUrl?: string;
|
|
19
|
+
userId?: string;
|
|
20
|
+
authToken?: string;
|
|
21
|
+
authTokenFile?: string;
|
|
22
|
+
dmPolicy?: "pairing" | "allowlist" | "open" | "disabled";
|
|
23
|
+
allowFrom?: string[];
|
|
24
|
+
groupPolicy?: "allowlist" | "open" | "disabled";
|
|
25
|
+
groupAllowFrom?: string[];
|
|
26
|
+
rooms?: Record<string, RocketChatRoomConfig>;
|
|
27
|
+
|
|
28
|
+
/** Reply mode selection (thread | channel | auto). Default: thread (legacy behavior). */
|
|
29
|
+
replyMode?: RocketChatReplyMode;
|
|
30
|
+
|
|
31
|
+
/** Back-compat: if true, behave like replyMode=thread; if false, replyMode=channel. */
|
|
32
|
+
replyInThread?: boolean;
|
|
33
|
+
|
|
34
|
+
/** Typing indicator delay (ms) before emitting user-typing. Default 1000. */
|
|
35
|
+
typingDelayMs?: number;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type ResolvedRocketChatAccount = {
|
|
39
|
+
accountId: string;
|
|
40
|
+
name?: string;
|
|
41
|
+
enabled: boolean;
|
|
42
|
+
baseUrl?: string;
|
|
43
|
+
userId?: string;
|
|
44
|
+
authToken?: string;
|
|
45
|
+
authTokenSource: "config" | "env" | "file" | "none";
|
|
46
|
+
config: RocketChatAccountConfig;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const DEFAULT_ACCOUNT_ID = "default";
|
|
50
|
+
|
|
51
|
+
function getRocketChatConfig(cfg: OpenclawConfig): Record<string, unknown> | undefined {
|
|
52
|
+
return (cfg as Record<string, unknown>).channels?.rocketchat;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function listRocketChatAccountIds(cfg: OpenclawConfig): string[] {
|
|
56
|
+
const rc = getRocketChatConfig(cfg);
|
|
57
|
+
if (!rc) return [];
|
|
58
|
+
|
|
59
|
+
const accounts = rc.accounts as Record<string, unknown> | undefined;
|
|
60
|
+
if (accounts && typeof accounts === "object") {
|
|
61
|
+
return Object.keys(accounts);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Check for top-level config (legacy/default account)
|
|
65
|
+
if (rc.baseUrl || rc.userId || rc.authToken) {
|
|
66
|
+
return [DEFAULT_ACCOUNT_ID];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function resolveDefaultRocketChatAccountId(cfg: OpenclawConfig): string {
|
|
73
|
+
const ids = listRocketChatAccountIds(cfg);
|
|
74
|
+
return ids[0] ?? DEFAULT_ACCOUNT_ID;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function resolveRocketChatAccount(opts: {
|
|
78
|
+
cfg: OpenclawConfig;
|
|
79
|
+
accountId?: string;
|
|
80
|
+
}): ResolvedRocketChatAccount {
|
|
81
|
+
const { cfg, accountId: rawAccountId } = opts;
|
|
82
|
+
const accountId = rawAccountId?.trim() || resolveDefaultRocketChatAccountId(cfg);
|
|
83
|
+
const rc = getRocketChatConfig(cfg) ?? {};
|
|
84
|
+
|
|
85
|
+
const accounts = rc.accounts as Record<string, RocketChatAccountConfig> | undefined;
|
|
86
|
+
const accountConfig = accounts?.[accountId];
|
|
87
|
+
|
|
88
|
+
// Check for top-level config (default account)
|
|
89
|
+
const isDefaultPath = accountId === DEFAULT_ACCOUNT_ID && !accountConfig;
|
|
90
|
+
const config: RocketChatAccountConfig = accountConfig ?? (isDefaultPath ? rc : {});
|
|
91
|
+
|
|
92
|
+
// Resolve auth token from config, env, or file
|
|
93
|
+
let authToken = config.authToken as string | undefined;
|
|
94
|
+
let authTokenSource: "config" | "env" | "file" | "none" = "none";
|
|
95
|
+
|
|
96
|
+
if (authToken) {
|
|
97
|
+
authTokenSource = "config";
|
|
98
|
+
} else if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
99
|
+
const envToken = process.env.ROCKETCHAT_AUTH_TOKEN;
|
|
100
|
+
if (envToken) {
|
|
101
|
+
authToken = envToken;
|
|
102
|
+
authTokenSource = "env";
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Resolve user ID from config or env
|
|
107
|
+
let userId = config.userId as string | undefined;
|
|
108
|
+
if (!userId && accountId === DEFAULT_ACCOUNT_ID) {
|
|
109
|
+
userId = process.env.ROCKETCHAT_USER_ID;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Resolve base URL from config or env
|
|
113
|
+
let baseUrl = config.baseUrl as string | undefined;
|
|
114
|
+
if (!baseUrl && accountId === DEFAULT_ACCOUNT_ID) {
|
|
115
|
+
baseUrl = process.env.ROCKETCHAT_URL;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
accountId,
|
|
120
|
+
name: config.name,
|
|
121
|
+
enabled: config.enabled !== false,
|
|
122
|
+
baseUrl,
|
|
123
|
+
userId,
|
|
124
|
+
authToken,
|
|
125
|
+
authTokenSource,
|
|
126
|
+
config,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rocket.Chat REST API client
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type RocketChatClient = {
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
userId: string;
|
|
8
|
+
authToken: string;
|
|
9
|
+
fetch: typeof fetch;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type RocketChatUser = {
|
|
13
|
+
_id: string;
|
|
14
|
+
username: string;
|
|
15
|
+
name?: string;
|
|
16
|
+
status?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type RocketChatRoom = {
|
|
20
|
+
_id: string;
|
|
21
|
+
name?: string;
|
|
22
|
+
fname?: string;
|
|
23
|
+
t: "c" | "p" | "d" | "l"; // channel, private, direct, livechat
|
|
24
|
+
usernames?: string[];
|
|
25
|
+
usersCount?: number;
|
|
26
|
+
msgs?: number;
|
|
27
|
+
default?: boolean;
|
|
28
|
+
topic?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type RocketChatMessage = {
|
|
32
|
+
_id: string;
|
|
33
|
+
rid: string;
|
|
34
|
+
msg: string;
|
|
35
|
+
ts: string;
|
|
36
|
+
u: RocketChatUser;
|
|
37
|
+
_updatedAt: string;
|
|
38
|
+
mentions?: RocketChatUser[];
|
|
39
|
+
channels?: { _id: string; name: string }[];
|
|
40
|
+
attachments?: RocketChatAttachment[];
|
|
41
|
+
tmid?: string; // thread message id
|
|
42
|
+
t?: string; // system message type
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type RocketChatAttachment = {
|
|
46
|
+
title?: string;
|
|
47
|
+
title_link?: string;
|
|
48
|
+
image_url?: string;
|
|
49
|
+
audio_url?: string;
|
|
50
|
+
video_url?: string;
|
|
51
|
+
type?: string;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export function createRocketChatClient(opts: {
|
|
55
|
+
baseUrl: string;
|
|
56
|
+
userId: string;
|
|
57
|
+
authToken: string;
|
|
58
|
+
}): RocketChatClient {
|
|
59
|
+
const baseUrl = normalizeRocketChatBaseUrl(opts.baseUrl);
|
|
60
|
+
if (!baseUrl) throw new Error("Invalid Rocket.Chat baseUrl");
|
|
61
|
+
return {
|
|
62
|
+
baseUrl,
|
|
63
|
+
userId: opts.userId,
|
|
64
|
+
authToken: opts.authToken,
|
|
65
|
+
fetch: globalThis.fetch,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function normalizeRocketChatBaseUrl(url?: string): string | null {
|
|
70
|
+
if (!url) return null;
|
|
71
|
+
const trimmed = url.trim().replace(/\/+$/, "");
|
|
72
|
+
if (!trimmed) return null;
|
|
73
|
+
try {
|
|
74
|
+
const parsed = new URL(trimmed.startsWith("http") ? trimmed : `https://${trimmed}`);
|
|
75
|
+
return `${parsed.protocol}//${parsed.host}`;
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function rcFetch<T>(
|
|
82
|
+
client: RocketChatClient,
|
|
83
|
+
path: string,
|
|
84
|
+
opts: RequestInit = {}
|
|
85
|
+
): Promise<T> {
|
|
86
|
+
const url = `${client.baseUrl}${path}`;
|
|
87
|
+
const headers: Record<string, string> = {
|
|
88
|
+
"X-Auth-Token": client.authToken,
|
|
89
|
+
"X-User-Id": client.userId,
|
|
90
|
+
"Content-Type": "application/json",
|
|
91
|
+
...(opts.headers as Record<string, string> ?? {}),
|
|
92
|
+
};
|
|
93
|
+
const res = await client.fetch(url, { ...opts, headers });
|
|
94
|
+
if (!res.ok) {
|
|
95
|
+
const text = await res.text().catch(() => "");
|
|
96
|
+
throw new Error(`Rocket.Chat API error ${res.status}: ${text}`);
|
|
97
|
+
}
|
|
98
|
+
return res.json() as Promise<T>;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function fetchRocketChatMe(client: RocketChatClient): Promise<RocketChatUser> {
|
|
102
|
+
const res = await rcFetch<{ _id: string; username: string; name?: string; success: boolean }>(
|
|
103
|
+
client,
|
|
104
|
+
"/api/v1/me"
|
|
105
|
+
);
|
|
106
|
+
return { _id: res._id, username: res.username, name: res.name };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function fetchRocketChatUser(
|
|
110
|
+
client: RocketChatClient,
|
|
111
|
+
userId: string
|
|
112
|
+
): Promise<RocketChatUser> {
|
|
113
|
+
const res = await rcFetch<{ user: RocketChatUser; success: boolean }>(
|
|
114
|
+
client,
|
|
115
|
+
`/api/v1/users.info?userId=${encodeURIComponent(userId)}`
|
|
116
|
+
);
|
|
117
|
+
return res.user;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function fetchRocketChatUserByUsername(
|
|
121
|
+
client: RocketChatClient,
|
|
122
|
+
username: string
|
|
123
|
+
): Promise<RocketChatUser> {
|
|
124
|
+
const res = await rcFetch<{ user: RocketChatUser; success: boolean }>(
|
|
125
|
+
client,
|
|
126
|
+
`/api/v1/users.info?username=${encodeURIComponent(username)}`
|
|
127
|
+
);
|
|
128
|
+
return res.user;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function fetchRocketChatRoom(
|
|
132
|
+
client: RocketChatClient,
|
|
133
|
+
roomId: string
|
|
134
|
+
): Promise<RocketChatRoom> {
|
|
135
|
+
const res = await rcFetch<{ room: RocketChatRoom; success: boolean }>(
|
|
136
|
+
client,
|
|
137
|
+
`/api/v1/rooms.info?roomId=${encodeURIComponent(roomId)}`
|
|
138
|
+
);
|
|
139
|
+
return res.room;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function fetchRocketChatChannels(
|
|
143
|
+
client: RocketChatClient
|
|
144
|
+
): Promise<RocketChatRoom[]> {
|
|
145
|
+
const res = await rcFetch<{ channels: RocketChatRoom[]; success: boolean }>(
|
|
146
|
+
client,
|
|
147
|
+
"/api/v1/channels.list.joined"
|
|
148
|
+
);
|
|
149
|
+
return res.channels;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export type RocketChatSubscription = {
|
|
153
|
+
_id: string;
|
|
154
|
+
rid: string;
|
|
155
|
+
name: string;
|
|
156
|
+
fname?: string;
|
|
157
|
+
t: "c" | "p" | "d" | "l";
|
|
158
|
+
open?: boolean;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
export async function fetchRocketChatSubscriptions(
|
|
162
|
+
client: RocketChatClient
|
|
163
|
+
): Promise<RocketChatSubscription[]> {
|
|
164
|
+
const res = await rcFetch<{ update: RocketChatSubscription[]; success: boolean }>(
|
|
165
|
+
client,
|
|
166
|
+
"/api/v1/subscriptions.get"
|
|
167
|
+
);
|
|
168
|
+
return res.update ?? [];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function createRocketChatDirectMessage(
|
|
172
|
+
client: RocketChatClient,
|
|
173
|
+
username: string
|
|
174
|
+
): Promise<{ rid: string }> {
|
|
175
|
+
const res = await rcFetch<{ room: { rid: string }; success: boolean }>(
|
|
176
|
+
client,
|
|
177
|
+
"/api/v1/im.create",
|
|
178
|
+
{
|
|
179
|
+
method: "POST",
|
|
180
|
+
body: JSON.stringify({ username }),
|
|
181
|
+
}
|
|
182
|
+
);
|
|
183
|
+
return { rid: res.room.rid };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export async function postRocketChatMessage(
|
|
187
|
+
client: RocketChatClient,
|
|
188
|
+
opts: {
|
|
189
|
+
roomId?: string;
|
|
190
|
+
channel?: string;
|
|
191
|
+
text: string;
|
|
192
|
+
tmid?: string;
|
|
193
|
+
attachments?: RocketChatAttachment[];
|
|
194
|
+
}
|
|
195
|
+
): Promise<RocketChatMessage> {
|
|
196
|
+
const payload: Record<string, unknown> = {
|
|
197
|
+
text: opts.text,
|
|
198
|
+
};
|
|
199
|
+
if (opts.roomId) payload.roomId = opts.roomId;
|
|
200
|
+
if (opts.channel) payload.channel = opts.channel;
|
|
201
|
+
if (opts.tmid) payload.tmid = opts.tmid;
|
|
202
|
+
if (opts.attachments) payload.attachments = opts.attachments;
|
|
203
|
+
|
|
204
|
+
const res = await rcFetch<{ message: RocketChatMessage; success: boolean }>(
|
|
205
|
+
client,
|
|
206
|
+
"/api/v1/chat.postMessage",
|
|
207
|
+
{
|
|
208
|
+
method: "POST",
|
|
209
|
+
body: JSON.stringify(payload),
|
|
210
|
+
}
|
|
211
|
+
);
|
|
212
|
+
return res.message;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export async function sendRocketChatTyping(
|
|
216
|
+
client: RocketChatClient,
|
|
217
|
+
roomId: string,
|
|
218
|
+
isTyping: boolean
|
|
219
|
+
): Promise<void> {
|
|
220
|
+
// Rocket.Chat exposes a REST endpoint for typing state in most deployments.
|
|
221
|
+
// If the server doesn't support it (or it changes), callers should treat
|
|
222
|
+
// failures as non-fatal.
|
|
223
|
+
await rcFetch<{ success: boolean }>(client, "/api/v1/typing", {
|
|
224
|
+
method: "POST",
|
|
225
|
+
body: JSON.stringify({ roomId, typing: Boolean(isTyping) }),
|
|
226
|
+
});
|
|
227
|
+
}
|