koishi-plugin-echo-cave 1.16.11 → 1.16.12
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/cqcode-helper.d.ts +1 -1
- package/lib/index.cjs +273 -30
- package/package.json +1 -1
package/lib/cqcode-helper.d.ts
CHANGED
package/lib/index.cjs
CHANGED
|
@@ -154,10 +154,9 @@ __export(index_exports, {
|
|
|
154
154
|
name: () => name
|
|
155
155
|
});
|
|
156
156
|
module.exports = __toCommonJS(index_exports);
|
|
157
|
-
var
|
|
157
|
+
var import_koishi_plugin_adapter_onebot = require("@pynickle/koishi-plugin-adapter-onebot");
|
|
158
158
|
|
|
159
159
|
// src/cqcode-helper.ts
|
|
160
|
-
var import_koishi_plugin_adapter_onebot = require("@pynickle/koishi-plugin-adapter-onebot");
|
|
161
160
|
function createTextMsg(content) {
|
|
162
161
|
return {
|
|
163
162
|
type: "text",
|
|
@@ -168,28 +167,8 @@ function createTextMsg(content) {
|
|
|
168
167
|
}
|
|
169
168
|
function parseUserIds(userIds) {
|
|
170
169
|
const parsedUserIds = [];
|
|
171
|
-
for (const
|
|
172
|
-
|
|
173
|
-
const cqCode = import_koishi_plugin_adapter_onebot.CQCode.from(userIdStr);
|
|
174
|
-
if (cqCode.type === "at") {
|
|
175
|
-
const qq = cqCode.data.qq;
|
|
176
|
-
if (qq === "all") {
|
|
177
|
-
return {
|
|
178
|
-
parsedUserIds: [],
|
|
179
|
-
error: "invalid_all_mention"
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
if (qq) {
|
|
183
|
-
parsedUserIds.push(qq);
|
|
184
|
-
}
|
|
185
|
-
} else {
|
|
186
|
-
const num = Number(userIdStr);
|
|
187
|
-
if (!Number.isNaN(num)) {
|
|
188
|
-
parsedUserIds.push(String(num));
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
} catch (e) {
|
|
192
|
-
parsedUserIds.push(userIdStr.split(":")[1]);
|
|
170
|
+
for (const userId of userIds) {
|
|
171
|
+
if (userId) {
|
|
193
172
|
}
|
|
194
173
|
}
|
|
195
174
|
return {
|
|
@@ -200,6 +179,82 @@ function parseUserIds(userIds) {
|
|
|
200
179
|
// src/media-helper.ts
|
|
201
180
|
var import_axios = __toESM(require("axios"), 1);
|
|
202
181
|
var import_node_fs = require("node:fs");
|
|
182
|
+
var import_node_path = __toESM(require("node:path"), 1);
|
|
183
|
+
async function saveMedia(ctx, mediaElement, type, cfg) {
|
|
184
|
+
const mediaUrl = mediaElement.url;
|
|
185
|
+
const originalMediaName = mediaElement.file;
|
|
186
|
+
const ext = (() => {
|
|
187
|
+
const i = originalMediaName.lastIndexOf(".");
|
|
188
|
+
return i === -1 ? type === "image" ? "png" : type === "video" ? "mp4" : type === "record" ? "mp3" : "bin" : originalMediaName.slice(i + 1).toLowerCase();
|
|
189
|
+
})();
|
|
190
|
+
const mediaDir = import_node_path.default.join(ctx.baseDir, "data", "cave", type + "s");
|
|
191
|
+
const mediaName = Date.now().toString();
|
|
192
|
+
const fullMediaPath = import_node_path.default.join(mediaDir, `${mediaName}.${ext}`);
|
|
193
|
+
ctx.logger.info(`Saving ${type} from ${mediaUrl} -> ${fullMediaPath}`);
|
|
194
|
+
try {
|
|
195
|
+
await import_node_fs.promises.mkdir(mediaDir, { recursive: true });
|
|
196
|
+
const res = await import_axios.default.get(mediaUrl, {
|
|
197
|
+
responseType: "arraybuffer",
|
|
198
|
+
validateStatus: () => true
|
|
199
|
+
});
|
|
200
|
+
if (res.status < 200 || res.status >= 300) {
|
|
201
|
+
ctx.logger.warn(
|
|
202
|
+
`${type.charAt(0).toUpperCase() + type.slice(1)} download failed: HTTP ${res.status}`
|
|
203
|
+
);
|
|
204
|
+
return mediaUrl;
|
|
205
|
+
}
|
|
206
|
+
const contentType = res.headers["content-type"];
|
|
207
|
+
if (contentType) {
|
|
208
|
+
if (type === "image" && !contentType.startsWith("image/")) {
|
|
209
|
+
ctx.logger.warn(`Invalid image content-type: ${contentType}`);
|
|
210
|
+
return mediaUrl;
|
|
211
|
+
}
|
|
212
|
+
if (type === "video" && !contentType.startsWith("video/")) {
|
|
213
|
+
ctx.logger.warn(`Invalid video content-type: ${contentType}`);
|
|
214
|
+
return mediaUrl;
|
|
215
|
+
}
|
|
216
|
+
if (type === "record" && !contentType.startsWith("audio/")) {
|
|
217
|
+
ctx.logger.warn(`Invalid record content-type: ${contentType}`);
|
|
218
|
+
return mediaUrl;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const buffer = Buffer.from(res.data);
|
|
222
|
+
if (!buffer || buffer.length === 0) {
|
|
223
|
+
ctx.logger.warn(`Downloaded ${type} buffer is empty`);
|
|
224
|
+
return mediaUrl;
|
|
225
|
+
}
|
|
226
|
+
await import_node_fs.promises.writeFile(fullMediaPath, buffer);
|
|
227
|
+
ctx.logger.info(
|
|
228
|
+
`${type.charAt(0).toUpperCase() + type.slice(1)} saved successfully: ${fullMediaPath}`
|
|
229
|
+
);
|
|
230
|
+
await checkAndCleanMediaFiles(ctx, cfg, type);
|
|
231
|
+
return fullMediaPath;
|
|
232
|
+
} catch (err) {
|
|
233
|
+
ctx.logger.error(`Failed to save ${type}: ${err}`);
|
|
234
|
+
return mediaUrl;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
async function processMediaElement(ctx, element, cfg) {
|
|
238
|
+
if (element.type === "image" || element.type === "video" || element.type === "file" || element.type === "record") {
|
|
239
|
+
const savedPath = await saveMedia(
|
|
240
|
+
ctx,
|
|
241
|
+
element.data,
|
|
242
|
+
element.type,
|
|
243
|
+
cfg
|
|
244
|
+
);
|
|
245
|
+
const fileUri = `file:///${savedPath.replace(/\\/g, "/")}`;
|
|
246
|
+
return {
|
|
247
|
+
...element,
|
|
248
|
+
data: {
|
|
249
|
+
...element.data,
|
|
250
|
+
file: fileUri,
|
|
251
|
+
// Remove the url field
|
|
252
|
+
url: void 0
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
return element;
|
|
257
|
+
}
|
|
203
258
|
async function convertFileUriToBase64(ctx, element) {
|
|
204
259
|
if (element.type === "image" || element.type === "video" || element.type === "file" || element.type === "record") {
|
|
205
260
|
const fileUri = element.data.file;
|
|
@@ -229,6 +284,72 @@ async function convertFileUriToBase64(ctx, element) {
|
|
|
229
284
|
}
|
|
230
285
|
return element;
|
|
231
286
|
}
|
|
287
|
+
async function checkAndCleanMediaFiles(ctx, cfg, type) {
|
|
288
|
+
if (!cfg.enableSizeLimit) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const mediaDir = import_node_path.default.join(ctx.baseDir, "data", "cave", type + "s");
|
|
292
|
+
const maxSize = (() => {
|
|
293
|
+
switch (type) {
|
|
294
|
+
case "image":
|
|
295
|
+
return (cfg.maxImageSize || 100) * 1024 * 1024;
|
|
296
|
+
// 转换为字节
|
|
297
|
+
case "video":
|
|
298
|
+
return (cfg.maxVideoSize || 500) * 1024 * 1024;
|
|
299
|
+
case "file":
|
|
300
|
+
return (cfg.maxFileSize || 1e3) * 1024 * 1024;
|
|
301
|
+
case "record":
|
|
302
|
+
return (cfg.maxRecordSize || 200) * 1024 * 1024;
|
|
303
|
+
}
|
|
304
|
+
})();
|
|
305
|
+
try {
|
|
306
|
+
const files = await import_node_fs.promises.readdir(mediaDir);
|
|
307
|
+
if (files.length === 0) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const fileInfos = await Promise.all(
|
|
311
|
+
files.map(async (file) => {
|
|
312
|
+
const filePath = import_node_path.default.join(mediaDir, file);
|
|
313
|
+
const stats = await import_node_fs.promises.stat(filePath);
|
|
314
|
+
return {
|
|
315
|
+
path: filePath,
|
|
316
|
+
size: stats.size,
|
|
317
|
+
mtime: stats.mtimeMs
|
|
318
|
+
};
|
|
319
|
+
})
|
|
320
|
+
);
|
|
321
|
+
const totalSize = fileInfos.reduce((sum, file) => sum + file.size, 0);
|
|
322
|
+
ctx.logger.info(
|
|
323
|
+
`${type} directory total size: ${(totalSize / (1024 * 1024)).toFixed(2)} MB, max allowed: ${(maxSize / (1024 * 1024)).toFixed(2)} MB`
|
|
324
|
+
);
|
|
325
|
+
if (totalSize > maxSize) {
|
|
326
|
+
ctx.logger.warn(
|
|
327
|
+
`${type} directory size exceeds limit! Total: ${(totalSize / (1024 * 1024)).toFixed(2)} MB, Max: ${(maxSize / (1024 * 1024)).toFixed(2)} MB`
|
|
328
|
+
);
|
|
329
|
+
fileInfos.sort((a, b) => a.mtime - b.mtime);
|
|
330
|
+
let currentSize = totalSize;
|
|
331
|
+
let filesToDelete = [];
|
|
332
|
+
for (const file of fileInfos) {
|
|
333
|
+
if (currentSize <= maxSize) {
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
filesToDelete.push(file);
|
|
337
|
+
currentSize -= file.size;
|
|
338
|
+
}
|
|
339
|
+
for (const file of filesToDelete) {
|
|
340
|
+
await import_node_fs.promises.unlink(file.path);
|
|
341
|
+
ctx.logger.info(
|
|
342
|
+
`Deleted oldest ${type} file: ${import_node_path.default.basename(file.path)} (${(file.size / (1024 * 1024)).toFixed(2)} MB)`
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
ctx.logger.info(
|
|
346
|
+
`Cleanup completed. ${type} directory new size: ${(currentSize / (1024 * 1024)).toFixed(2)} MB`
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
} catch (err) {
|
|
350
|
+
ctx.logger.error(`Failed to check and clean ${type} files: ${err}`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
232
353
|
async function deleteMediaFilesFromMessage(ctx, content) {
|
|
233
354
|
try {
|
|
234
355
|
const elements = JSON.parse(content);
|
|
@@ -346,8 +467,62 @@ function formatDate(date) {
|
|
|
346
467
|
});
|
|
347
468
|
}
|
|
348
469
|
|
|
470
|
+
// src/forward-helper.ts
|
|
471
|
+
async function reconstructForwardMsg(ctx, session, message, cfg) {
|
|
472
|
+
return Promise.all(
|
|
473
|
+
message.map(async (msg) => {
|
|
474
|
+
const content = await processForwardMessageContent(ctx, session, msg, cfg);
|
|
475
|
+
const senderNickname = msg.sender.nickname;
|
|
476
|
+
let senderUserId = msg.sender.user_id;
|
|
477
|
+
senderUserId = senderUserId === 1094950020 ? await getUserIdFromNickname(session, senderNickname, senderUserId) : senderUserId;
|
|
478
|
+
return {
|
|
479
|
+
type: "node",
|
|
480
|
+
data: {
|
|
481
|
+
user_id: senderUserId,
|
|
482
|
+
nickname: senderNickname,
|
|
483
|
+
content
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
})
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
async function getUserIdFromNickname(session, nickname, userId) {
|
|
490
|
+
const memberInfos = await session.onebot.getGroupMemberList(session.channelId);
|
|
491
|
+
const matches = memberInfos.filter((m) => m.nickname === nickname);
|
|
492
|
+
if (matches.length === 1) {
|
|
493
|
+
return matches[0].user_id;
|
|
494
|
+
}
|
|
495
|
+
return userId;
|
|
496
|
+
}
|
|
497
|
+
async function processForwardMessageContent(ctx, session, msg, cfg) {
|
|
498
|
+
if (typeof msg.message === "string") {
|
|
499
|
+
return msg.message;
|
|
500
|
+
}
|
|
501
|
+
const firstElement = msg.message[0];
|
|
502
|
+
if (firstElement?.type === "forward") {
|
|
503
|
+
return reconstructForwardMsg(ctx, session, firstElement.data.content, cfg);
|
|
504
|
+
}
|
|
505
|
+
return Promise.all(
|
|
506
|
+
msg.message.map(async (element) => {
|
|
507
|
+
return processMediaElement(ctx, element, cfg);
|
|
508
|
+
})
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// src/msg-helper.ts
|
|
513
|
+
async function processMessageContent(ctx, msg, cfg) {
|
|
514
|
+
return Promise.all(
|
|
515
|
+
msg.map(async (element) => {
|
|
516
|
+
if (element.type === "reply") {
|
|
517
|
+
return element;
|
|
518
|
+
}
|
|
519
|
+
return processMediaElement(ctx, element, cfg);
|
|
520
|
+
})
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
|
|
349
524
|
// src/index.ts
|
|
350
|
-
var
|
|
525
|
+
var import_koishi_plugin_adapter_onebot2 = require("@pynickle/koishi-plugin-adapter-onebot");
|
|
351
526
|
var import_koishi = require("koishi");
|
|
352
527
|
var name = "echo-cave";
|
|
353
528
|
var inject = ["database"];
|
|
@@ -387,10 +562,8 @@ function apply(ctx, cfg) {
|
|
|
387
562
|
ctx.command("cave [id:number]").action(
|
|
388
563
|
async ({ session }, id) => await getCave(ctx, session, cfg, id)
|
|
389
564
|
);
|
|
390
|
-
ctx.command("cave.echo [userIds
|
|
391
|
-
async ({ session }, userIds) =>
|
|
392
|
-
ctx.logger.info(`User ${session.userId} is adding a cave message with related users: ${userIds}`);
|
|
393
|
-
}
|
|
565
|
+
ctx.command("cave.echo [...userIds]").action(
|
|
566
|
+
async ({ session }, ...userIds) => await addCave(ctx, session, cfg, userIds)
|
|
394
567
|
);
|
|
395
568
|
ctx.command("cave.wipe <id:number>").action(
|
|
396
569
|
async ({ session }, id) => await deleteCave(ctx, session, cfg, id)
|
|
@@ -402,6 +575,10 @@ function apply(ctx, cfg) {
|
|
|
402
575
|
ctx.command("cave.bind <id:number> <...userIds>", { authority: 4 }).action(
|
|
403
576
|
async ({ session }, id, ...userIds) => {
|
|
404
577
|
ctx.logger.info(`Binding users ${JSON.stringify(userIds)} to cave ID ${id}`);
|
|
578
|
+
for (const uid of userIds) {
|
|
579
|
+
ctx.logger.info(`User ID to bind: ${uid}`);
|
|
580
|
+
ctx.logger.info(`userid type: ${typeof uid}`);
|
|
581
|
+
}
|
|
405
582
|
await bindUsersToCave(ctx, session, id, userIds);
|
|
406
583
|
}
|
|
407
584
|
);
|
|
@@ -510,6 +687,73 @@ async function deleteCave(ctx, session, cfg, id) {
|
|
|
510
687
|
await ctx.database.remove("echo_cave", id);
|
|
511
688
|
return session.text(".msgDeleted", [id]);
|
|
512
689
|
}
|
|
690
|
+
async function addCave(ctx, session, cfg, userIds) {
|
|
691
|
+
if (!session.guildId) {
|
|
692
|
+
return session.text("echo-cave.general.privateChatReminder");
|
|
693
|
+
}
|
|
694
|
+
if (!session.quote) {
|
|
695
|
+
return session.text(".noMsgQuoted");
|
|
696
|
+
}
|
|
697
|
+
const { userId, channelId, quote } = session;
|
|
698
|
+
const messageId = quote.id;
|
|
699
|
+
let parsedUserIds = [];
|
|
700
|
+
if (userIds && userIds.length > 0) {
|
|
701
|
+
ctx.logger.info(`Original userIds in addCave: ${JSON.stringify(userIds)}`);
|
|
702
|
+
const result = parseUserIds(userIds);
|
|
703
|
+
if (result.error === "invalid_all_mention") {
|
|
704
|
+
return session.text(".invalidAllMention");
|
|
705
|
+
}
|
|
706
|
+
parsedUserIds = result.parsedUserIds;
|
|
707
|
+
const isAllUsersInGroup = await checkUsersInGroup(ctx, session, parsedUserIds);
|
|
708
|
+
if (!isAllUsersInGroup) {
|
|
709
|
+
return session.text(".userNotInGroup");
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
let content;
|
|
713
|
+
let type;
|
|
714
|
+
if (quote.elements[0].type === "forward") {
|
|
715
|
+
type = "forward";
|
|
716
|
+
const message = await reconstructForwardMsg(
|
|
717
|
+
ctx,
|
|
718
|
+
session,
|
|
719
|
+
await session.onebot.getForwardMsg(messageId),
|
|
720
|
+
cfg
|
|
721
|
+
);
|
|
722
|
+
content = JSON.stringify(message);
|
|
723
|
+
} else {
|
|
724
|
+
type = "msg";
|
|
725
|
+
const message = (await session.onebot.getMsg(messageId)).message;
|
|
726
|
+
let msgJson;
|
|
727
|
+
if (typeof message === "string") {
|
|
728
|
+
msgJson = import_koishi_plugin_adapter_onebot2.CQCode.parse(message);
|
|
729
|
+
} else {
|
|
730
|
+
if (message[0].type === "video" || message[0].type === "file") {
|
|
731
|
+
type = "forward";
|
|
732
|
+
}
|
|
733
|
+
msgJson = message;
|
|
734
|
+
}
|
|
735
|
+
content = JSON.stringify(await processMessageContent(ctx, msgJson, cfg));
|
|
736
|
+
}
|
|
737
|
+
await ctx.database.get("echo_cave", { content }).then((existing) => {
|
|
738
|
+
if (existing) {
|
|
739
|
+
return session.text(".existingMsg");
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
try {
|
|
743
|
+
const result = await ctx.database.create("echo_cave", {
|
|
744
|
+
channelId,
|
|
745
|
+
createTime: /* @__PURE__ */ new Date(),
|
|
746
|
+
userId,
|
|
747
|
+
originUserId: quote.user.id,
|
|
748
|
+
type,
|
|
749
|
+
content,
|
|
750
|
+
relatedUsers: parsedUserIds || []
|
|
751
|
+
});
|
|
752
|
+
return session.text(".msgSaved", [result.id]);
|
|
753
|
+
} catch (error) {
|
|
754
|
+
return session.text(".msgFailedToSave");
|
|
755
|
+
}
|
|
756
|
+
}
|
|
513
757
|
async function bindUsersToCave(ctx, session, id, userIds) {
|
|
514
758
|
if (!session.guildId) {
|
|
515
759
|
return session.text("echo-cave.general.privateChatReminder");
|
|
@@ -526,7 +770,6 @@ async function bindUsersToCave(ctx, session, id, userIds) {
|
|
|
526
770
|
return session.text(".invalidAllMention");
|
|
527
771
|
}
|
|
528
772
|
parsedUserIds = result.parsedUserIds;
|
|
529
|
-
ctx.logger.info(`Parsed userIds: ${JSON.stringify(parsedUserIds)}`);
|
|
530
773
|
const caves = await ctx.database.get("echo_cave", id);
|
|
531
774
|
if (caves.length === 0) {
|
|
532
775
|
return session.text("echo-cave.general.noMsgWithId");
|