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.
- package/lib/cqcode-helper.d.ts +1 -1
- package/lib/index.cjs +292 -39
- package/package.json +1 -1
package/lib/cqcode-helper.d.ts
CHANGED
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
|
|
157
|
+
var import_koishi_plugin_adapter_onebot = require("@pynickle/koishi-plugin-adapter-onebot");
|
|
158
158
|
|
|
159
159
|
// src/cqcode-helper.ts
|
|
160
|
-
var
|
|
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
|
|
171
|
+
for (const userId of userIds) {
|
|
172
172
|
try {
|
|
173
|
-
const
|
|
174
|
-
if (
|
|
175
|
-
|
|
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
|
-
|
|
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
|
|
351
|
-
var
|
|
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 =
|
|
355
|
-
adminMessageProtection:
|
|
356
|
-
allowContributorDelete:
|
|
357
|
-
allowSenderDelete:
|
|
358
|
-
deleteMediaWhenDeletingMsg:
|
|
359
|
-
enableSizeLimit:
|
|
360
|
-
maxImageSize:
|
|
361
|
-
maxVideoSize:
|
|
362
|
-
maxFileSize:
|
|
363
|
-
maxRecordSize:
|
|
364
|
-
useBase64ForMedia:
|
|
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
|
|
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");
|