@spectrum-ts/imessage 8.1.1 → 8.2.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 +105 -18
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { MessageEffect, NotFoundError, ValidationError, createClient } from "@ph
2
2
  import { IMessageSDK } from "@photon-ai/imessage-kit";
3
3
  import { sanitizePhone, withSpan } from "@photon-ai/otel";
4
4
  import { UnsupportedError, appLayoutSchema, cloud, definePlatform, fromVCard, mergeStreams, read, stream, text, toVCard } from "@spectrum-ts/core";
5
- import { asAttachment, asContact, asCustom, asGroup, asPoll, asPollOption, asText, buildPhotoAction, createLogger, ensureM4a, errorAttrs, groupSchema, messageEffectSchema, photoActionSchema, reactionSchema, resumableOrderedStream, sanitizeErrorMessage } from "@spectrum-ts/core/authoring";
5
+ import { asAttachment, asContact, asCustom, asGroup, asPoll, asPollOption, asReply, asText, buildPhotoAction, createLogger, ensureM4a, errorAttrs, groupSchema, messageEffectSchema, photoActionSchema, reactionSchema, resumableOrderedStream, sanitizeErrorMessage } from "@spectrum-ts/core/authoring";
6
6
  import z from "zod";
7
7
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
8
8
  import { createReadStream } from "node:fs";
@@ -530,6 +530,26 @@ const ATTACHMENT_JOIN_RETRY_LIMIT = 8;
530
530
  const ATTACHMENT_JOIN_FETCH_LIMIT = 10;
531
531
  const hasAttachmentPlaceholder = (message) => message.text?.includes("") ?? false;
532
532
  const isPendingAttachmentJoin = (message) => message.attachments.length === 0 && (message.hasAttachments || hasAttachmentPlaceholder(message));
533
+ const replyTargetId = (message) => message.threadRootMessageId ?? void 0;
534
+ const stubReplyTarget$1 = (space, targetId) => ({
535
+ id: targetId,
536
+ content: asCustom({
537
+ imessage_type: "reply-target",
538
+ stub: true
539
+ }),
540
+ space
541
+ });
542
+ const asProviderReply$1 = (content, target) => asReply({
543
+ content,
544
+ target
545
+ });
546
+ const wrapReply = (messages, targetId) => {
547
+ if (!targetId) return messages;
548
+ return messages.map((message) => ({
549
+ ...message,
550
+ content: asProviderReply$1(message.content, stubReplyTarget$1(message.space, targetId))
551
+ }));
552
+ };
533
553
  const refetchUntilAttachmentsSettle = async (client, message) => {
534
554
  if (!message.chatId) return message;
535
555
  for (let attempt = 0; attempt < ATTACHMENT_JOIN_RETRY_LIMIT; attempt += 1) {
@@ -566,12 +586,13 @@ const toMessages = async (message) => {
566
586
  },
567
587
  timestamp: message.createdAt
568
588
  };
589
+ const targetId = replyTargetId(message);
569
590
  if (message.attachments.length > 0) {
570
- if (!hasUsableTextPart(message.text)) return Promise.all(message.attachments.map(async (att) => ({
591
+ if (!hasUsableTextPart(message.text)) return wrapReply(await Promise.all(message.attachments.map(async (att) => ({
571
592
  ...base,
572
593
  id: `${message.id}:${att.id}`,
573
594
  content: await localAttachmentContent(att)
574
- })));
595
+ }))), targetId);
575
596
  const parts = toOrderedParts(message.text, message.attachments);
576
597
  const messages = [];
577
598
  for (let i = 0; i < parts.length; i++) {
@@ -589,16 +610,16 @@ const toMessages = async (message) => {
589
610
  partIndex: i
590
611
  });
591
612
  }
592
- return messages;
613
+ return wrapReply(messages, targetId);
593
614
  }
594
- return [{
615
+ return wrapReply([{
595
616
  ...base,
596
617
  id: message.id,
597
618
  content: {
598
619
  type: "text",
599
620
  text: message.text ?? ""
600
621
  }
601
- }];
622
+ }], targetId);
602
623
  };
603
624
  const messages$3 = (client) => stream((emit, end) => {
604
625
  let lastPromise = Promise.resolve();
@@ -881,6 +902,10 @@ const asProviderGroup = (items) => groupSchema.parse({
881
902
  type: "group",
882
903
  items
883
904
  });
905
+ const asProviderReply = (content, target) => asReply({
906
+ content,
907
+ target
908
+ });
884
909
  const buildMessageBase = (message, chatGuidHint, timestamp, phone) => {
885
910
  const chat = resolveChatGuid(message, chatGuidHint);
886
911
  return {
@@ -936,7 +961,7 @@ const buildTextMessage = (base, text, id, partIndex, parentId) => {
936
961
  return msg;
937
962
  };
938
963
  const buildOrderedPartMessage = async (client, base, part, id, partIndex, parentId) => part.type === "text" ? buildTextMessage(base, part.text, id, partIndex, parentId) : await buildAttachmentMessage(client, base, part.attachment, id, partIndex, parentId);
939
- const buildContentMessage = async (client, base, message, messageGuidStr) => {
964
+ const buildUnwrappedContentMessage = async (client, base, message, messageGuidStr) => {
940
965
  const attachments = messageAttachments(message);
941
966
  if (attachments.length === 0) {
942
967
  const text = message.content.text;
@@ -969,20 +994,70 @@ const buildContentMessage = async (client, base, message, messageGuidStr) => {
969
994
  content: asProviderGroup(items)
970
995
  };
971
996
  };
972
- const rebuildFromAppleMessage = async (client, message, phone, chatGuidHint) => {
997
+ const replyTargetGuid = (message) => message.replyTargetGuid ?? message.threadOriginatorGuid;
998
+ const stubReplyTarget = (base, targetGuid) => ({
999
+ id: targetGuid,
1000
+ content: asCustom({
1001
+ imessage_type: "reply-target",
1002
+ stub: true
1003
+ }),
1004
+ space: base.space
1005
+ });
1006
+ const resolveReplyTarget = async (client, base, targetGuid, currentGuid, options) => {
1007
+ if (targetGuid === currentGuid || options.visitedReplyGuids?.has(targetGuid)) return stubReplyTarget(base, targetGuid);
1008
+ const cached = options.cache?.get(targetGuid);
1009
+ if (cached) return cached;
1010
+ try {
1011
+ const visitedReplyGuids = new Set(options.visitedReplyGuids);
1012
+ visitedReplyGuids.add(currentGuid);
1013
+ const rebuilt = await rebuildFromAppleMessage(client, await client.messages.get(toMessageGuid(targetGuid)), options.phone, base.space.id, options.cache, visitedReplyGuids);
1014
+ if (options.cache) cacheMessage(options.cache, rebuilt);
1015
+ return rebuilt;
1016
+ } catch (err) {
1017
+ if (!(err instanceof NotFoundError)) log$2.warn("failed to resolve iMessage reply target; falling back to stub target", {
1018
+ "spectrum.imessage.message.guid": currentGuid,
1019
+ "spectrum.imessage.reply.target_guid": targetGuid,
1020
+ ...errorAttrs(err)
1021
+ }, err);
1022
+ return stubReplyTarget(base, targetGuid);
1023
+ }
1024
+ };
1025
+ const buildContentMessage = async (client, base, message, messageGuidStr, options) => {
1026
+ const msg = await buildUnwrappedContentMessage(client, base, message, messageGuidStr);
1027
+ const targetGuid = replyTargetGuid(message);
1028
+ if (!targetGuid) return msg;
1029
+ const target = await resolveReplyTarget(client, base, targetGuid, messageGuidStr, options);
1030
+ return {
1031
+ ...msg,
1032
+ content: asProviderReply(msg.content, target)
1033
+ };
1034
+ };
1035
+ const messageGroupContent = (message) => {
1036
+ if (message.content.type === "group") return message.content;
1037
+ if (message.content.type === "reply" && message.content.content.type === "group") return message.content.content;
1038
+ };
1039
+ const rebuildFromAppleMessage = async (client, message, phone, chatGuidHint, cache, visitedReplyGuids) => {
973
1040
  const messageGuidStr = message.guid;
974
- return buildContentMessage(client, buildMessageBase(message, chatGuidHint, message.dateCreated ?? /* @__PURE__ */ new Date(), phone), message, messageGuidStr);
1041
+ return buildContentMessage(client, buildMessageBase(message, chatGuidHint, message.dateCreated ?? /* @__PURE__ */ new Date(), phone), message, messageGuidStr, {
1042
+ cache,
1043
+ phone,
1044
+ visitedReplyGuids
1045
+ });
975
1046
  };
976
1047
  const cacheMessage = (cache, message) => {
977
1048
  cache.set(message.id, message);
978
- if (message.content.type === "group") {
979
- for (const item of message.content.items) if (isIMessageMessage(item)) cache.set(item.id, item);
1049
+ const group = messageGroupContent(message);
1050
+ if (group) {
1051
+ for (const item of group.items) if (isIMessageMessage(item)) cache.set(item.id, item);
980
1052
  }
981
1053
  };
982
1054
  const toInboundMessages = async (client, cache, event, phone) => {
983
1055
  const base = buildMessageBase(event.message, event.chatGuid, event.occurredAt, phone);
984
1056
  const messageGuidStr = event.message.guid;
985
- const msg = await buildContentMessage(client, base, event.message, messageGuidStr);
1057
+ const msg = await buildContentMessage(client, base, event.message, messageGuidStr, {
1058
+ cache,
1059
+ phone
1060
+ });
986
1061
  cacheMessage(cache, msg);
987
1062
  return [msg];
988
1063
  };
@@ -992,17 +1067,18 @@ const getMessage$1 = async (remote, spaceId, msgId, phone) => {
992
1067
  if (cached) return cached;
993
1068
  const childRef = parseChildId(msgId);
994
1069
  if (childRef) try {
995
- const parent = await rebuildFromAppleMessage(remote, await remote.messages.get(toMessageGuid(childRef.parentGuid)), phone, spaceId);
1070
+ const parent = await rebuildFromAppleMessage(remote, await remote.messages.get(toMessageGuid(childRef.parentGuid)), phone, spaceId, cache);
996
1071
  cacheMessage(cache, parent);
997
- if (parent.content.type !== "group") return;
998
- const item = parent.content.items[childRef.partIndex];
1072
+ const group = messageGroupContent(parent);
1073
+ if (!group) return;
1074
+ const item = group.items[childRef.partIndex];
999
1075
  return isIMessageMessage(item) ? item : void 0;
1000
1076
  } catch (err) {
1001
1077
  if (err instanceof NotFoundError) return;
1002
1078
  throw err;
1003
1079
  }
1004
1080
  try {
1005
- const rebuilt = await rebuildFromAppleMessage(remote, await remote.messages.get(toMessageGuid(msgId)), phone, spaceId);
1081
+ const rebuilt = await rebuildFromAppleMessage(remote, await remote.messages.get(toMessageGuid(msgId)), phone, spaceId, cache);
1006
1082
  cacheMessage(cache, rebuilt);
1007
1083
  return rebuilt;
1008
1084
  } catch (err) {
@@ -2121,8 +2197,15 @@ const imessage = definePlatform("iMessage", {
2121
2197
  schema: spaceSchema,
2122
2198
  params: spaceParamsSchema,
2123
2199
  create: async ({ input, client }) => {
2124
- if (isLocal(client)) throw UnsupportedError.action("space.create", "iMessage (local mode)", "local mode only supports replying to existing messages");
2125
2200
  if (input.users.length === 0) throw new Error("iMessage space creation requires at least one user");
2201
+ if (isLocal(client)) {
2202
+ if (input.users.length > 1) throw UnsupportedError.action("space.create", "iMessage (local mode)", "local mode cannot create group chats — use space.get(chatGuid) for an existing group");
2203
+ return {
2204
+ id: dmChatGuid(input.users[0]?.id ?? ""),
2205
+ type: "dm",
2206
+ phone: ""
2207
+ };
2208
+ }
2126
2209
  if (client.length === 0) throw new Error("No iMessage clients configured");
2127
2210
  const addresses = input.users.map((u) => u.id);
2128
2211
  if (isSharedMode(client)) {
@@ -2142,7 +2225,11 @@ const imessage = definePlatform("iMessage", {
2142
2225
  };
2143
2226
  },
2144
2227
  get: async ({ input, client }) => {
2145
- if (isLocal(client)) throw UnsupportedError.action("space.get", "iMessage (local mode)", "local mode only supports replying to existing messages");
2228
+ if (isLocal(client)) return {
2229
+ id: input.id,
2230
+ type: chatTypeFromGuid(input.id),
2231
+ phone: ""
2232
+ };
2146
2233
  if (client.length === 0) throw new Error("No iMessage clients configured");
2147
2234
  const phone = isSharedMode(client) ? SHARED_PHONE : input.params?.phone ?? (client.length === 1 ? client[0]?.phone : void 0);
2148
2235
  if (!phone) throw new Error(`iMessage space.get requires params.phone when multiple clients are configured. Available: ${availablePhones(client).join(", ")}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spectrum-ts/imessage",
3
- "version": "8.1.1",
3
+ "version": "8.2.0",
4
4
  "description": "iMessage provider for spectrum-ts — local and remote (advanced) modes.",
5
5
  "repository": {
6
6
  "type": "git",