koishi-plugin-echo-cave 1.16.10 → 1.16.11
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/lib/index.cjs +8 -267
- package/package.json +1 -1
package/lib/index.cjs
CHANGED
|
@@ -200,82 +200,6 @@ function parseUserIds(userIds) {
|
|
|
200
200
|
// src/media-helper.ts
|
|
201
201
|
var import_axios = __toESM(require("axios"), 1);
|
|
202
202
|
var import_node_fs = require("node:fs");
|
|
203
|
-
var import_node_path = __toESM(require("node:path"), 1);
|
|
204
|
-
async function saveMedia(ctx, mediaElement, type, cfg) {
|
|
205
|
-
const mediaUrl = mediaElement.url;
|
|
206
|
-
const originalMediaName = mediaElement.file;
|
|
207
|
-
const ext = (() => {
|
|
208
|
-
const i = originalMediaName.lastIndexOf(".");
|
|
209
|
-
return i === -1 ? type === "image" ? "png" : type === "video" ? "mp4" : type === "record" ? "mp3" : "bin" : originalMediaName.slice(i + 1).toLowerCase();
|
|
210
|
-
})();
|
|
211
|
-
const mediaDir = import_node_path.default.join(ctx.baseDir, "data", "cave", type + "s");
|
|
212
|
-
const mediaName = Date.now().toString();
|
|
213
|
-
const fullMediaPath = import_node_path.default.join(mediaDir, `${mediaName}.${ext}`);
|
|
214
|
-
ctx.logger.info(`Saving ${type} from ${mediaUrl} -> ${fullMediaPath}`);
|
|
215
|
-
try {
|
|
216
|
-
await import_node_fs.promises.mkdir(mediaDir, { recursive: true });
|
|
217
|
-
const res = await import_axios.default.get(mediaUrl, {
|
|
218
|
-
responseType: "arraybuffer",
|
|
219
|
-
validateStatus: () => true
|
|
220
|
-
});
|
|
221
|
-
if (res.status < 200 || res.status >= 300) {
|
|
222
|
-
ctx.logger.warn(
|
|
223
|
-
`${type.charAt(0).toUpperCase() + type.slice(1)} download failed: HTTP ${res.status}`
|
|
224
|
-
);
|
|
225
|
-
return mediaUrl;
|
|
226
|
-
}
|
|
227
|
-
const contentType = res.headers["content-type"];
|
|
228
|
-
if (contentType) {
|
|
229
|
-
if (type === "image" && !contentType.startsWith("image/")) {
|
|
230
|
-
ctx.logger.warn(`Invalid image content-type: ${contentType}`);
|
|
231
|
-
return mediaUrl;
|
|
232
|
-
}
|
|
233
|
-
if (type === "video" && !contentType.startsWith("video/")) {
|
|
234
|
-
ctx.logger.warn(`Invalid video content-type: ${contentType}`);
|
|
235
|
-
return mediaUrl;
|
|
236
|
-
}
|
|
237
|
-
if (type === "record" && !contentType.startsWith("audio/")) {
|
|
238
|
-
ctx.logger.warn(`Invalid record content-type: ${contentType}`);
|
|
239
|
-
return mediaUrl;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
const buffer = Buffer.from(res.data);
|
|
243
|
-
if (!buffer || buffer.length === 0) {
|
|
244
|
-
ctx.logger.warn(`Downloaded ${type} buffer is empty`);
|
|
245
|
-
return mediaUrl;
|
|
246
|
-
}
|
|
247
|
-
await import_node_fs.promises.writeFile(fullMediaPath, buffer);
|
|
248
|
-
ctx.logger.info(
|
|
249
|
-
`${type.charAt(0).toUpperCase() + type.slice(1)} saved successfully: ${fullMediaPath}`
|
|
250
|
-
);
|
|
251
|
-
await checkAndCleanMediaFiles(ctx, cfg, type);
|
|
252
|
-
return fullMediaPath;
|
|
253
|
-
} catch (err) {
|
|
254
|
-
ctx.logger.error(`Failed to save ${type}: ${err}`);
|
|
255
|
-
return mediaUrl;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
async function processMediaElement(ctx, element, cfg) {
|
|
259
|
-
if (element.type === "image" || element.type === "video" || element.type === "file" || element.type === "record") {
|
|
260
|
-
const savedPath = await saveMedia(
|
|
261
|
-
ctx,
|
|
262
|
-
element.data,
|
|
263
|
-
element.type,
|
|
264
|
-
cfg
|
|
265
|
-
);
|
|
266
|
-
const fileUri = `file:///${savedPath.replace(/\\/g, "/")}`;
|
|
267
|
-
return {
|
|
268
|
-
...element,
|
|
269
|
-
data: {
|
|
270
|
-
...element.data,
|
|
271
|
-
file: fileUri,
|
|
272
|
-
// Remove the url field
|
|
273
|
-
url: void 0
|
|
274
|
-
}
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
return element;
|
|
278
|
-
}
|
|
279
203
|
async function convertFileUriToBase64(ctx, element) {
|
|
280
204
|
if (element.type === "image" || element.type === "video" || element.type === "file" || element.type === "record") {
|
|
281
205
|
const fileUri = element.data.file;
|
|
@@ -305,72 +229,6 @@ async function convertFileUriToBase64(ctx, element) {
|
|
|
305
229
|
}
|
|
306
230
|
return element;
|
|
307
231
|
}
|
|
308
|
-
async function checkAndCleanMediaFiles(ctx, cfg, type) {
|
|
309
|
-
if (!cfg.enableSizeLimit) {
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
const mediaDir = import_node_path.default.join(ctx.baseDir, "data", "cave", type + "s");
|
|
313
|
-
const maxSize = (() => {
|
|
314
|
-
switch (type) {
|
|
315
|
-
case "image":
|
|
316
|
-
return (cfg.maxImageSize || 100) * 1024 * 1024;
|
|
317
|
-
// 转换为字节
|
|
318
|
-
case "video":
|
|
319
|
-
return (cfg.maxVideoSize || 500) * 1024 * 1024;
|
|
320
|
-
case "file":
|
|
321
|
-
return (cfg.maxFileSize || 1e3) * 1024 * 1024;
|
|
322
|
-
case "record":
|
|
323
|
-
return (cfg.maxRecordSize || 200) * 1024 * 1024;
|
|
324
|
-
}
|
|
325
|
-
})();
|
|
326
|
-
try {
|
|
327
|
-
const files = await import_node_fs.promises.readdir(mediaDir);
|
|
328
|
-
if (files.length === 0) {
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
331
|
-
const fileInfos = await Promise.all(
|
|
332
|
-
files.map(async (file) => {
|
|
333
|
-
const filePath = import_node_path.default.join(mediaDir, file);
|
|
334
|
-
const stats = await import_node_fs.promises.stat(filePath);
|
|
335
|
-
return {
|
|
336
|
-
path: filePath,
|
|
337
|
-
size: stats.size,
|
|
338
|
-
mtime: stats.mtimeMs
|
|
339
|
-
};
|
|
340
|
-
})
|
|
341
|
-
);
|
|
342
|
-
const totalSize = fileInfos.reduce((sum, file) => sum + file.size, 0);
|
|
343
|
-
ctx.logger.info(
|
|
344
|
-
`${type} directory total size: ${(totalSize / (1024 * 1024)).toFixed(2)} MB, max allowed: ${(maxSize / (1024 * 1024)).toFixed(2)} MB`
|
|
345
|
-
);
|
|
346
|
-
if (totalSize > maxSize) {
|
|
347
|
-
ctx.logger.warn(
|
|
348
|
-
`${type} directory size exceeds limit! Total: ${(totalSize / (1024 * 1024)).toFixed(2)} MB, Max: ${(maxSize / (1024 * 1024)).toFixed(2)} MB`
|
|
349
|
-
);
|
|
350
|
-
fileInfos.sort((a, b) => a.mtime - b.mtime);
|
|
351
|
-
let currentSize = totalSize;
|
|
352
|
-
let filesToDelete = [];
|
|
353
|
-
for (const file of fileInfos) {
|
|
354
|
-
if (currentSize <= maxSize) {
|
|
355
|
-
break;
|
|
356
|
-
}
|
|
357
|
-
filesToDelete.push(file);
|
|
358
|
-
currentSize -= file.size;
|
|
359
|
-
}
|
|
360
|
-
for (const file of filesToDelete) {
|
|
361
|
-
await import_node_fs.promises.unlink(file.path);
|
|
362
|
-
ctx.logger.info(
|
|
363
|
-
`Deleted oldest ${type} file: ${import_node_path.default.basename(file.path)} (${(file.size / (1024 * 1024)).toFixed(2)} MB)`
|
|
364
|
-
);
|
|
365
|
-
}
|
|
366
|
-
ctx.logger.info(
|
|
367
|
-
`Cleanup completed. ${type} directory new size: ${(currentSize / (1024 * 1024)).toFixed(2)} MB`
|
|
368
|
-
);
|
|
369
|
-
}
|
|
370
|
-
} catch (err) {
|
|
371
|
-
ctx.logger.error(`Failed to check and clean ${type} files: ${err}`);
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
232
|
async function deleteMediaFilesFromMessage(ctx, content) {
|
|
375
233
|
try {
|
|
376
234
|
const elements = JSON.parse(content);
|
|
@@ -488,60 +346,6 @@ function formatDate(date) {
|
|
|
488
346
|
});
|
|
489
347
|
}
|
|
490
348
|
|
|
491
|
-
// src/forward-helper.ts
|
|
492
|
-
async function reconstructForwardMsg(ctx, session, message, cfg) {
|
|
493
|
-
return Promise.all(
|
|
494
|
-
message.map(async (msg) => {
|
|
495
|
-
const content = await processForwardMessageContent(ctx, session, msg, cfg);
|
|
496
|
-
const senderNickname = msg.sender.nickname;
|
|
497
|
-
let senderUserId = msg.sender.user_id;
|
|
498
|
-
senderUserId = senderUserId === 1094950020 ? await getUserIdFromNickname(session, senderNickname, senderUserId) : senderUserId;
|
|
499
|
-
return {
|
|
500
|
-
type: "node",
|
|
501
|
-
data: {
|
|
502
|
-
user_id: senderUserId,
|
|
503
|
-
nickname: senderNickname,
|
|
504
|
-
content
|
|
505
|
-
}
|
|
506
|
-
};
|
|
507
|
-
})
|
|
508
|
-
);
|
|
509
|
-
}
|
|
510
|
-
async function getUserIdFromNickname(session, nickname, userId) {
|
|
511
|
-
const memberInfos = await session.onebot.getGroupMemberList(session.channelId);
|
|
512
|
-
const matches = memberInfos.filter((m) => m.nickname === nickname);
|
|
513
|
-
if (matches.length === 1) {
|
|
514
|
-
return matches[0].user_id;
|
|
515
|
-
}
|
|
516
|
-
return userId;
|
|
517
|
-
}
|
|
518
|
-
async function processForwardMessageContent(ctx, session, msg, cfg) {
|
|
519
|
-
if (typeof msg.message === "string") {
|
|
520
|
-
return msg.message;
|
|
521
|
-
}
|
|
522
|
-
const firstElement = msg.message[0];
|
|
523
|
-
if (firstElement?.type === "forward") {
|
|
524
|
-
return reconstructForwardMsg(ctx, session, firstElement.data.content, cfg);
|
|
525
|
-
}
|
|
526
|
-
return Promise.all(
|
|
527
|
-
msg.message.map(async (element) => {
|
|
528
|
-
return processMediaElement(ctx, element, cfg);
|
|
529
|
-
})
|
|
530
|
-
);
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
// src/msg-helper.ts
|
|
534
|
-
async function processMessageContent(ctx, msg, cfg) {
|
|
535
|
-
return Promise.all(
|
|
536
|
-
msg.map(async (element) => {
|
|
537
|
-
if (element.type === "reply") {
|
|
538
|
-
return element;
|
|
539
|
-
}
|
|
540
|
-
return processMediaElement(ctx, element, cfg);
|
|
541
|
-
})
|
|
542
|
-
);
|
|
543
|
-
}
|
|
544
|
-
|
|
545
349
|
// src/index.ts
|
|
546
350
|
var import_koishi_plugin_adapter_onebot3 = require("@pynickle/koishi-plugin-adapter-onebot");
|
|
547
351
|
var import_koishi = require("koishi");
|
|
@@ -583,8 +387,10 @@ function apply(ctx, cfg) {
|
|
|
583
387
|
ctx.command("cave [id:number]").action(
|
|
584
388
|
async ({ session }, id) => await getCave(ctx, session, cfg, id)
|
|
585
389
|
);
|
|
586
|
-
ctx.command("cave.echo [
|
|
587
|
-
async ({ session },
|
|
390
|
+
ctx.command("cave.echo [userIds:text]").action(
|
|
391
|
+
async ({ session }, userIds) => {
|
|
392
|
+
ctx.logger.info(`User ${session.userId} is adding a cave message with related users: ${userIds}`);
|
|
393
|
+
}
|
|
588
394
|
);
|
|
589
395
|
ctx.command("cave.wipe <id:number>").action(
|
|
590
396
|
async ({ session }, id) => await deleteCave(ctx, session, cfg, id)
|
|
@@ -594,7 +400,10 @@ function apply(ctx, cfg) {
|
|
|
594
400
|
async ({ session }) => await getCaveListByOriginUser(ctx, session)
|
|
595
401
|
);
|
|
596
402
|
ctx.command("cave.bind <id:number> <...userIds>", { authority: 4 }).action(
|
|
597
|
-
async ({ session }, id, ...userIds) =>
|
|
403
|
+
async ({ session }, id, ...userIds) => {
|
|
404
|
+
ctx.logger.info(`Binding users ${JSON.stringify(userIds)} to cave ID ${id}`);
|
|
405
|
+
await bindUsersToCave(ctx, session, id, userIds);
|
|
406
|
+
}
|
|
598
407
|
);
|
|
599
408
|
}
|
|
600
409
|
async function getCaveListByUser(ctx, session) {
|
|
@@ -701,74 +510,6 @@ async function deleteCave(ctx, session, cfg, id) {
|
|
|
701
510
|
await ctx.database.remove("echo_cave", id);
|
|
702
511
|
return session.text(".msgDeleted", [id]);
|
|
703
512
|
}
|
|
704
|
-
async function addCave(ctx, session, cfg, userIds) {
|
|
705
|
-
if (!session.guildId) {
|
|
706
|
-
return session.text("echo-cave.general.privateChatReminder");
|
|
707
|
-
}
|
|
708
|
-
if (!session.quote) {
|
|
709
|
-
return session.text(".noMsgQuoted");
|
|
710
|
-
}
|
|
711
|
-
const { userId, channelId, quote } = session;
|
|
712
|
-
const messageId = quote.id;
|
|
713
|
-
let parsedUserIds = [];
|
|
714
|
-
if (userIds && userIds.length > 0) {
|
|
715
|
-
ctx.logger.info(`Original userIds in addCave: ${JSON.stringify(userIds)}`);
|
|
716
|
-
const result = parseUserIds(userIds);
|
|
717
|
-
if (result.error === "invalid_all_mention") {
|
|
718
|
-
return session.text(".invalidAllMention");
|
|
719
|
-
}
|
|
720
|
-
parsedUserIds = result.parsedUserIds;
|
|
721
|
-
ctx.logger.info(`Parsed userIds in addCave: ${JSON.stringify(parsedUserIds)}`);
|
|
722
|
-
const isAllUsersInGroup = await checkUsersInGroup(ctx, session, parsedUserIds);
|
|
723
|
-
if (!isAllUsersInGroup) {
|
|
724
|
-
return session.text(".userNotInGroup");
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
let content;
|
|
728
|
-
let type;
|
|
729
|
-
if (quote.elements[0].type === "forward") {
|
|
730
|
-
type = "forward";
|
|
731
|
-
const message = await reconstructForwardMsg(
|
|
732
|
-
ctx,
|
|
733
|
-
session,
|
|
734
|
-
await session.onebot.getForwardMsg(messageId),
|
|
735
|
-
cfg
|
|
736
|
-
);
|
|
737
|
-
content = JSON.stringify(message);
|
|
738
|
-
} else {
|
|
739
|
-
type = "msg";
|
|
740
|
-
const message = (await session.onebot.getMsg(messageId)).message;
|
|
741
|
-
let msgJson;
|
|
742
|
-
if (typeof message === "string") {
|
|
743
|
-
msgJson = import_koishi_plugin_adapter_onebot3.CQCode.parse(message);
|
|
744
|
-
} else {
|
|
745
|
-
if (message[0].type === "video" || message[0].type === "file") {
|
|
746
|
-
type = "forward";
|
|
747
|
-
}
|
|
748
|
-
msgJson = message;
|
|
749
|
-
}
|
|
750
|
-
content = JSON.stringify(await processMessageContent(ctx, msgJson, cfg));
|
|
751
|
-
}
|
|
752
|
-
await ctx.database.get("echo_cave", { content }).then((existing) => {
|
|
753
|
-
if (existing) {
|
|
754
|
-
return session.text(".existingMsg");
|
|
755
|
-
}
|
|
756
|
-
});
|
|
757
|
-
try {
|
|
758
|
-
const result = await ctx.database.create("echo_cave", {
|
|
759
|
-
channelId,
|
|
760
|
-
createTime: /* @__PURE__ */ new Date(),
|
|
761
|
-
userId,
|
|
762
|
-
originUserId: quote.user.id,
|
|
763
|
-
type,
|
|
764
|
-
content,
|
|
765
|
-
relatedUsers: parsedUserIds || []
|
|
766
|
-
});
|
|
767
|
-
return session.text(".msgSaved", [result.id]);
|
|
768
|
-
} catch (error) {
|
|
769
|
-
return session.text(".msgFailedToSave");
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
513
|
async function bindUsersToCave(ctx, session, id, userIds) {
|
|
773
514
|
if (!session.guildId) {
|
|
774
515
|
return session.text("echo-cave.general.privateChatReminder");
|