moltbot-channel-feishu 0.0.8
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 +68 -0
- package/clawdbot.plugin.json +33 -0
- package/moltbot.plugin.json +33 -0
- package/package.json +86 -0
- package/src/api/client.ts +140 -0
- package/src/api/directory.ts +186 -0
- package/src/api/media.ts +335 -0
- package/src/api/messages.ts +290 -0
- package/src/api/reactions.ts +155 -0
- package/src/config/schema.ts +183 -0
- package/src/core/dispatcher.ts +227 -0
- package/src/core/gateway.ts +202 -0
- package/src/core/handler.ts +231 -0
- package/src/core/parser.ts +112 -0
- package/src/core/policy.ts +199 -0
- package/src/core/reply-dispatcher.ts +151 -0
- package/src/core/runtime.ts +27 -0
- package/src/index.ts +108 -0
- package/src/plugin/channel.ts +367 -0
- package/src/plugin/index.ts +28 -0
- package/src/plugin/onboarding.ts +378 -0
- package/src/types/clawdbot.d.ts +377 -0
- package/src/types/events.ts +72 -0
- package/src/types/index.ts +6 -0
- package/src/types/messages.ts +172 -0
package/src/api/media.ts
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Media upload and sending operations.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { Readable } from "node:stream";
|
|
8
|
+
import type { Config } from "../config/schema.js";
|
|
9
|
+
import type {
|
|
10
|
+
UploadImageParams,
|
|
11
|
+
UploadFileParams,
|
|
12
|
+
SendMediaParams,
|
|
13
|
+
SendResult,
|
|
14
|
+
ImageUploadResult,
|
|
15
|
+
FileUploadResult,
|
|
16
|
+
FileType,
|
|
17
|
+
SendImageParams,
|
|
18
|
+
SendFileParams,
|
|
19
|
+
} from "../types/index.js";
|
|
20
|
+
import { getApiClient } from "./client.js";
|
|
21
|
+
import { normalizeTarget, resolveReceiveIdType } from "./messages.js";
|
|
22
|
+
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// File Type Detection
|
|
25
|
+
// ============================================================================
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Detect file type from extension for upload.
|
|
29
|
+
*/
|
|
30
|
+
export function detectFileType(fileName: string): FileType {
|
|
31
|
+
const ext = path.extname(fileName).toLowerCase();
|
|
32
|
+
switch (ext) {
|
|
33
|
+
case ".opus":
|
|
34
|
+
case ".ogg":
|
|
35
|
+
return "opus";
|
|
36
|
+
case ".mp4":
|
|
37
|
+
case ".mov":
|
|
38
|
+
case ".avi":
|
|
39
|
+
return "mp4";
|
|
40
|
+
case ".pdf":
|
|
41
|
+
return "pdf";
|
|
42
|
+
case ".doc":
|
|
43
|
+
case ".docx":
|
|
44
|
+
return "doc";
|
|
45
|
+
case ".xls":
|
|
46
|
+
case ".xlsx":
|
|
47
|
+
return "xls";
|
|
48
|
+
case ".ppt":
|
|
49
|
+
case ".pptx":
|
|
50
|
+
return "ppt";
|
|
51
|
+
default:
|
|
52
|
+
return "stream";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Check if extension indicates an image.
|
|
58
|
+
*/
|
|
59
|
+
function isImageExtension(fileName: string): boolean {
|
|
60
|
+
const ext = path.extname(fileName).toLowerCase();
|
|
61
|
+
return [".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".ico", ".tiff"].includes(ext);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Check if a string is a local file path (not a URL).
|
|
66
|
+
*/
|
|
67
|
+
function isLocalPath(urlOrPath: string): boolean {
|
|
68
|
+
if (urlOrPath.startsWith("/") || urlOrPath.startsWith("~")) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
if (/^[a-zA-Z]:/.test(urlOrPath)) {
|
|
72
|
+
return true; // Windows drive letter
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const url = new URL(urlOrPath);
|
|
76
|
+
return url.protocol === "file:";
|
|
77
|
+
} catch {
|
|
78
|
+
return true; // Not a valid URL, treat as local path
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ============================================================================
|
|
83
|
+
// Upload Operations
|
|
84
|
+
// ============================================================================
|
|
85
|
+
|
|
86
|
+
interface UploadImageResponse {
|
|
87
|
+
code?: number;
|
|
88
|
+
msg?: string;
|
|
89
|
+
image_key?: string;
|
|
90
|
+
data?: { image_key?: string };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Upload an image to Feishu.
|
|
95
|
+
* Supports JPEG, PNG, WEBP, GIF, TIFF, BMP, ICO.
|
|
96
|
+
*
|
|
97
|
+
* @throws Error if upload fails
|
|
98
|
+
*/
|
|
99
|
+
export async function uploadImage(
|
|
100
|
+
config: Config,
|
|
101
|
+
params: UploadImageParams
|
|
102
|
+
): Promise<ImageUploadResult> {
|
|
103
|
+
const client = getApiClient(config);
|
|
104
|
+
const imageType = params.imageType ?? "message";
|
|
105
|
+
|
|
106
|
+
// Create readable stream from input
|
|
107
|
+
const imageStream =
|
|
108
|
+
typeof params.image === "string"
|
|
109
|
+
? fs.createReadStream(params.image)
|
|
110
|
+
: Readable.from(params.image);
|
|
111
|
+
|
|
112
|
+
const response = (await client.im.image.create({
|
|
113
|
+
data: {
|
|
114
|
+
image_type: imageType,
|
|
115
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
116
|
+
image: imageStream as any,
|
|
117
|
+
},
|
|
118
|
+
})) as UploadImageResponse;
|
|
119
|
+
|
|
120
|
+
if (response.code !== undefined && response.code !== 0) {
|
|
121
|
+
throw new Error(`Image upload failed: ${response.msg ?? `code ${response.code}`}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const imageKey = response.image_key ?? response.data?.image_key;
|
|
125
|
+
if (!imageKey) {
|
|
126
|
+
throw new Error("Image upload failed: no image_key returned");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { imageKey };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
interface UploadFileResponse {
|
|
133
|
+
code?: number;
|
|
134
|
+
msg?: string;
|
|
135
|
+
file_key?: string;
|
|
136
|
+
data?: { file_key?: string };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Upload a file to Feishu.
|
|
141
|
+
* Max file size: 30MB.
|
|
142
|
+
*
|
|
143
|
+
* @throws Error if upload fails
|
|
144
|
+
*/
|
|
145
|
+
export async function uploadFile(
|
|
146
|
+
config: Config,
|
|
147
|
+
params: UploadFileParams
|
|
148
|
+
): Promise<FileUploadResult> {
|
|
149
|
+
const client = getApiClient(config);
|
|
150
|
+
|
|
151
|
+
// Create readable stream from input
|
|
152
|
+
const fileStream =
|
|
153
|
+
typeof params.file === "string" ? fs.createReadStream(params.file) : Readable.from(params.file);
|
|
154
|
+
|
|
155
|
+
const response = (await client.im.file.create({
|
|
156
|
+
data: {
|
|
157
|
+
file_type: params.fileType,
|
|
158
|
+
file_name: params.fileName,
|
|
159
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
160
|
+
file: fileStream as any,
|
|
161
|
+
...(params.duration !== undefined ? { duration: params.duration } : {}),
|
|
162
|
+
},
|
|
163
|
+
})) as UploadFileResponse;
|
|
164
|
+
|
|
165
|
+
if (response.code !== undefined && response.code !== 0) {
|
|
166
|
+
throw new Error(`File upload failed: ${response.msg ?? `code ${response.code}`}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const fileKey = response.file_key ?? response.data?.file_key;
|
|
170
|
+
if (!fileKey) {
|
|
171
|
+
throw new Error("File upload failed: no file_key returned");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return { fileKey };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ============================================================================
|
|
178
|
+
// Media Sending
|
|
179
|
+
// ============================================================================
|
|
180
|
+
|
|
181
|
+
interface SendMediaResponse {
|
|
182
|
+
code?: number;
|
|
183
|
+
msg?: string;
|
|
184
|
+
data?: { message_id?: string };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Send an image message using an image_key.
|
|
189
|
+
*
|
|
190
|
+
* @throws Error if target is invalid or send fails
|
|
191
|
+
*/
|
|
192
|
+
export async function sendImage(config: Config, params: SendImageParams): Promise<SendResult> {
|
|
193
|
+
const client = getApiClient(config);
|
|
194
|
+
const receiveId = normalizeTarget(params.to);
|
|
195
|
+
|
|
196
|
+
if (!receiveId) {
|
|
197
|
+
throw new Error(`Invalid target: ${params.to}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const receiveIdType = resolveReceiveIdType(receiveId);
|
|
201
|
+
const content = JSON.stringify({ image_key: params.imageKey });
|
|
202
|
+
|
|
203
|
+
if (params.replyToMessageId) {
|
|
204
|
+
const response = (await client.im.message.reply({
|
|
205
|
+
path: { message_id: params.replyToMessageId },
|
|
206
|
+
data: { content, msg_type: "image" },
|
|
207
|
+
})) as SendMediaResponse;
|
|
208
|
+
|
|
209
|
+
if (response.code !== 0) {
|
|
210
|
+
throw new Error(`Image reply failed: ${response.msg ?? `code ${response.code}`}`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
messageId: response.data?.message_id ?? "unknown",
|
|
215
|
+
chatId: receiveId,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const response = (await client.im.message.create({
|
|
220
|
+
params: { receive_id_type: receiveIdType },
|
|
221
|
+
data: { receive_id: receiveId, content, msg_type: "image" },
|
|
222
|
+
})) as SendMediaResponse;
|
|
223
|
+
|
|
224
|
+
if (response.code !== 0) {
|
|
225
|
+
throw new Error(`Image send failed: ${response.msg ?? `code ${response.code}`}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
messageId: response.data?.message_id ?? "unknown",
|
|
230
|
+
chatId: receiveId,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Send a file message using a file_key.
|
|
236
|
+
*
|
|
237
|
+
* @throws Error if target is invalid or send fails
|
|
238
|
+
*/
|
|
239
|
+
export async function sendFile(config: Config, params: SendFileParams): Promise<SendResult> {
|
|
240
|
+
const client = getApiClient(config);
|
|
241
|
+
const receiveId = normalizeTarget(params.to);
|
|
242
|
+
|
|
243
|
+
if (!receiveId) {
|
|
244
|
+
throw new Error(`Invalid target: ${params.to}`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const receiveIdType = resolveReceiveIdType(receiveId);
|
|
248
|
+
const content = JSON.stringify({ file_key: params.fileKey });
|
|
249
|
+
|
|
250
|
+
if (params.replyToMessageId) {
|
|
251
|
+
const response = (await client.im.message.reply({
|
|
252
|
+
path: { message_id: params.replyToMessageId },
|
|
253
|
+
data: { content, msg_type: "file" },
|
|
254
|
+
})) as SendMediaResponse;
|
|
255
|
+
|
|
256
|
+
if (response.code !== 0) {
|
|
257
|
+
throw new Error(`File reply failed: ${response.msg ?? `code ${response.code}`}`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
messageId: response.data?.message_id ?? "unknown",
|
|
262
|
+
chatId: receiveId,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const response = (await client.im.message.create({
|
|
267
|
+
params: { receive_id_type: receiveIdType },
|
|
268
|
+
data: { receive_id: receiveId, content, msg_type: "file" },
|
|
269
|
+
})) as SendMediaResponse;
|
|
270
|
+
|
|
271
|
+
if (response.code !== 0) {
|
|
272
|
+
throw new Error(`File send failed: ${response.msg ?? `code ${response.code}`}`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
messageId: response.data?.message_id ?? "unknown",
|
|
277
|
+
chatId: receiveId,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Upload and send media (image or file) from URL, local path, or buffer.
|
|
283
|
+
*
|
|
284
|
+
* @throws Error if no media source provided or upload/send fails
|
|
285
|
+
*/
|
|
286
|
+
export async function sendMedia(config: Config, params: SendMediaParams): Promise<SendResult> {
|
|
287
|
+
let buffer: Buffer;
|
|
288
|
+
let name: string;
|
|
289
|
+
|
|
290
|
+
if (params.mediaBuffer) {
|
|
291
|
+
buffer = params.mediaBuffer;
|
|
292
|
+
name = params.fileName ?? "file";
|
|
293
|
+
} else if (params.mediaUrl) {
|
|
294
|
+
if (isLocalPath(params.mediaUrl)) {
|
|
295
|
+
// Local file path
|
|
296
|
+
const filePath = params.mediaUrl.startsWith("~")
|
|
297
|
+
? params.mediaUrl.replace("~", process.env["HOME"] ?? "")
|
|
298
|
+
: params.mediaUrl.replace("file://", "");
|
|
299
|
+
|
|
300
|
+
if (!fs.existsSync(filePath)) {
|
|
301
|
+
throw new Error(`Local file not found: ${filePath}`);
|
|
302
|
+
}
|
|
303
|
+
buffer = fs.readFileSync(filePath);
|
|
304
|
+
name = params.fileName ?? path.basename(filePath);
|
|
305
|
+
} else {
|
|
306
|
+
// Remote URL
|
|
307
|
+
const response = await fetch(params.mediaUrl);
|
|
308
|
+
if (!response.ok) {
|
|
309
|
+
throw new Error(`Failed to fetch media from URL: ${response.status}`);
|
|
310
|
+
}
|
|
311
|
+
buffer = Buffer.from(await response.arrayBuffer());
|
|
312
|
+
name = params.fileName ?? (path.basename(new URL(params.mediaUrl).pathname) || "file");
|
|
313
|
+
}
|
|
314
|
+
} else {
|
|
315
|
+
throw new Error("Either mediaUrl or mediaBuffer must be provided");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Determine if it's an image and upload accordingly
|
|
319
|
+
if (isImageExtension(name)) {
|
|
320
|
+
const { imageKey } = await uploadImage(config, { image: buffer });
|
|
321
|
+
return sendImage(config, {
|
|
322
|
+
to: params.to,
|
|
323
|
+
imageKey,
|
|
324
|
+
replyToMessageId: params.replyToMessageId,
|
|
325
|
+
});
|
|
326
|
+
} else {
|
|
327
|
+
const fileType = detectFileType(name);
|
|
328
|
+
const { fileKey } = await uploadFile(config, {
|
|
329
|
+
file: buffer,
|
|
330
|
+
fileName: name,
|
|
331
|
+
fileType,
|
|
332
|
+
});
|
|
333
|
+
return sendFile(config, { to: params.to, fileKey, replyToMessageId: params.replyToMessageId });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message sending and retrieval operations.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Config } from "../config/schema.js";
|
|
6
|
+
import type {
|
|
7
|
+
SendTextParams,
|
|
8
|
+
SendCardParams,
|
|
9
|
+
EditMessageParams,
|
|
10
|
+
SendResult,
|
|
11
|
+
MessageInfo,
|
|
12
|
+
ReceiveIdType,
|
|
13
|
+
} from "../types/index.js";
|
|
14
|
+
import { getApiClient } from "./client.js";
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Target Resolution
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Normalize a target string to a receive_id.
|
|
22
|
+
* Handles prefixed formats like "user:xxx" or "chat:xxx".
|
|
23
|
+
*/
|
|
24
|
+
export function normalizeTarget(target: string): string | null {
|
|
25
|
+
const trimmed = target.trim();
|
|
26
|
+
if (!trimmed) return null;
|
|
27
|
+
|
|
28
|
+
// Handle prefixed formats
|
|
29
|
+
if (trimmed.startsWith("user:")) {
|
|
30
|
+
return trimmed.slice(5).trim() || null;
|
|
31
|
+
}
|
|
32
|
+
if (trimmed.startsWith("chat:")) {
|
|
33
|
+
return trimmed.slice(5).trim() || null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Return as-is if no prefix
|
|
37
|
+
return trimmed;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Determine the receive_id_type based on ID format.
|
|
42
|
+
*/
|
|
43
|
+
export function resolveReceiveIdType(receiveId: string): ReceiveIdType {
|
|
44
|
+
if (receiveId.startsWith("oc_")) return "chat_id";
|
|
45
|
+
if (receiveId.startsWith("ou_")) return "open_id";
|
|
46
|
+
if (receiveId.startsWith("on_")) return "union_id";
|
|
47
|
+
// Default to open_id for DMs
|
|
48
|
+
return "open_id";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Check if a string looks like a Feishu ID.
|
|
53
|
+
*/
|
|
54
|
+
export function isValidId(id: string): boolean {
|
|
55
|
+
const trimmed = id.trim();
|
|
56
|
+
return (
|
|
57
|
+
trimmed.startsWith("oc_") ||
|
|
58
|
+
trimmed.startsWith("ou_") ||
|
|
59
|
+
trimmed.startsWith("on_") ||
|
|
60
|
+
trimmed.startsWith("u_") ||
|
|
61
|
+
trimmed.length > 10
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ============================================================================
|
|
66
|
+
// Message Retrieval
|
|
67
|
+
// ============================================================================
|
|
68
|
+
|
|
69
|
+
interface GetMessageResponse {
|
|
70
|
+
code?: number;
|
|
71
|
+
msg?: string;
|
|
72
|
+
data?: {
|
|
73
|
+
items?: {
|
|
74
|
+
message_id?: string;
|
|
75
|
+
chat_id?: string;
|
|
76
|
+
msg_type?: string;
|
|
77
|
+
body?: { content?: string };
|
|
78
|
+
sender?: {
|
|
79
|
+
id?: string;
|
|
80
|
+
id_type?: string;
|
|
81
|
+
sender_type?: string;
|
|
82
|
+
};
|
|
83
|
+
create_time?: string;
|
|
84
|
+
}[];
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get a message by ID.
|
|
90
|
+
* Returns null if message not found or access denied.
|
|
91
|
+
*/
|
|
92
|
+
export async function getMessage(config: Config, messageId: string): Promise<MessageInfo | null> {
|
|
93
|
+
const client = getApiClient(config);
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const response = (await client.im.message.get({
|
|
97
|
+
path: { message_id: messageId },
|
|
98
|
+
})) as GetMessageResponse;
|
|
99
|
+
|
|
100
|
+
if (response.code !== 0) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const item = response.data?.items?.[0];
|
|
105
|
+
if (!item) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Parse content based on message type
|
|
110
|
+
let content = item.body?.content ?? "";
|
|
111
|
+
try {
|
|
112
|
+
const parsed: unknown = JSON.parse(content);
|
|
113
|
+
if (
|
|
114
|
+
item.msg_type === "text" &&
|
|
115
|
+
typeof parsed === "object" &&
|
|
116
|
+
parsed !== null &&
|
|
117
|
+
"text" in parsed
|
|
118
|
+
) {
|
|
119
|
+
content = String((parsed as { text: unknown }).text);
|
|
120
|
+
}
|
|
121
|
+
} catch {
|
|
122
|
+
// Keep raw content if parsing fails
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
messageId: item.message_id ?? messageId,
|
|
127
|
+
chatId: item.chat_id ?? "",
|
|
128
|
+
senderId: item.sender?.id,
|
|
129
|
+
senderOpenId: item.sender?.id_type === "open_id" ? item.sender?.id : undefined,
|
|
130
|
+
content,
|
|
131
|
+
contentType: item.msg_type ?? "text",
|
|
132
|
+
createTime: item.create_time ? parseInt(item.create_time, 10) : undefined,
|
|
133
|
+
};
|
|
134
|
+
} catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ============================================================================
|
|
140
|
+
// Message Sending
|
|
141
|
+
// ============================================================================
|
|
142
|
+
|
|
143
|
+
interface SendMessageResponse {
|
|
144
|
+
code?: number;
|
|
145
|
+
msg?: string;
|
|
146
|
+
data?: {
|
|
147
|
+
message_id?: string;
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Send a text message.
|
|
153
|
+
*
|
|
154
|
+
* @throws Error if target is invalid or send fails
|
|
155
|
+
*/
|
|
156
|
+
export async function sendTextMessage(config: Config, params: SendTextParams): Promise<SendResult> {
|
|
157
|
+
const client = getApiClient(config);
|
|
158
|
+
const receiveId = normalizeTarget(params.to);
|
|
159
|
+
|
|
160
|
+
if (!receiveId) {
|
|
161
|
+
throw new Error(`Invalid target: ${params.to}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const receiveIdType = resolveReceiveIdType(receiveId);
|
|
165
|
+
const content = JSON.stringify({ text: params.text });
|
|
166
|
+
|
|
167
|
+
// Reply to existing message
|
|
168
|
+
if (params.replyToMessageId) {
|
|
169
|
+
const response = (await client.im.message.reply({
|
|
170
|
+
path: { message_id: params.replyToMessageId },
|
|
171
|
+
data: { content, msg_type: "text" },
|
|
172
|
+
})) as SendMessageResponse;
|
|
173
|
+
|
|
174
|
+
if (response.code !== 0) {
|
|
175
|
+
throw new Error(`Reply failed: ${response.msg ?? `code ${response.code}`}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
messageId: response.data?.message_id ?? "unknown",
|
|
180
|
+
chatId: receiveId,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Create new message
|
|
185
|
+
const response = (await client.im.message.create({
|
|
186
|
+
params: { receive_id_type: receiveIdType },
|
|
187
|
+
data: { receive_id: receiveId, content, msg_type: "text" },
|
|
188
|
+
})) as SendMessageResponse;
|
|
189
|
+
|
|
190
|
+
if (response.code !== 0) {
|
|
191
|
+
throw new Error(`Send failed: ${response.msg ?? `code ${response.code}`}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
messageId: response.data?.message_id ?? "unknown",
|
|
196
|
+
chatId: receiveId,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Send an interactive card message.
|
|
202
|
+
*
|
|
203
|
+
* @throws Error if target is invalid or send fails
|
|
204
|
+
*/
|
|
205
|
+
export async function sendCardMessage(config: Config, params: SendCardParams): Promise<SendResult> {
|
|
206
|
+
const client = getApiClient(config);
|
|
207
|
+
const receiveId = normalizeTarget(params.to);
|
|
208
|
+
|
|
209
|
+
if (!receiveId) {
|
|
210
|
+
throw new Error(`Invalid target: ${params.to}`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const receiveIdType = resolveReceiveIdType(receiveId);
|
|
214
|
+
const content = JSON.stringify(params.card);
|
|
215
|
+
|
|
216
|
+
// Reply with card
|
|
217
|
+
if (params.replyToMessageId) {
|
|
218
|
+
const response = (await client.im.message.reply({
|
|
219
|
+
path: { message_id: params.replyToMessageId },
|
|
220
|
+
data: { content, msg_type: "interactive" },
|
|
221
|
+
})) as SendMessageResponse;
|
|
222
|
+
|
|
223
|
+
if (response.code !== 0) {
|
|
224
|
+
throw new Error(`Card reply failed: ${response.msg ?? `code ${response.code}`}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
messageId: response.data?.message_id ?? "unknown",
|
|
229
|
+
chatId: receiveId,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Create card message
|
|
234
|
+
const response = (await client.im.message.create({
|
|
235
|
+
params: { receive_id_type: receiveIdType },
|
|
236
|
+
data: { receive_id: receiveId, content, msg_type: "interactive" },
|
|
237
|
+
})) as SendMessageResponse;
|
|
238
|
+
|
|
239
|
+
if (response.code !== 0) {
|
|
240
|
+
throw new Error(`Card send failed: ${response.msg ?? `code ${response.code}`}`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
messageId: response.data?.message_id ?? "unknown",
|
|
245
|
+
chatId: receiveId,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Update an interactive card message.
|
|
251
|
+
*
|
|
252
|
+
* @throws Error if update fails
|
|
253
|
+
*/
|
|
254
|
+
export async function updateCard(
|
|
255
|
+
config: Config,
|
|
256
|
+
messageId: string,
|
|
257
|
+
card: Record<string, unknown>
|
|
258
|
+
): Promise<void> {
|
|
259
|
+
const client = getApiClient(config);
|
|
260
|
+
const content = JSON.stringify(card);
|
|
261
|
+
|
|
262
|
+
const response = (await client.im.message.patch({
|
|
263
|
+
path: { message_id: messageId },
|
|
264
|
+
data: { content },
|
|
265
|
+
})) as SendMessageResponse;
|
|
266
|
+
|
|
267
|
+
if (response.code !== 0) {
|
|
268
|
+
throw new Error(`Card update failed: ${response.msg ?? `code ${response.code}`}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Edit an existing text message.
|
|
274
|
+
* Note: Feishu only allows editing messages within 24 hours.
|
|
275
|
+
*
|
|
276
|
+
* @throws Error if edit fails
|
|
277
|
+
*/
|
|
278
|
+
export async function editMessage(config: Config, params: EditMessageParams): Promise<void> {
|
|
279
|
+
const client = getApiClient(config);
|
|
280
|
+
const content = JSON.stringify({ text: params.text });
|
|
281
|
+
|
|
282
|
+
const response = (await client.im.message.update({
|
|
283
|
+
path: { message_id: params.messageId },
|
|
284
|
+
data: { msg_type: "text", content },
|
|
285
|
+
})) as SendMessageResponse;
|
|
286
|
+
|
|
287
|
+
if (response.code !== 0) {
|
|
288
|
+
throw new Error(`Edit failed: ${response.msg ?? `code ${response.code}`}`);
|
|
289
|
+
}
|
|
290
|
+
}
|