@spectrum-ts/imessage 8.1.1 → 8.2.1
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/dist/index.js +105 -18
- 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
|
|
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
|
|
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
|
-
|
|
979
|
-
|
|
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
|
-
|
|
998
|
-
|
|
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))
|
|
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(", ")}`);
|