koishi-plugin-echo-cave 1.10.4 → 1.12.0
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/forward-helper.d.ts +2 -1
- package/lib/image-helper.d.ts +4 -1
- package/lib/index.cjs +148 -64
- package/lib/index.d.ts +4 -0
- package/lib/msg-helper.d.ts +2 -1
- package/package.json +1 -1
package/lib/forward-helper.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
+
import { Config } from './index';
|
|
1
2
|
import { CQCode } from '@pynickle/koishi-plugin-adapter-onebot';
|
|
2
3
|
import { Message } from '@pynickle/koishi-plugin-adapter-onebot/lib/types';
|
|
3
4
|
import { Context } from 'koishi';
|
|
4
|
-
export declare function reconstructForwardMsg(ctx: Context, message: Message[]): Promise<CQCode[]>;
|
|
5
|
+
export declare function reconstructForwardMsg(ctx: Context, message: Message[], cfg: Config): Promise<CQCode[]>;
|
package/lib/image-helper.d.ts
CHANGED
|
@@ -1,2 +1,5 @@
|
|
|
1
|
+
import { Config } from './index';
|
|
1
2
|
import { Context } from 'koishi';
|
|
2
|
-
export declare function
|
|
3
|
+
export declare function saveMedia(ctx: Context, mediaElement: Record<string, any>, type: 'image' | 'video' | 'file', cfg: Config): Promise<string>;
|
|
4
|
+
export declare function processMediaElement(ctx: Context, element: any, cfg: Config): Promise<any>;
|
|
5
|
+
export declare function checkAndCleanMediaFiles(ctx: Context, cfg: Config, type: 'image' | 'video' | 'file'): Promise<void>;
|
package/lib/index.cjs
CHANGED
|
@@ -36,7 +36,11 @@ var require_zh_CN = __commonJS({
|
|
|
36
36
|
_config: {
|
|
37
37
|
adminMessageProtection: "\u5F00\u542F\u7BA1\u7406\u5458\u6D88\u606F\u4FDD\u62A4\uFF0C\u5F00\u542F\u540E\u7BA1\u7406\u5458\u53D1\u5E03\u7684\u6D88\u606F\u53EA\u80FD\u7531\u7BA1\u7406\u5458\u5220\u9664",
|
|
38
38
|
allowContributorDelete: "\u5141\u8BB8\u6295\u7A3F\u8005\u5220\u9664\u81EA\u5DF1\u6295\u7A3F\u7684\u56DE\u58F0\u6D1E",
|
|
39
|
-
allowSenderDelete: "\u5141\u8BB8\u539F\u59CB\u53D1\u9001\u8005\u5220\u9664\u88AB\u6295\u7A3F\u7684\u56DE\u58F0\u6D1E"
|
|
39
|
+
allowSenderDelete: "\u5141\u8BB8\u539F\u59CB\u53D1\u9001\u8005\u5220\u9664\u88AB\u6295\u7A3F\u7684\u56DE\u58F0\u6D1E",
|
|
40
|
+
enableSizeLimit: "\u662F\u5426\u542F\u7528\u5A92\u4F53\u6587\u4EF6\u5927\u5C0F\u9650\u5236",
|
|
41
|
+
maxImageSize: "\u6700\u5927\u56FE\u7247\u5927\u5C0F (MB)",
|
|
42
|
+
maxVideoSize: "\u6700\u5927\u89C6\u9891\u5927\u5C0F (MB)",
|
|
43
|
+
maxFileSize: "\u6700\u5927\u6587\u4EF6\u5927\u5C0F (MB)"
|
|
40
44
|
},
|
|
41
45
|
"echo-cave": {
|
|
42
46
|
general: {
|
|
@@ -219,51 +223,143 @@ function formatDate(date) {
|
|
|
219
223
|
var import_axios = __toESM(require("axios"), 1);
|
|
220
224
|
var import_node_fs = require("node:fs");
|
|
221
225
|
var import_node_path = __toESM(require("node:path"), 1);
|
|
222
|
-
async function
|
|
223
|
-
const
|
|
224
|
-
const
|
|
226
|
+
async function saveMedia(ctx, mediaElement, type, cfg) {
|
|
227
|
+
const mediaUrl = mediaElement.url;
|
|
228
|
+
const originalMediaName = mediaElement.file;
|
|
225
229
|
const ext = (() => {
|
|
226
|
-
const i =
|
|
227
|
-
return i === -1 ? "png" :
|
|
230
|
+
const i = originalMediaName.lastIndexOf(".");
|
|
231
|
+
return i === -1 ? type === "image" ? "png" : type === "video" ? "mp4" : "bin" : originalMediaName.slice(i + 1).toLowerCase();
|
|
228
232
|
})();
|
|
229
|
-
const
|
|
230
|
-
const
|
|
231
|
-
const
|
|
232
|
-
ctx.logger.info(`Saving
|
|
233
|
+
const mediaDir = import_node_path.default.join(ctx.baseDir, "data", "cave", type + "s");
|
|
234
|
+
const mediaName = Date.now().toString();
|
|
235
|
+
const fullMediaPath = import_node_path.default.join(mediaDir, `${mediaName}.${ext}`);
|
|
236
|
+
ctx.logger.info(`Saving ${type} from ${mediaUrl} -> ${fullMediaPath}`);
|
|
233
237
|
try {
|
|
234
|
-
await import_node_fs.promises.mkdir(
|
|
235
|
-
const res = await import_axios.default.get(
|
|
238
|
+
await import_node_fs.promises.mkdir(mediaDir, { recursive: true });
|
|
239
|
+
const res = await import_axios.default.get(mediaUrl, {
|
|
236
240
|
responseType: "arraybuffer",
|
|
237
241
|
validateStatus: () => true
|
|
238
242
|
});
|
|
239
243
|
if (res.status < 200 || res.status >= 300) {
|
|
240
|
-
ctx.logger.warn(
|
|
241
|
-
|
|
244
|
+
ctx.logger.warn(
|
|
245
|
+
`${type.charAt(0).toUpperCase() + type.slice(1)} download failed: HTTP ${res.status}`
|
|
246
|
+
);
|
|
247
|
+
return mediaUrl;
|
|
242
248
|
}
|
|
243
|
-
const
|
|
244
|
-
if (
|
|
245
|
-
|
|
246
|
-
|
|
249
|
+
const contentType = res.headers["content-type"];
|
|
250
|
+
if (contentType) {
|
|
251
|
+
if (type === "image" && !contentType.startsWith("image/")) {
|
|
252
|
+
ctx.logger.warn(`Invalid image content-type: ${contentType}`);
|
|
253
|
+
return mediaUrl;
|
|
254
|
+
}
|
|
255
|
+
if (type === "video" && !contentType.startsWith("video/")) {
|
|
256
|
+
ctx.logger.warn(`Invalid video content-type: ${contentType}`);
|
|
257
|
+
return mediaUrl;
|
|
258
|
+
}
|
|
247
259
|
}
|
|
248
260
|
const buffer = Buffer.from(res.data);
|
|
249
261
|
if (!buffer || buffer.length === 0) {
|
|
250
|
-
ctx.logger.warn(
|
|
251
|
-
return
|
|
262
|
+
ctx.logger.warn(`Downloaded ${type} buffer is empty`);
|
|
263
|
+
return mediaUrl;
|
|
264
|
+
}
|
|
265
|
+
await import_node_fs.promises.writeFile(fullMediaPath, buffer);
|
|
266
|
+
ctx.logger.info(
|
|
267
|
+
`${type.charAt(0).toUpperCase() + type.slice(1)} saved successfully: ${fullMediaPath}`
|
|
268
|
+
);
|
|
269
|
+
await checkAndCleanMediaFiles(ctx, cfg, type);
|
|
270
|
+
return fullMediaPath;
|
|
271
|
+
} catch (err) {
|
|
272
|
+
ctx.logger.error(`Failed to save ${type}: ${err}`);
|
|
273
|
+
return mediaUrl;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
async function processMediaElement(ctx, element, cfg) {
|
|
277
|
+
if (element.type === "image" || element.type === "video" || element.type === "file") {
|
|
278
|
+
return {
|
|
279
|
+
...element,
|
|
280
|
+
data: {
|
|
281
|
+
...element.data,
|
|
282
|
+
url: await saveMedia(
|
|
283
|
+
ctx,
|
|
284
|
+
element.data,
|
|
285
|
+
element.type,
|
|
286
|
+
cfg
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
return element;
|
|
292
|
+
}
|
|
293
|
+
async function checkAndCleanMediaFiles(ctx, cfg, type) {
|
|
294
|
+
if (!cfg.enableSizeLimit) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const mediaDir = import_node_path.default.join(ctx.baseDir, "data", "cave", type + "s");
|
|
298
|
+
const maxSize = (() => {
|
|
299
|
+
switch (type) {
|
|
300
|
+
case "image":
|
|
301
|
+
return (cfg.maxImageSize || 100) * 1024 * 1024;
|
|
302
|
+
// 转换为字节
|
|
303
|
+
case "video":
|
|
304
|
+
return (cfg.maxVideoSize || 500) * 1024 * 1024;
|
|
305
|
+
case "file":
|
|
306
|
+
return (cfg.maxFileSize || 1e3) * 1024 * 1024;
|
|
307
|
+
}
|
|
308
|
+
})();
|
|
309
|
+
try {
|
|
310
|
+
const files = await import_node_fs.promises.readdir(mediaDir);
|
|
311
|
+
if (files.length === 0) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const fileInfos = await Promise.all(
|
|
315
|
+
files.map(async (file) => {
|
|
316
|
+
const filePath = import_node_path.default.join(mediaDir, file);
|
|
317
|
+
const stats = await import_node_fs.promises.stat(filePath);
|
|
318
|
+
return {
|
|
319
|
+
path: filePath,
|
|
320
|
+
size: stats.size,
|
|
321
|
+
mtime: stats.mtimeMs
|
|
322
|
+
};
|
|
323
|
+
})
|
|
324
|
+
);
|
|
325
|
+
const totalSize = fileInfos.reduce((sum, file) => sum + file.size, 0);
|
|
326
|
+
ctx.logger.info(
|
|
327
|
+
`${type} directory total size: ${(totalSize / (1024 * 1024)).toFixed(2)} MB, max allowed: ${(maxSize / (1024 * 1024)).toFixed(2)} MB`
|
|
328
|
+
);
|
|
329
|
+
if (totalSize > maxSize) {
|
|
330
|
+
ctx.logger.warn(
|
|
331
|
+
`${type} directory size exceeds limit! Total: ${(totalSize / (1024 * 1024)).toFixed(2)} MB, Max: ${(maxSize / (1024 * 1024)).toFixed(2)} MB`
|
|
332
|
+
);
|
|
333
|
+
fileInfos.sort((a, b) => a.mtime - b.mtime);
|
|
334
|
+
let currentSize = totalSize;
|
|
335
|
+
let filesToDelete = [];
|
|
336
|
+
for (const file of fileInfos) {
|
|
337
|
+
if (currentSize <= maxSize) {
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
filesToDelete.push(file);
|
|
341
|
+
currentSize -= file.size;
|
|
342
|
+
}
|
|
343
|
+
for (const file of filesToDelete) {
|
|
344
|
+
await import_node_fs.promises.unlink(file.path);
|
|
345
|
+
ctx.logger.info(
|
|
346
|
+
`Deleted oldest ${type} file: ${import_node_path.default.basename(file.path)} (${(file.size / (1024 * 1024)).toFixed(2)} MB)`
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
ctx.logger.info(
|
|
350
|
+
`Cleanup completed. ${type} directory new size: ${(currentSize / (1024 * 1024)).toFixed(2)} MB`
|
|
351
|
+
);
|
|
252
352
|
}
|
|
253
|
-
await import_node_fs.promises.writeFile(fullImgPath, buffer);
|
|
254
|
-
ctx.logger.info(`Image saved successfully: ${fullImgPath}`);
|
|
255
|
-
return fullImgPath;
|
|
256
353
|
} catch (err) {
|
|
257
|
-
ctx.logger.error(`Failed to
|
|
258
|
-
return imgUrl;
|
|
354
|
+
ctx.logger.error(`Failed to check and clean ${type} files: ${err}`);
|
|
259
355
|
}
|
|
260
356
|
}
|
|
261
357
|
|
|
262
358
|
// src/forward-helper.ts
|
|
263
|
-
async function reconstructForwardMsg(ctx, message) {
|
|
359
|
+
async function reconstructForwardMsg(ctx, message, cfg) {
|
|
264
360
|
return Promise.all(
|
|
265
361
|
message.map(async (msg) => {
|
|
266
|
-
const content = await processForwardMessageContent(ctx, msg);
|
|
362
|
+
const content = await processForwardMessageContent(ctx, msg, cfg);
|
|
267
363
|
return {
|
|
268
364
|
type: "node",
|
|
269
365
|
data: {
|
|
@@ -275,51 +371,31 @@ async function reconstructForwardMsg(ctx, message) {
|
|
|
275
371
|
})
|
|
276
372
|
);
|
|
277
373
|
}
|
|
278
|
-
async function processForwardMessageContent(ctx, msg) {
|
|
374
|
+
async function processForwardMessageContent(ctx, msg, cfg) {
|
|
279
375
|
if (typeof msg.message === "string") {
|
|
280
376
|
return msg.message;
|
|
281
377
|
}
|
|
282
378
|
const firstElement = msg.message[0];
|
|
283
379
|
if (firstElement?.type === "forward") {
|
|
284
|
-
return reconstructForwardMsg(ctx, firstElement.data.content);
|
|
380
|
+
return reconstructForwardMsg(ctx, firstElement.data.content, cfg);
|
|
285
381
|
}
|
|
286
382
|
return Promise.all(
|
|
287
383
|
msg.message.map(async (element) => {
|
|
288
|
-
|
|
289
|
-
return {
|
|
290
|
-
...element,
|
|
291
|
-
data: {
|
|
292
|
-
...element.data,
|
|
293
|
-
url: await saveImages(ctx, element.data)
|
|
294
|
-
}
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
return element;
|
|
384
|
+
return processMediaElement(ctx, element, cfg);
|
|
298
385
|
})
|
|
299
386
|
);
|
|
300
387
|
}
|
|
301
388
|
|
|
302
389
|
// src/msg-helper.ts
|
|
303
|
-
async function processMessageContent(ctx, msg) {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
...element,
|
|
313
|
-
data: {
|
|
314
|
-
...element.data,
|
|
315
|
-
url: newUrl
|
|
316
|
-
}
|
|
317
|
-
});
|
|
318
|
-
} else {
|
|
319
|
-
result.push(element);
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
return result;
|
|
390
|
+
async function processMessageContent(ctx, msg, cfg) {
|
|
391
|
+
return Promise.all(
|
|
392
|
+
msg.map(async (element) => {
|
|
393
|
+
if (element.type === "reply") {
|
|
394
|
+
return element;
|
|
395
|
+
}
|
|
396
|
+
return processMediaElement(ctx, element, cfg);
|
|
397
|
+
})
|
|
398
|
+
);
|
|
323
399
|
}
|
|
324
400
|
|
|
325
401
|
// src/index.ts
|
|
@@ -332,7 +408,11 @@ var inject = ["database"];
|
|
|
332
408
|
var Config = import_koishi.Schema.object({
|
|
333
409
|
adminMessageProtection: import_koishi.Schema.boolean().default(false),
|
|
334
410
|
allowContributorDelete: import_koishi.Schema.boolean().default(true),
|
|
335
|
-
allowSenderDelete: import_koishi.Schema.boolean().default(true)
|
|
411
|
+
allowSenderDelete: import_koishi.Schema.boolean().default(true),
|
|
412
|
+
enableSizeLimit: import_koishi.Schema.boolean().default(false),
|
|
413
|
+
maxImageSize: import_koishi.Schema.number().default(2048),
|
|
414
|
+
maxVideoSize: import_koishi.Schema.number().default(512),
|
|
415
|
+
maxFileSize: import_koishi.Schema.number().default(512)
|
|
336
416
|
}).i18n({
|
|
337
417
|
"zh-CN": require_zh_CN()._config
|
|
338
418
|
});
|
|
@@ -361,7 +441,7 @@ function apply(ctx, cfg) {
|
|
|
361
441
|
ctx.command("cave [id:number]").action(
|
|
362
442
|
async ({ session }, id) => await getCave(ctx, session, id)
|
|
363
443
|
);
|
|
364
|
-
ctx.command("cave.echo").action(async ({ session }) => await addCave(ctx, session));
|
|
444
|
+
ctx.command("cave.echo").action(async ({ session }) => await addCave(ctx, session, cfg));
|
|
365
445
|
ctx.command("cave.wipe <id:number>").action(
|
|
366
446
|
async ({ session }, id) => await deleteCave(ctx, session, cfg, id)
|
|
367
447
|
);
|
|
@@ -471,7 +551,7 @@ async function deleteCave(ctx, session, cfg, id) {
|
|
|
471
551
|
await ctx.database.remove("echo_cave", id);
|
|
472
552
|
return session.text(".msgDeleted", [id]);
|
|
473
553
|
}
|
|
474
|
-
async function addCave(ctx, session) {
|
|
554
|
+
async function addCave(ctx, session, cfg) {
|
|
475
555
|
if (!session.guildId) {
|
|
476
556
|
return session.text("echo-cave.general.privateChatReminder");
|
|
477
557
|
}
|
|
@@ -486,7 +566,8 @@ async function addCave(ctx, session) {
|
|
|
486
566
|
type = "forward";
|
|
487
567
|
const message = await reconstructForwardMsg(
|
|
488
568
|
ctx,
|
|
489
|
-
await session.onebot.getForwardMsg(messageId)
|
|
569
|
+
await session.onebot.getForwardMsg(messageId),
|
|
570
|
+
cfg
|
|
490
571
|
);
|
|
491
572
|
content = JSON.stringify(message);
|
|
492
573
|
} else {
|
|
@@ -496,9 +577,12 @@ async function addCave(ctx, session) {
|
|
|
496
577
|
if (typeof message === "string") {
|
|
497
578
|
msgJson = import_koishi_plugin_adapter_onebot2.CQCode.parse(message);
|
|
498
579
|
} else {
|
|
580
|
+
if (message[0].type === "video" || message[0].type === "file") {
|
|
581
|
+
type = "forward";
|
|
582
|
+
}
|
|
499
583
|
msgJson = message;
|
|
500
584
|
}
|
|
501
|
-
content = JSON.stringify(await processMessageContent(ctx, msgJson));
|
|
585
|
+
content = JSON.stringify(await processMessageContent(ctx, msgJson, cfg));
|
|
502
586
|
}
|
|
503
587
|
await ctx.database.get("echo_cave", { content }).then((existing) => {
|
|
504
588
|
if (existing) {
|
package/lib/index.d.ts
CHANGED
|
@@ -6,6 +6,10 @@ export interface Config {
|
|
|
6
6
|
adminMessageProtection?: boolean;
|
|
7
7
|
allowContributorDelete?: boolean;
|
|
8
8
|
allowSenderDelete?: boolean;
|
|
9
|
+
enableSizeLimit?: boolean;
|
|
10
|
+
maxImageSize?: number;
|
|
11
|
+
maxVideoSize?: number;
|
|
12
|
+
maxFileSize?: number;
|
|
9
13
|
}
|
|
10
14
|
export declare const Config: Schema<Config>;
|
|
11
15
|
export interface EchoCave {
|
package/lib/msg-helper.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Config } from './index';
|
|
1
2
|
import { CQCode } from '@pynickle/koishi-plugin-adapter-onebot';
|
|
2
3
|
import { Context } from 'koishi';
|
|
3
|
-
export declare function processMessageContent(ctx: Context, msg: CQCode[]): Promise<CQCode[]>;
|
|
4
|
+
export declare function processMessageContent(ctx: Context, msg: CQCode[], cfg: Config): Promise<CQCode[]>;
|