@spectrum-ts/whatsapp-business 10.0.0 → 11.1.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.
Files changed (2) hide show
  1. package/dist/index.js +97 -19
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { TypedEventStream, button, buttons, createClient, list } from "@photon-ai/whatsapp-business";
2
2
  import { UnsupportedError, cloud, definePlatform, mergeStreams, stream } from "@spectrum-ts/core";
3
- import { asAttachment, asContact, asCustom, asPollOption, asReaction, asText, createLogger, errorAttrs, tracedFetch } from "@spectrum-ts/core/authoring";
3
+ import { asAttachment, asContact, asCustom, asGroup, asPollOption, asReaction, asText, asUnsend, createLogger, errorAttrs, tracedFetch } from "@spectrum-ts/core/authoring";
4
4
  import { extension } from "mime-types";
5
5
  import z from "zod";
6
6
  //#region src/auth.ts
@@ -296,6 +296,31 @@ const cachePoll = (client, messageId, poll) => {
296
296
  if (first !== void 0) cache.delete(first);
297
297
  }
298
298
  };
299
+ const MAX_REACTION_CACHE_SIZE = 1e3;
300
+ const reactionCaches = /* @__PURE__ */ new WeakMap();
301
+ const reactionCacheKey = (reactedId, from) => `${reactedId}:${from}`;
302
+ const cacheReaction = (client, reactedId, from, entry) => {
303
+ let cache = reactionCaches.get(client);
304
+ if (!cache) {
305
+ cache = /* @__PURE__ */ new Map();
306
+ reactionCaches.set(client, cache);
307
+ }
308
+ const key = reactionCacheKey(reactedId, from);
309
+ cache.delete(key);
310
+ cache.set(key, entry);
311
+ if (cache.size > MAX_REACTION_CACHE_SIZE) {
312
+ const first = cache.keys().next().value;
313
+ if (first !== void 0) cache.delete(first);
314
+ }
315
+ };
316
+ const getCachedReaction = (client, reactedId, from) => reactionCaches.get(client)?.get(reactionCacheKey(reactedId, from));
317
+ const reactionTargetStub = (reactedId) => ({
318
+ id: reactedId,
319
+ content: asCustom({
320
+ whatsapp_type: "reaction-target",
321
+ stub: true
322
+ })
323
+ });
299
324
  const optionIndexFromId = (id) => {
300
325
  if (!id.startsWith(OPTION_ID_PREFIX)) return;
301
326
  const index = Number(id.slice(4));
@@ -367,6 +392,14 @@ const waContactToSpectrum = (card) => {
367
392
  if (card.birthday) input.birthday = card.birthday;
368
393
  return asContact(input);
369
394
  };
395
+ const groupItem = (msg, index, content) => ({
396
+ id: `${msg.id}:${index}`,
397
+ content,
398
+ sender: { id: msg.from },
399
+ space: { id: msg.from },
400
+ timestamp: msg.timestamp
401
+ });
402
+ const parentWamid = (id) => id.split(":")[0] ?? id;
370
403
  const toMessages = (client, msg) => {
371
404
  const base = {
372
405
  sender: { id: msg.from },
@@ -387,6 +420,34 @@ const toMessages = (client, msg) => {
387
420
  content: mapContent(client, msg)
388
421
  }];
389
422
  };
423
+ const mapReactionContent = (client, msg, reaction) => {
424
+ const reactedId = reaction.messageId;
425
+ if (!reaction.emoji) {
426
+ const cached = getCachedReaction(client, reactedId, msg.from);
427
+ return asUnsend({ target: cached ? {
428
+ id: cached.id,
429
+ content: asReaction({
430
+ emoji: cached.emoji,
431
+ target: reactionTargetStub(reactedId)
432
+ })
433
+ } : {
434
+ id: `${reactedId}:reaction:${msg.from}`,
435
+ content: asCustom({
436
+ whatsapp_type: "reaction-removed",
437
+ messageId: reactedId,
438
+ stub: true
439
+ })
440
+ } });
441
+ }
442
+ cacheReaction(client, reactedId, msg.from, {
443
+ id: msg.id,
444
+ emoji: reaction.emoji
445
+ });
446
+ return asReaction({
447
+ emoji: reaction.emoji,
448
+ target: reactionTargetStub(reactedId)
449
+ });
450
+ };
390
451
  const mapContent = (client, msg) => {
391
452
  const { content } = msg;
392
453
  switch (content.type) {
@@ -394,7 +455,12 @@ const mapContent = (client, msg) => {
394
455
  case "image":
395
456
  case "video":
396
457
  case "audio":
397
- case "document": return lazyMedia(client, content.media);
458
+ case "document": {
459
+ const media = lazyMedia(client, content.media);
460
+ const caption = content.media.caption?.trim();
461
+ if (!caption) return media;
462
+ return asGroup({ items: [groupItem(msg, 0, media), groupItem(msg, 1, asText(caption))] });
463
+ }
398
464
  case "sticker": return asCustom({
399
465
  whatsapp_type: "sticker",
400
466
  ...content.sticker
@@ -403,19 +469,7 @@ const mapContent = (client, msg) => {
403
469
  whatsapp_type: "location",
404
470
  ...content.location
405
471
  });
406
- case "reaction": {
407
- const stubTarget = {
408
- id: content.reaction.messageId,
409
- content: asCustom({
410
- whatsapp_type: "reaction-target",
411
- stub: true
412
- })
413
- };
414
- return asReaction({
415
- emoji: content.reaction.emoji,
416
- target: stubTarget
417
- });
418
- }
472
+ case "reaction": return mapReactionContent(client, msg, content.reaction);
419
473
  case "interactive": {
420
474
  const inter = content.interactive;
421
475
  if (inter.type === "button_reply" || inter.type === "list_reply") {
@@ -547,7 +601,19 @@ const clientStream = (client) => {
547
601
  return stream((emit, end) => {
548
602
  const pump = (async () => {
549
603
  try {
550
- for await (const event of eventStream) for (const m of toMessages(client, event.message)) await emit(m);
604
+ for await (const event of eventStream) {
605
+ let mapped;
606
+ try {
607
+ mapped = toMessages(client, event.message);
608
+ } catch (error) {
609
+ streamLog.warn("skipping unmappable whatsapp message event", {
610
+ "spectrum.whatsapp.message_id": event.message.id,
611
+ ...errorAttrs(error)
612
+ }, error instanceof Error ? error : void 0);
613
+ continue;
614
+ }
615
+ for (const m of mapped) await emit(m);
616
+ }
551
617
  end();
552
618
  } catch (e) {
553
619
  end(e);
@@ -561,11 +627,23 @@ const clientStream = (client) => {
561
627
  };
562
628
  const messages = (clients) => mergeStreams(clients.map(clientStream));
563
629
  const send = async (clients, spaceId, content) => {
564
- if (content.type === "reply") return await replyToMessage(clients, spaceId, content.target.id, content.content);
630
+ if (content.type === "reply") return await replyToMessage(clients, spaceId, parentWamid(content.target.id), content.content);
565
631
  if (content.type === "reaction") return await reactToMessage(clients, spaceId, content);
566
632
  if (content.type === "typing") return;
567
633
  if (content.type === "read") {
568
- await primary(clients).messages.markRead(content.target.id);
634
+ await primary(clients).messages.markRead(parentWamid(content.target.id));
635
+ return;
636
+ }
637
+ if (content.type === "unsend") {
638
+ const unsent = content.target.content;
639
+ if (unsent.type !== "reaction") throw UnsupportedError.content(content.type);
640
+ await primary(clients).messages.send({
641
+ to: spaceId,
642
+ reaction: {
643
+ messageId: parentWamid(unsent.target.id),
644
+ emoji: ""
645
+ }
646
+ });
569
647
  return;
570
648
  }
571
649
  const client = primary(clients);
@@ -624,7 +702,7 @@ const reactToMessage = async (clients, spaceId, content) => {
624
702
  return toRecord(await primary(clients).messages.send({
625
703
  to: spaceId,
626
704
  reaction: {
627
- messageId: content.target.id,
705
+ messageId: parentWamid(content.target.id),
628
706
  emoji: content.emoji
629
707
  }
630
708
  }), spaceId, content);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spectrum-ts/whatsapp-business",
3
- "version": "10.0.0",
3
+ "version": "11.1.0",
4
4
  "description": "WhatsApp Business provider for spectrum-ts.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -36,7 +36,7 @@
36
36
  "zod": "^4.2.1"
37
37
  },
38
38
  "peerDependencies": {
39
- "@spectrum-ts/core": "^10.0.0",
39
+ "@spectrum-ts/core": "^11.0.0",
40
40
  "typescript": "^5 || ^6.0.0"
41
41
  },
42
42
  "license": "MIT"