koishi-plugin-dynamic-bot 0.0.6 → 0.0.7
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/index.js +67 -13
- package/package.json +1 -1
- package/src/dbk/send.ts +78 -13
package/lib/index.js
CHANGED
|
@@ -6407,32 +6407,85 @@ async function forwardNodeElements(ctx, node, options) {
|
|
|
6407
6407
|
`), ...body];
|
|
6408
6408
|
}
|
|
6409
6409
|
async function fetchMediaElement(ctx, kind, uri) {
|
|
6410
|
-
const http = ctx.http;
|
|
6411
|
-
if (typeof http?.file !== "function") {
|
|
6412
|
-
throw mediaFetchError(kind, uri, new Error("Koishi http service is required to send media"));
|
|
6413
|
-
}
|
|
6414
6410
|
ctx.logger.debug("message.send: fetch %s %s", kind, displayUri(uri));
|
|
6415
|
-
let file;
|
|
6416
|
-
try {
|
|
6417
|
-
file = await http.file(uri);
|
|
6418
|
-
} catch (error) {
|
|
6419
|
-
throw mediaFetchError(kind, uri, error);
|
|
6420
|
-
}
|
|
6421
6411
|
let buffer;
|
|
6412
|
+
let mime;
|
|
6422
6413
|
try {
|
|
6423
|
-
|
|
6414
|
+
const inline = decodeInlineMedia(uri, kind);
|
|
6415
|
+
if (inline) {
|
|
6416
|
+
buffer = inline.buffer;
|
|
6417
|
+
mime = inline.mime;
|
|
6418
|
+
} else {
|
|
6419
|
+
const fetched = await fetchRemoteMedia(ctx, kind, uri);
|
|
6420
|
+
buffer = fetched.buffer;
|
|
6421
|
+
mime = fetched.mime;
|
|
6422
|
+
}
|
|
6424
6423
|
} catch (error) {
|
|
6425
6424
|
throw mediaFetchError(kind, uri, error);
|
|
6426
6425
|
}
|
|
6427
6426
|
if (buffer.length === 0) {
|
|
6428
6427
|
throw mediaFetchError(kind, uri, new Error("empty body"));
|
|
6429
6428
|
}
|
|
6430
|
-
const mime = (file.mime ?? file.type ?? "").trim() || MEDIA_FALLBACK_TYPE[kind];
|
|
6431
6429
|
ctx.logger.debug("message.send: fetched %s bytes=%d mime=%s", kind, buffer.length, mime);
|
|
6432
6430
|
if (kind === "image") return import_koishi.h.image(buffer, mime);
|
|
6433
6431
|
if (kind === "video") return import_koishi.h.video(buffer, mime);
|
|
6434
6432
|
return import_koishi.h.audio(buffer, mime);
|
|
6435
6433
|
}
|
|
6434
|
+
async function fetchRemoteMedia(ctx, kind, uri) {
|
|
6435
|
+
const http = ctx.http;
|
|
6436
|
+
if (typeof http?.file !== "function") {
|
|
6437
|
+
throw new Error("Koishi http service is required to send media");
|
|
6438
|
+
}
|
|
6439
|
+
const file = await http.file(uri);
|
|
6440
|
+
const buffer = mediaBytes(file.data);
|
|
6441
|
+
const mime = (file.mime ?? file.type ?? "").trim() || MEDIA_FALLBACK_TYPE[kind];
|
|
6442
|
+
return { buffer, mime };
|
|
6443
|
+
}
|
|
6444
|
+
function decodeInlineMedia(uri, kind) {
|
|
6445
|
+
if (/^data:/i.test(uri)) {
|
|
6446
|
+
const parsed = decodeDataUri(uri);
|
|
6447
|
+
if (!parsed) throw new Error("invalid data URI");
|
|
6448
|
+
return { buffer: parsed.buffer, mime: parsed.mime || sniffMime(parsed.buffer, kind) };
|
|
6449
|
+
}
|
|
6450
|
+
const base64 = /^base64:\/\//i.exec(uri);
|
|
6451
|
+
if (base64) {
|
|
6452
|
+
const buffer = decodeBase64Payload(uri.slice(base64[0].length));
|
|
6453
|
+
return { buffer, mime: sniffMime(buffer, kind) };
|
|
6454
|
+
}
|
|
6455
|
+
return void 0;
|
|
6456
|
+
}
|
|
6457
|
+
function decodeDataUri(uri) {
|
|
6458
|
+
const comma = uri.indexOf(",");
|
|
6459
|
+
if (comma < 5) return void 0;
|
|
6460
|
+
const parts = uri.slice(5, comma).split(";").map((part) => part.trim()).filter(Boolean);
|
|
6461
|
+
const isBase64 = parts.some((part) => part.toLowerCase() === "base64");
|
|
6462
|
+
const mime = parts.find((part) => part.includes("/")) ?? "";
|
|
6463
|
+
const payload = uri.slice(comma + 1);
|
|
6464
|
+
const buffer = isBase64 ? decodeBase64Payload(payload) : Buffer.from(decodeURIComponent(payload.replace(/\+/g, " ")));
|
|
6465
|
+
return { buffer, mime };
|
|
6466
|
+
}
|
|
6467
|
+
function decodeBase64Payload(payload) {
|
|
6468
|
+
const cleaned = payload.replace(/\s/g, "").replace(/-/g, "+").replace(/_/g, "/");
|
|
6469
|
+
if (!cleaned) throw new Error("empty base64 payload");
|
|
6470
|
+
const buffer = Buffer.from(cleaned, "base64");
|
|
6471
|
+
if (buffer.length === 0) throw new Error("invalid base64 payload");
|
|
6472
|
+
return buffer;
|
|
6473
|
+
}
|
|
6474
|
+
function sniffMime(buffer, kind) {
|
|
6475
|
+
if (buffer.length >= 8 && buffer[0] === 137 && buffer[1] === 80 && buffer[2] === 78 && buffer[3] === 71) {
|
|
6476
|
+
return "image/png";
|
|
6477
|
+
}
|
|
6478
|
+
if (buffer.length >= 3 && buffer[0] === 255 && buffer[1] === 216 && buffer[2] === 255) {
|
|
6479
|
+
return "image/jpeg";
|
|
6480
|
+
}
|
|
6481
|
+
if (buffer.length >= 6 && buffer.toString("ascii", 0, 6).startsWith("GIF8")) {
|
|
6482
|
+
return "image/gif";
|
|
6483
|
+
}
|
|
6484
|
+
if (buffer.length >= 12 && buffer.toString("ascii", 0, 4) === "RIFF" && buffer.toString("ascii", 8, 12) === "WEBP") {
|
|
6485
|
+
return "image/webp";
|
|
6486
|
+
}
|
|
6487
|
+
return MEDIA_FALLBACK_TYPE[kind];
|
|
6488
|
+
}
|
|
6436
6489
|
function mediaBytes(data) {
|
|
6437
6490
|
if (Buffer.isBuffer(data)) return data;
|
|
6438
6491
|
if (data instanceof ArrayBuffer) return Buffer.from(data);
|
|
@@ -6447,7 +6500,8 @@ function mediaFetchError(kind, uri, cause) {
|
|
|
6447
6500
|
return error;
|
|
6448
6501
|
}
|
|
6449
6502
|
function displayUri(uri) {
|
|
6450
|
-
if (
|
|
6503
|
+
if (/^data:/i.test(uri)) return "data:...";
|
|
6504
|
+
if (/^base64:\/\//i.test(uri)) return "base64://...";
|
|
6451
6505
|
return uri.length > 200 ? `${uri.slice(0, 200)}...` : uri;
|
|
6452
6506
|
}
|
|
6453
6507
|
function withQuote(elements, quoteId) {
|
package/package.json
CHANGED
package/src/dbk/send.ts
CHANGED
|
@@ -276,33 +276,97 @@ async function forwardNodeElements(
|
|
|
276
276
|
type MediaKind = keyof typeof MEDIA_FALLBACK_TYPE;
|
|
277
277
|
|
|
278
278
|
async function fetchMediaElement(ctx: Context, kind: MediaKind, uri: string): Promise<El> {
|
|
279
|
-
const http = ctx.http;
|
|
280
|
-
if (typeof http?.file !== "function") {
|
|
281
|
-
throw mediaFetchError(kind, uri, new Error("Koishi http service is required to send media"));
|
|
282
|
-
}
|
|
283
279
|
ctx.logger.debug("message.send: fetch %s %s", kind, displayUri(uri));
|
|
284
|
-
let file: { data?: unknown; mime?: string; type?: string };
|
|
285
|
-
try {
|
|
286
|
-
file = await http.file(uri);
|
|
287
|
-
} catch (error) {
|
|
288
|
-
throw mediaFetchError(kind, uri, error);
|
|
289
|
-
}
|
|
290
280
|
let buffer: Buffer;
|
|
281
|
+
let mime: string;
|
|
291
282
|
try {
|
|
292
|
-
|
|
283
|
+
const inline = decodeInlineMedia(uri, kind);
|
|
284
|
+
if (inline) {
|
|
285
|
+
buffer = inline.buffer;
|
|
286
|
+
mime = inline.mime;
|
|
287
|
+
} else {
|
|
288
|
+
const fetched = await fetchRemoteMedia(ctx, kind, uri);
|
|
289
|
+
buffer = fetched.buffer;
|
|
290
|
+
mime = fetched.mime;
|
|
291
|
+
}
|
|
293
292
|
} catch (error) {
|
|
294
293
|
throw mediaFetchError(kind, uri, error);
|
|
295
294
|
}
|
|
296
295
|
if (buffer.length === 0) {
|
|
297
296
|
throw mediaFetchError(kind, uri, new Error("empty body"));
|
|
298
297
|
}
|
|
299
|
-
const mime = (file.mime ?? file.type ?? "").trim() || MEDIA_FALLBACK_TYPE[kind];
|
|
300
298
|
ctx.logger.debug("message.send: fetched %s bytes=%d mime=%s", kind, buffer.length, mime);
|
|
301
299
|
if (kind === "image") return h.image(buffer, mime);
|
|
302
300
|
if (kind === "video") return h.video(buffer, mime);
|
|
303
301
|
return h.audio(buffer, mime);
|
|
304
302
|
}
|
|
305
303
|
|
|
304
|
+
async function fetchRemoteMedia(
|
|
305
|
+
ctx: Context,
|
|
306
|
+
kind: MediaKind,
|
|
307
|
+
uri: string,
|
|
308
|
+
): Promise<{ buffer: Buffer; mime: string }> {
|
|
309
|
+
const http = ctx.http;
|
|
310
|
+
if (typeof http?.file !== "function") {
|
|
311
|
+
throw new Error("Koishi http service is required to send media");
|
|
312
|
+
}
|
|
313
|
+
const file = await http.file(uri);
|
|
314
|
+
const buffer = mediaBytes(file.data);
|
|
315
|
+
const mime = (file.mime ?? file.type ?? "").trim() || MEDIA_FALLBACK_TYPE[kind];
|
|
316
|
+
return { buffer, mime };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function decodeInlineMedia(uri: string, kind: MediaKind): { buffer: Buffer; mime: string } | undefined {
|
|
320
|
+
if (/^data:/i.test(uri)) {
|
|
321
|
+
const parsed = decodeDataUri(uri);
|
|
322
|
+
if (!parsed) throw new Error("invalid data URI");
|
|
323
|
+
return { buffer: parsed.buffer, mime: parsed.mime || sniffMime(parsed.buffer, kind) };
|
|
324
|
+
}
|
|
325
|
+
const base64 = /^base64:\/\//i.exec(uri);
|
|
326
|
+
if (base64) {
|
|
327
|
+
const buffer = decodeBase64Payload(uri.slice(base64[0].length));
|
|
328
|
+
return { buffer, mime: sniffMime(buffer, kind) };
|
|
329
|
+
}
|
|
330
|
+
return undefined;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function decodeDataUri(uri: string): { buffer: Buffer; mime: string } | undefined {
|
|
334
|
+
const comma = uri.indexOf(",");
|
|
335
|
+
if (comma < 5) return undefined;
|
|
336
|
+
const parts = uri.slice(5, comma).split(";").map((part) => part.trim()).filter(Boolean);
|
|
337
|
+
const isBase64 = parts.some((part) => part.toLowerCase() === "base64");
|
|
338
|
+
const mime = parts.find((part) => part.includes("/")) ?? "";
|
|
339
|
+
const payload = uri.slice(comma + 1);
|
|
340
|
+
const buffer = isBase64
|
|
341
|
+
? decodeBase64Payload(payload)
|
|
342
|
+
: Buffer.from(decodeURIComponent(payload.replace(/\+/g, " ")));
|
|
343
|
+
return { buffer, mime };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function decodeBase64Payload(payload: string): Buffer {
|
|
347
|
+
const cleaned = payload.replace(/\s/g, "").replace(/-/g, "+").replace(/_/g, "/");
|
|
348
|
+
if (!cleaned) throw new Error("empty base64 payload");
|
|
349
|
+
const buffer = Buffer.from(cleaned, "base64");
|
|
350
|
+
if (buffer.length === 0) throw new Error("invalid base64 payload");
|
|
351
|
+
return buffer;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function sniffMime(buffer: Buffer, kind: MediaKind): string {
|
|
355
|
+
if (buffer.length >= 8 && buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47) {
|
|
356
|
+
return "image/png";
|
|
357
|
+
}
|
|
358
|
+
if (buffer.length >= 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) {
|
|
359
|
+
return "image/jpeg";
|
|
360
|
+
}
|
|
361
|
+
if (buffer.length >= 6 && buffer.toString("ascii", 0, 6).startsWith("GIF8")) {
|
|
362
|
+
return "image/gif";
|
|
363
|
+
}
|
|
364
|
+
if (buffer.length >= 12 && buffer.toString("ascii", 0, 4) === "RIFF" && buffer.toString("ascii", 8, 12) === "WEBP") {
|
|
365
|
+
return "image/webp";
|
|
366
|
+
}
|
|
367
|
+
return MEDIA_FALLBACK_TYPE[kind];
|
|
368
|
+
}
|
|
369
|
+
|
|
306
370
|
function mediaBytes(data: unknown): Buffer {
|
|
307
371
|
if (Buffer.isBuffer(data)) return data;
|
|
308
372
|
if (data instanceof ArrayBuffer) return Buffer.from(data);
|
|
@@ -319,7 +383,8 @@ function mediaFetchError(kind: MediaKind, uri: string, cause: unknown): Error {
|
|
|
319
383
|
}
|
|
320
384
|
|
|
321
385
|
function displayUri(uri: string): string {
|
|
322
|
-
if (
|
|
386
|
+
if (/^data:/i.test(uri)) return "data:...";
|
|
387
|
+
if (/^base64:\/\//i.test(uri)) return "base64://...";
|
|
323
388
|
return uri.length > 200 ? `${uri.slice(0, 200)}...` : uri;
|
|
324
389
|
}
|
|
325
390
|
|