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.
@@ -8,4 +8,4 @@ export declare function createTextMsg(content: string): {
8
8
  text: string;
9
9
  };
10
10
  };
11
- export declare function parseUserIds(userIds: string[]): ParseResult;
11
+ export declare function parseUserIds(userIds: any[]): ParseResult;
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 import_koishi_plugin_adapter_onebot2 = require("@pynickle/koishi-plugin-adapter-onebot");
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 userIdStr of userIds) {
172
- try {
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 import_koishi_plugin_adapter_onebot3 = require("@pynickle/koishi-plugin-adapter-onebot");
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:text]").action(
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");
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-echo-cave",
3
3
  "description": "Group echo cave",
4
- "version": "1.16.11",
4
+ "version": "1.16.12",
5
5
  "main": "lib/index.cjs",
6
6
  "typings": "lib/index.d.ts",
7
7
  "type": "module",