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 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
- buffer = mediaBytes(file.data);
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 (uri.startsWith("data:")) return "data:...";
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
@@ -4,7 +4,7 @@
4
4
  "type": "git",
5
5
  "url": "https://github.com/incubator4/dynamic-bot-koishi"
6
6
  },
7
- "version": "0.0.6",
7
+ "version": "0.0.7",
8
8
  "license": "Apache-2.0",
9
9
  "contributors": [
10
10
  "AresWang <incubator4@outlook.com>"
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
- buffer = mediaBytes(file.data);
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 (uri.startsWith("data:")) return "data:...";
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