@cloudrise/openclaw-channel-rocketchat 0.1.12 → 0.1.14
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/package.json +1 -1
- package/src/rocketchat/client.ts +111 -0
- package/src/rocketchat/send.ts +70 -2
package/package.json
CHANGED
package/src/rocketchat/client.ts
CHANGED
|
@@ -225,3 +225,114 @@ export async function sendRocketChatTyping(
|
|
|
225
225
|
body: JSON.stringify({ roomId, typing: Boolean(isTyping) }),
|
|
226
226
|
});
|
|
227
227
|
}
|
|
228
|
+
|
|
229
|
+
export type RocketChatUploadOpts = {
|
|
230
|
+
roomId: string;
|
|
231
|
+
file: Buffer;
|
|
232
|
+
fileName: string;
|
|
233
|
+
mimeType?: string;
|
|
234
|
+
description?: string;
|
|
235
|
+
tmid?: string;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
export type RocketChatUploadResult = {
|
|
239
|
+
_id: string;
|
|
240
|
+
rid: string;
|
|
241
|
+
ts: string;
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Upload a file to a Rocket.Chat room.
|
|
246
|
+
* RC 8.x uses a two-step process:
|
|
247
|
+
* 1. POST /api/v1/rooms.media/{roomId} → returns file._id
|
|
248
|
+
* 2. POST /api/v1/chat.sendMessage with file reference
|
|
249
|
+
*/
|
|
250
|
+
export async function uploadRocketChatFile(
|
|
251
|
+
client: RocketChatClient,
|
|
252
|
+
opts: RocketChatUploadOpts
|
|
253
|
+
): Promise<RocketChatUploadResult> {
|
|
254
|
+
// Step 1: Upload file to rooms.media
|
|
255
|
+
const uploadUrl = `${client.baseUrl}/api/v1/rooms.media/${opts.roomId}`;
|
|
256
|
+
|
|
257
|
+
// Build FormData manually for Node.js
|
|
258
|
+
const boundary = `----OpenClawBoundary${Date.now()}`;
|
|
259
|
+
const parts: Buffer[] = [];
|
|
260
|
+
|
|
261
|
+
// Add file part
|
|
262
|
+
const fileHeader = [
|
|
263
|
+
`--${boundary}`,
|
|
264
|
+
`Content-Disposition: form-data; name="file"; filename="${opts.fileName}"`,
|
|
265
|
+
`Content-Type: ${opts.mimeType ?? "application/octet-stream"}`,
|
|
266
|
+
"",
|
|
267
|
+
"",
|
|
268
|
+
].join("\r\n");
|
|
269
|
+
parts.push(Buffer.from(fileHeader));
|
|
270
|
+
parts.push(opts.file);
|
|
271
|
+
parts.push(Buffer.from("\r\n"));
|
|
272
|
+
|
|
273
|
+
// End boundary
|
|
274
|
+
parts.push(Buffer.from(`--${boundary}--\r\n`));
|
|
275
|
+
|
|
276
|
+
const uploadBody = Buffer.concat(parts);
|
|
277
|
+
|
|
278
|
+
const uploadRes = await client.fetch(uploadUrl, {
|
|
279
|
+
method: "POST",
|
|
280
|
+
headers: {
|
|
281
|
+
"X-Auth-Token": client.authToken,
|
|
282
|
+
"X-User-Id": client.userId,
|
|
283
|
+
"Content-Type": `multipart/form-data; boundary=${boundary}`,
|
|
284
|
+
"Content-Length": String(uploadBody.length),
|
|
285
|
+
},
|
|
286
|
+
body: uploadBody,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
if (!uploadRes.ok) {
|
|
290
|
+
const text = await uploadRes.text().catch(() => "");
|
|
291
|
+
throw new Error(`Rocket.Chat media upload error ${uploadRes.status}: ${text}`);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const uploadData = await uploadRes.json() as {
|
|
295
|
+
file: { _id: string; url: string };
|
|
296
|
+
success: boolean
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
if (!uploadData.success || !uploadData.file?._id) {
|
|
300
|
+
throw new Error("Rocket.Chat media upload failed: no file ID returned");
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Step 2: Send message with file reference
|
|
304
|
+
const messagePayload = {
|
|
305
|
+
message: {
|
|
306
|
+
rid: opts.roomId,
|
|
307
|
+
msg: opts.description ?? "",
|
|
308
|
+
file: { _id: uploadData.file._id },
|
|
309
|
+
...(opts.tmid && { tmid: opts.tmid }),
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const sendRes = await client.fetch(`${client.baseUrl}/api/v1/chat.sendMessage`, {
|
|
314
|
+
method: "POST",
|
|
315
|
+
headers: {
|
|
316
|
+
"X-Auth-Token": client.authToken,
|
|
317
|
+
"X-User-Id": client.userId,
|
|
318
|
+
"Content-Type": "application/json",
|
|
319
|
+
},
|
|
320
|
+
body: JSON.stringify(messagePayload),
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
if (!sendRes.ok) {
|
|
324
|
+
const text = await sendRes.text().catch(() => "");
|
|
325
|
+
throw new Error(`Rocket.Chat send message error ${sendRes.status}: ${text}`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const sendData = await sendRes.json() as {
|
|
329
|
+
message: { _id: string; rid: string; ts: string };
|
|
330
|
+
success: boolean
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
return {
|
|
334
|
+
_id: sendData.message._id,
|
|
335
|
+
rid: sendData.message.rid,
|
|
336
|
+
ts: sendData.message.ts,
|
|
337
|
+
};
|
|
338
|
+
}
|
package/src/rocketchat/send.ts
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
* Rocket.Chat message sending
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import * as fs from "node:fs/promises";
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
|
|
5
8
|
import { resolveRocketChatAccount } from "./accounts.js";
|
|
6
9
|
import {
|
|
7
10
|
createRocketChatClient,
|
|
@@ -10,6 +13,7 @@ import {
|
|
|
10
13
|
fetchRocketChatUserByUsername,
|
|
11
14
|
normalizeRocketChatBaseUrl,
|
|
12
15
|
postRocketChatMessage,
|
|
16
|
+
uploadRocketChatFile,
|
|
13
17
|
type RocketChatUser,
|
|
14
18
|
} from "./client.js";
|
|
15
19
|
import { getRocketChatRuntime } from "../runtime.js";
|
|
@@ -47,6 +51,32 @@ function isHttpUrl(value: string): boolean {
|
|
|
47
51
|
return /^https?:\/\//i.test(value);
|
|
48
52
|
}
|
|
49
53
|
|
|
54
|
+
function isLocalPath(value: string): boolean {
|
|
55
|
+
// Check if it's an absolute path or relative path (not a URL)
|
|
56
|
+
return value.startsWith("/") || value.startsWith("./") || value.startsWith("../") || /^[A-Za-z]:\\/.test(value);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Map file extensions to MIME types */
|
|
60
|
+
function getMimeFromExt(filePath: string): string {
|
|
61
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
62
|
+
const map: Record<string, string> = {
|
|
63
|
+
".png": "image/png",
|
|
64
|
+
".jpg": "image/jpeg",
|
|
65
|
+
".jpeg": "image/jpeg",
|
|
66
|
+
".gif": "image/gif",
|
|
67
|
+
".webp": "image/webp",
|
|
68
|
+
".pdf": "application/pdf",
|
|
69
|
+
".txt": "text/plain",
|
|
70
|
+
".md": "text/markdown",
|
|
71
|
+
".json": "application/json",
|
|
72
|
+
".mp3": "audio/mpeg",
|
|
73
|
+
".wav": "audio/wav",
|
|
74
|
+
".mp4": "video/mp4",
|
|
75
|
+
".webm": "video/webm",
|
|
76
|
+
};
|
|
77
|
+
return map[ext] ?? "application/octet-stream";
|
|
78
|
+
}
|
|
79
|
+
|
|
50
80
|
function parseRocketChatTarget(raw: string): RocketChatTarget {
|
|
51
81
|
const trimmed = raw.trim();
|
|
52
82
|
if (!trimmed) throw new Error("Recipient is required for Rocket.Chat sends");
|
|
@@ -165,7 +195,46 @@ export async function sendMessageRocketChat(
|
|
|
165
195
|
let message = text?.trim() ?? "";
|
|
166
196
|
const mediaUrl = opts.mediaUrl?.trim();
|
|
167
197
|
|
|
168
|
-
//
|
|
198
|
+
// Resolve room ID for uploads (channels need special handling)
|
|
199
|
+
const isChannel = target.kind === "channel";
|
|
200
|
+
const uploadRoomId = isChannel ? roomId : roomId;
|
|
201
|
+
|
|
202
|
+
// Handle local file uploads
|
|
203
|
+
if (mediaUrl && isLocalPath(mediaUrl)) {
|
|
204
|
+
try {
|
|
205
|
+
const fileBuffer = await fs.readFile(mediaUrl);
|
|
206
|
+
const fileName = path.basename(mediaUrl);
|
|
207
|
+
const mimeType = getMimeFromExt(mediaUrl);
|
|
208
|
+
|
|
209
|
+
logger?.debug?.(`Uploading file to Rocket.Chat: ${fileName} (${mimeType}, ${fileBuffer.length} bytes)`);
|
|
210
|
+
|
|
211
|
+
const upload = await uploadRocketChatFile(client, {
|
|
212
|
+
roomId: uploadRoomId,
|
|
213
|
+
file: fileBuffer,
|
|
214
|
+
fileName,
|
|
215
|
+
mimeType,
|
|
216
|
+
description: message || undefined,
|
|
217
|
+
tmid: opts.replyToId,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
core?.channel?.activity?.record?.({
|
|
221
|
+
channel: "rocketchat",
|
|
222
|
+
accountId: account.accountId,
|
|
223
|
+
direction: "outbound",
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
messageId: upload._id ?? "unknown",
|
|
228
|
+
roomId: upload.rid ?? uploadRoomId,
|
|
229
|
+
};
|
|
230
|
+
} catch (err) {
|
|
231
|
+
logger?.error?.(`Failed to upload file to Rocket.Chat: ${String(err)}`);
|
|
232
|
+
// Fall through to send as text message with path (degraded experience)
|
|
233
|
+
message = normalizeMessage(message, `[File: ${mediaUrl}]`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// For HTTP URLs, append to message (Rocket.Chat will unfurl)
|
|
169
238
|
if (mediaUrl && isHttpUrl(mediaUrl)) {
|
|
170
239
|
message = normalizeMessage(message, mediaUrl);
|
|
171
240
|
}
|
|
@@ -174,7 +243,6 @@ export async function sendMessageRocketChat(
|
|
|
174
243
|
throw new Error("Rocket.Chat message is empty");
|
|
175
244
|
}
|
|
176
245
|
|
|
177
|
-
const isChannel = target.kind === "channel";
|
|
178
246
|
const post = await postRocketChatMessage(client, {
|
|
179
247
|
roomId: isChannel ? undefined : roomId,
|
|
180
248
|
channel: isChannel ? `#${target.name}` : undefined,
|