koishi-plugin-echo-cave 1.16.11 → 1.16.13

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,10 @@ __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");
160
+ var import_koishi = require("koishi");
161
161
  function createTextMsg(content) {
162
162
  return {
163
163
  type: "text",
@@ -168,28 +168,17 @@ function createTextMsg(content) {
168
168
  }
169
169
  function parseUserIds(userIds) {
170
170
  const parsedUserIds = [];
171
- for (const userIdStr of userIds) {
171
+ for (const userId of userIds) {
172
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
- }
173
+ const element = import_koishi.h.parse(userId);
174
+ if (element.length === 1 && element[0].type === "at") {
175
+ parsedUserIds.push(element[0].attrs.id);
190
176
  }
191
177
  } catch (e) {
192
- parsedUserIds.push(userIdStr.split(":")[1]);
178
+ const num = Number(userId);
179
+ if (Number.isNaN(num)) {
180
+ parsedUserIds.push(userId);
181
+ }
193
182
  }
194
183
  }
195
184
  return {
@@ -200,6 +189,82 @@ function parseUserIds(userIds) {
200
189
  // src/media-helper.ts
201
190
  var import_axios = __toESM(require("axios"), 1);
202
191
  var import_node_fs = require("node:fs");
192
+ var import_node_path = __toESM(require("node:path"), 1);
193
+ async function saveMedia(ctx, mediaElement, type, cfg) {
194
+ const mediaUrl = mediaElement.url;
195
+ const originalMediaName = mediaElement.file;
196
+ const ext = (() => {
197
+ const i = originalMediaName.lastIndexOf(".");
198
+ return i === -1 ? type === "image" ? "png" : type === "video" ? "mp4" : type === "record" ? "mp3" : "bin" : originalMediaName.slice(i + 1).toLowerCase();
199
+ })();
200
+ const mediaDir = import_node_path.default.join(ctx.baseDir, "data", "cave", type + "s");
201
+ const mediaName = Date.now().toString();
202
+ const fullMediaPath = import_node_path.default.join(mediaDir, `${mediaName}.${ext}`);
203
+ ctx.logger.info(`Saving ${type} from ${mediaUrl} -> ${fullMediaPath}`);
204
+ try {
205
+ await import_node_fs.promises.mkdir(mediaDir, { recursive: true });
206
+ const res = await import_axios.default.get(mediaUrl, {
207
+ responseType: "arraybuffer",
208
+ validateStatus: () => true
209
+ });
210
+ if (res.status < 200 || res.status >= 300) {
211
+ ctx.logger.warn(
212
+ `${type.charAt(0).toUpperCase() + type.slice(1)} download failed: HTTP ${res.status}`
213
+ );
214
+ return mediaUrl;
215
+ }
216
+ const contentType = res.headers["content-type"];
217
+ if (contentType) {
218
+ if (type === "image" && !contentType.startsWith("image/")) {
219
+ ctx.logger.warn(`Invalid image content-type: ${contentType}`);
220
+ return mediaUrl;
221
+ }
222
+ if (type === "video" && !contentType.startsWith("video/")) {
223
+ ctx.logger.warn(`Invalid video content-type: ${contentType}`);
224
+ return mediaUrl;
225
+ }
226
+ if (type === "record" && !contentType.startsWith("audio/")) {
227
+ ctx.logger.warn(`Invalid record content-type: ${contentType}`);
228
+ return mediaUrl;
229
+ }
230
+ }
231
+ const buffer = Buffer.from(res.data);
232
+ if (!buffer || buffer.length === 0) {
233
+ ctx.logger.warn(`Downloaded ${type} buffer is empty`);
234
+ return mediaUrl;
235
+ }
236
+ await import_node_fs.promises.writeFile(fullMediaPath, buffer);
237
+ ctx.logger.info(
238
+ `${type.charAt(0).toUpperCase() + type.slice(1)} saved successfully: ${fullMediaPath}`
239
+ );
240
+ await checkAndCleanMediaFiles(ctx, cfg, type);
241
+ return fullMediaPath;
242
+ } catch (err) {
243
+ ctx.logger.error(`Failed to save ${type}: ${err}`);
244
+ return mediaUrl;
245
+ }
246
+ }
247
+ async function processMediaElement(ctx, element, cfg) {
248
+ if (element.type === "image" || element.type === "video" || element.type === "file" || element.type === "record") {
249
+ const savedPath = await saveMedia(
250
+ ctx,
251
+ element.data,
252
+ element.type,
253
+ cfg
254
+ );
255
+ const fileUri = `file:///${savedPath.replace(/\\/g, "/")}`;
256
+ return {
257
+ ...element,
258
+ data: {
259
+ ...element.data,
260
+ file: fileUri,
261
+ // Remove the url field
262
+ url: void 0
263
+ }
264
+ };
265
+ }
266
+ return element;
267
+ }
203
268
  async function convertFileUriToBase64(ctx, element) {
204
269
  if (element.type === "image" || element.type === "video" || element.type === "file" || element.type === "record") {
205
270
  const fileUri = element.data.file;
@@ -229,6 +294,72 @@ async function convertFileUriToBase64(ctx, element) {
229
294
  }
230
295
  return element;
231
296
  }
297
+ async function checkAndCleanMediaFiles(ctx, cfg, type) {
298
+ if (!cfg.enableSizeLimit) {
299
+ return;
300
+ }
301
+ const mediaDir = import_node_path.default.join(ctx.baseDir, "data", "cave", type + "s");
302
+ const maxSize = (() => {
303
+ switch (type) {
304
+ case "image":
305
+ return (cfg.maxImageSize || 100) * 1024 * 1024;
306
+ // 转换为字节
307
+ case "video":
308
+ return (cfg.maxVideoSize || 500) * 1024 * 1024;
309
+ case "file":
310
+ return (cfg.maxFileSize || 1e3) * 1024 * 1024;
311
+ case "record":
312
+ return (cfg.maxRecordSize || 200) * 1024 * 1024;
313
+ }
314
+ })();
315
+ try {
316
+ const files = await import_node_fs.promises.readdir(mediaDir);
317
+ if (files.length === 0) {
318
+ return;
319
+ }
320
+ const fileInfos = await Promise.all(
321
+ files.map(async (file) => {
322
+ const filePath = import_node_path.default.join(mediaDir, file);
323
+ const stats = await import_node_fs.promises.stat(filePath);
324
+ return {
325
+ path: filePath,
326
+ size: stats.size,
327
+ mtime: stats.mtimeMs
328
+ };
329
+ })
330
+ );
331
+ const totalSize = fileInfos.reduce((sum, file) => sum + file.size, 0);
332
+ ctx.logger.info(
333
+ `${type} directory total size: ${(totalSize / (1024 * 1024)).toFixed(2)} MB, max allowed: ${(maxSize / (1024 * 1024)).toFixed(2)} MB`
334
+ );
335
+ if (totalSize > maxSize) {
336
+ ctx.logger.warn(
337
+ `${type} directory size exceeds limit! Total: ${(totalSize / (1024 * 1024)).toFixed(2)} MB, Max: ${(maxSize / (1024 * 1024)).toFixed(2)} MB`
338
+ );
339
+ fileInfos.sort((a, b) => a.mtime - b.mtime);
340
+ let currentSize = totalSize;
341
+ let filesToDelete = [];
342
+ for (const file of fileInfos) {
343
+ if (currentSize <= maxSize) {
344
+ break;
345
+ }
346
+ filesToDelete.push(file);
347
+ currentSize -= file.size;
348
+ }
349
+ for (const file of filesToDelete) {
350
+ await import_node_fs.promises.unlink(file.path);
351
+ ctx.logger.info(
352
+ `Deleted oldest ${type} file: ${import_node_path.default.basename(file.path)} (${(file.size / (1024 * 1024)).toFixed(2)} MB)`
353
+ );
354
+ }
355
+ ctx.logger.info(
356
+ `Cleanup completed. ${type} directory new size: ${(currentSize / (1024 * 1024)).toFixed(2)} MB`
357
+ );
358
+ }
359
+ } catch (err) {
360
+ ctx.logger.error(`Failed to check and clean ${type} files: ${err}`);
361
+ }
362
+ }
232
363
  async function deleteMediaFilesFromMessage(ctx, content) {
233
364
  try {
234
365
  const elements = JSON.parse(content);
@@ -346,22 +477,76 @@ function formatDate(date) {
346
477
  });
347
478
  }
348
479
 
480
+ // src/forward-helper.ts
481
+ async function reconstructForwardMsg(ctx, session, message, cfg) {
482
+ return Promise.all(
483
+ message.map(async (msg) => {
484
+ const content = await processForwardMessageContent(ctx, session, msg, cfg);
485
+ const senderNickname = msg.sender.nickname;
486
+ let senderUserId = msg.sender.user_id;
487
+ senderUserId = senderUserId === 1094950020 ? await getUserIdFromNickname(session, senderNickname, senderUserId) : senderUserId;
488
+ return {
489
+ type: "node",
490
+ data: {
491
+ user_id: senderUserId,
492
+ nickname: senderNickname,
493
+ content
494
+ }
495
+ };
496
+ })
497
+ );
498
+ }
499
+ async function getUserIdFromNickname(session, nickname, userId) {
500
+ const memberInfos = await session.onebot.getGroupMemberList(session.channelId);
501
+ const matches = memberInfos.filter((m) => m.nickname === nickname);
502
+ if (matches.length === 1) {
503
+ return matches[0].user_id;
504
+ }
505
+ return userId;
506
+ }
507
+ async function processForwardMessageContent(ctx, session, msg, cfg) {
508
+ if (typeof msg.message === "string") {
509
+ return msg.message;
510
+ }
511
+ const firstElement = msg.message[0];
512
+ if (firstElement?.type === "forward") {
513
+ return reconstructForwardMsg(ctx, session, firstElement.data.content, cfg);
514
+ }
515
+ return Promise.all(
516
+ msg.message.map(async (element) => {
517
+ return processMediaElement(ctx, element, cfg);
518
+ })
519
+ );
520
+ }
521
+
522
+ // src/msg-helper.ts
523
+ async function processMessageContent(ctx, msg, cfg) {
524
+ return Promise.all(
525
+ msg.map(async (element) => {
526
+ if (element.type === "reply") {
527
+ return element;
528
+ }
529
+ return processMediaElement(ctx, element, cfg);
530
+ })
531
+ );
532
+ }
533
+
349
534
  // src/index.ts
350
- var import_koishi_plugin_adapter_onebot3 = require("@pynickle/koishi-plugin-adapter-onebot");
351
- var import_koishi = require("koishi");
535
+ var import_koishi_plugin_adapter_onebot2 = require("@pynickle/koishi-plugin-adapter-onebot");
536
+ var import_koishi2 = require("koishi");
352
537
  var name = "echo-cave";
353
538
  var inject = ["database"];
354
- var Config = import_koishi.Schema.object({
355
- adminMessageProtection: import_koishi.Schema.boolean().default(false),
356
- allowContributorDelete: import_koishi.Schema.boolean().default(true),
357
- allowSenderDelete: import_koishi.Schema.boolean().default(true),
358
- deleteMediaWhenDeletingMsg: import_koishi.Schema.boolean().default(true),
359
- enableSizeLimit: import_koishi.Schema.boolean().default(false),
360
- maxImageSize: import_koishi.Schema.number().default(2048),
361
- maxVideoSize: import_koishi.Schema.number().default(512),
362
- maxFileSize: import_koishi.Schema.number().default(512),
363
- maxRecordSize: import_koishi.Schema.number().default(512),
364
- useBase64ForMedia: import_koishi.Schema.boolean().default(false)
539
+ var Config = import_koishi2.Schema.object({
540
+ adminMessageProtection: import_koishi2.Schema.boolean().default(false),
541
+ allowContributorDelete: import_koishi2.Schema.boolean().default(true),
542
+ allowSenderDelete: import_koishi2.Schema.boolean().default(true),
543
+ deleteMediaWhenDeletingMsg: import_koishi2.Schema.boolean().default(true),
544
+ enableSizeLimit: import_koishi2.Schema.boolean().default(false),
545
+ maxImageSize: import_koishi2.Schema.number().default(2048),
546
+ maxVideoSize: import_koishi2.Schema.number().default(512),
547
+ maxFileSize: import_koishi2.Schema.number().default(512),
548
+ maxRecordSize: import_koishi2.Schema.number().default(512),
549
+ useBase64ForMedia: import_koishi2.Schema.boolean().default(false)
365
550
  }).i18n({
366
551
  "zh-CN": require_zh_CN()._config
367
552
  });
@@ -387,10 +572,8 @@ function apply(ctx, cfg) {
387
572
  ctx.command("cave [id:number]").action(
388
573
  async ({ session }, id) => await getCave(ctx, session, cfg, id)
389
574
  );
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
- }
575
+ ctx.command("cave.echo [...userIds]").action(
576
+ async ({ session }, ...userIds) => await addCave(ctx, session, cfg, userIds)
394
577
  );
395
578
  ctx.command("cave.wipe <id:number>").action(
396
579
  async ({ session }, id) => await deleteCave(ctx, session, cfg, id)
@@ -402,6 +585,10 @@ function apply(ctx, cfg) {
402
585
  ctx.command("cave.bind <id:number> <...userIds>", { authority: 4 }).action(
403
586
  async ({ session }, id, ...userIds) => {
404
587
  ctx.logger.info(`Binding users ${JSON.stringify(userIds)} to cave ID ${id}`);
588
+ for (const uid of userIds) {
589
+ ctx.logger.info(`User ID to bind: ${uid}`);
590
+ ctx.logger.info(`userid type: ${typeof uid}`);
591
+ }
405
592
  await bindUsersToCave(ctx, session, id, userIds);
406
593
  }
407
594
  );
@@ -510,6 +697,73 @@ async function deleteCave(ctx, session, cfg, id) {
510
697
  await ctx.database.remove("echo_cave", id);
511
698
  return session.text(".msgDeleted", [id]);
512
699
  }
700
+ async function addCave(ctx, session, cfg, userIds) {
701
+ if (!session.guildId) {
702
+ return session.text("echo-cave.general.privateChatReminder");
703
+ }
704
+ if (!session.quote) {
705
+ return session.text(".noMsgQuoted");
706
+ }
707
+ const { userId, channelId, quote } = session;
708
+ const messageId = quote.id;
709
+ let parsedUserIds = [];
710
+ if (userIds && userIds.length > 0) {
711
+ ctx.logger.info(`Original userIds in addCave: ${JSON.stringify(userIds)}`);
712
+ const result = parseUserIds(userIds);
713
+ if (result.error === "invalid_all_mention") {
714
+ return session.text(".invalidAllMention");
715
+ }
716
+ parsedUserIds = result.parsedUserIds;
717
+ const isAllUsersInGroup = await checkUsersInGroup(ctx, session, parsedUserIds);
718
+ if (!isAllUsersInGroup) {
719
+ return session.text(".userNotInGroup");
720
+ }
721
+ }
722
+ let content;
723
+ let type;
724
+ if (quote.elements[0].type === "forward") {
725
+ type = "forward";
726
+ const message = await reconstructForwardMsg(
727
+ ctx,
728
+ session,
729
+ await session.onebot.getForwardMsg(messageId),
730
+ cfg
731
+ );
732
+ content = JSON.stringify(message);
733
+ } else {
734
+ type = "msg";
735
+ const message = (await session.onebot.getMsg(messageId)).message;
736
+ let msgJson;
737
+ if (typeof message === "string") {
738
+ msgJson = import_koishi_plugin_adapter_onebot2.CQCode.parse(message);
739
+ } else {
740
+ if (message[0].type === "video" || message[0].type === "file") {
741
+ type = "forward";
742
+ }
743
+ msgJson = message;
744
+ }
745
+ content = JSON.stringify(await processMessageContent(ctx, msgJson, cfg));
746
+ }
747
+ await ctx.database.get("echo_cave", { content }).then((existing) => {
748
+ if (existing) {
749
+ return session.text(".existingMsg");
750
+ }
751
+ });
752
+ try {
753
+ const result = await ctx.database.create("echo_cave", {
754
+ channelId,
755
+ createTime: /* @__PURE__ */ new Date(),
756
+ userId,
757
+ originUserId: quote.user.id,
758
+ type,
759
+ content,
760
+ relatedUsers: parsedUserIds || []
761
+ });
762
+ return session.text(".msgSaved", [result.id]);
763
+ } catch (error) {
764
+ return session.text(".msgFailedToSave");
765
+ }
766
+ }
513
767
  async function bindUsersToCave(ctx, session, id, userIds) {
514
768
  if (!session.guildId) {
515
769
  return session.text("echo-cave.general.privateChatReminder");
@@ -526,7 +780,6 @@ async function bindUsersToCave(ctx, session, id, userIds) {
526
780
  return session.text(".invalidAllMention");
527
781
  }
528
782
  parsedUserIds = result.parsedUserIds;
529
- ctx.logger.info(`Parsed userIds: ${JSON.stringify(parsedUserIds)}`);
530
783
  const caves = await ctx.database.get("echo_cave", id);
531
784
  if (caves.length === 0) {
532
785
  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.13",
5
5
  "main": "lib/index.cjs",
6
6
  "typings": "lib/index.d.ts",
7
7
  "type": "module",