@spectrum-ts/imessage 8.1.0 → 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.
- package/dist/index.js +202 -67
- 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";
|
|
@@ -204,8 +204,9 @@ const spaceSchema = z.object({
|
|
|
204
204
|
const spaceParamsSchema = z.object({ phone: z.string().optional() });
|
|
205
205
|
/**
|
|
206
206
|
* iMessage-specific per-message metadata surfaced on `IMessageMessage`.
|
|
207
|
-
* - `partIndex`:
|
|
208
|
-
*
|
|
207
|
+
* - `partIndex`: ordered part index within a multi-part message. Text and
|
|
208
|
+
* attachment parts both consume an index (0 for bare or single-part
|
|
209
|
+
* messages; 0..N-1 for a group's sub-items).
|
|
209
210
|
* - `parentId`: guid of the parent message for a group sub-item. Undefined
|
|
210
211
|
* when the message itself is the parent.
|
|
211
212
|
*/
|
|
@@ -445,6 +446,43 @@ const getPollCache = (owner) => {
|
|
|
445
446
|
}
|
|
446
447
|
return cache;
|
|
447
448
|
};
|
|
449
|
+
const addTextPart = (parts, text) => {
|
|
450
|
+
const trimmed = text?.trim();
|
|
451
|
+
if (trimmed) parts.push({
|
|
452
|
+
type: "text",
|
|
453
|
+
text: trimmed
|
|
454
|
+
});
|
|
455
|
+
};
|
|
456
|
+
const hasUsableTextPart = (text) => text?.split("").some((segment) => segment.trim()) ?? false;
|
|
457
|
+
const addAttachmentParts = (parts, attachments) => {
|
|
458
|
+
for (const attachment of attachments) if (attachment) parts.push({
|
|
459
|
+
type: "attachment",
|
|
460
|
+
attachment
|
|
461
|
+
});
|
|
462
|
+
};
|
|
463
|
+
const toOrderedParts = (text, attachments) => {
|
|
464
|
+
const parts = [];
|
|
465
|
+
if (!text) {
|
|
466
|
+
addAttachmentParts(parts, attachments);
|
|
467
|
+
return parts;
|
|
468
|
+
}
|
|
469
|
+
if (!text.includes("")) {
|
|
470
|
+
addAttachmentParts(parts, attachments);
|
|
471
|
+
addTextPart(parts, text);
|
|
472
|
+
return parts;
|
|
473
|
+
}
|
|
474
|
+
const textSegments = text.split("");
|
|
475
|
+
for (let i = 0; i < attachments.length; i++) {
|
|
476
|
+
addTextPart(parts, textSegments[i]);
|
|
477
|
+
const attachment = attachments[i];
|
|
478
|
+
if (attachment) parts.push({
|
|
479
|
+
type: "attachment",
|
|
480
|
+
attachment
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
addTextPart(parts, textSegments.slice(attachments.length).join(""));
|
|
484
|
+
return parts;
|
|
485
|
+
};
|
|
448
486
|
//#endregion
|
|
449
487
|
//#region src/shared/vcard.ts
|
|
450
488
|
const VCARD_MIME_TYPES = new Set([
|
|
@@ -487,12 +525,31 @@ const toVCardContent$1 = async (att) => {
|
|
|
487
525
|
const localAttachmentContent = async (att) => isVCardAttachment(att.mimeType, att.fileName) ? await toVCardContent$1(att) : toAttachmentContent$1(att);
|
|
488
526
|
//#endregion
|
|
489
527
|
//#region src/local/inbound.ts
|
|
490
|
-
const ATTACHMENT_PLACEHOLDER = "";
|
|
491
528
|
const ATTACHMENT_JOIN_RETRY_DELAY_MS = 250;
|
|
492
529
|
const ATTACHMENT_JOIN_RETRY_LIMIT = 8;
|
|
493
530
|
const ATTACHMENT_JOIN_FETCH_LIMIT = 10;
|
|
494
|
-
const hasAttachmentPlaceholder = (message) => message.text?.includes(
|
|
531
|
+
const hasAttachmentPlaceholder = (message) => message.text?.includes("") ?? false;
|
|
495
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
|
+
};
|
|
496
553
|
const refetchUntilAttachmentsSettle = async (client, message) => {
|
|
497
554
|
if (!message.chatId) return message;
|
|
498
555
|
for (let attempt = 0; attempt < ATTACHMENT_JOIN_RETRY_LIMIT; attempt += 1) {
|
|
@@ -529,19 +586,40 @@ const toMessages = async (message) => {
|
|
|
529
586
|
},
|
|
530
587
|
timestamp: message.createdAt
|
|
531
588
|
};
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
589
|
+
const targetId = replyTargetId(message);
|
|
590
|
+
if (message.attachments.length > 0) {
|
|
591
|
+
if (!hasUsableTextPart(message.text)) return wrapReply(await Promise.all(message.attachments.map(async (att) => ({
|
|
592
|
+
...base,
|
|
593
|
+
id: `${message.id}:${att.id}`,
|
|
594
|
+
content: await localAttachmentContent(att)
|
|
595
|
+
}))), targetId);
|
|
596
|
+
const parts = toOrderedParts(message.text, message.attachments);
|
|
597
|
+
const messages = [];
|
|
598
|
+
for (let i = 0; i < parts.length; i++) {
|
|
599
|
+
const part = parts[i];
|
|
600
|
+
if (!part) continue;
|
|
601
|
+
messages.push(part.type === "text" ? {
|
|
602
|
+
...base,
|
|
603
|
+
id: `${message.id}:text:${i}`,
|
|
604
|
+
content: asText(part.text),
|
|
605
|
+
partIndex: i
|
|
606
|
+
} : {
|
|
607
|
+
...base,
|
|
608
|
+
id: `${message.id}:${part.attachment.id}`,
|
|
609
|
+
content: await localAttachmentContent(part.attachment),
|
|
610
|
+
partIndex: i
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
return wrapReply(messages, targetId);
|
|
614
|
+
}
|
|
615
|
+
return wrapReply([{
|
|
538
616
|
...base,
|
|
539
617
|
id: message.id,
|
|
540
618
|
content: {
|
|
541
619
|
type: "text",
|
|
542
620
|
text: message.text ?? ""
|
|
543
621
|
}
|
|
544
|
-
}];
|
|
622
|
+
}], targetId);
|
|
545
623
|
};
|
|
546
624
|
const messages$3 = (client) => stream((emit, end) => {
|
|
547
625
|
let lastPromise = Promise.resolve();
|
|
@@ -824,6 +902,10 @@ const asProviderGroup = (items) => groupSchema.parse({
|
|
|
824
902
|
type: "group",
|
|
825
903
|
items
|
|
826
904
|
});
|
|
905
|
+
const asProviderReply = (content, target) => asReply({
|
|
906
|
+
content,
|
|
907
|
+
target
|
|
908
|
+
});
|
|
827
909
|
const buildMessageBase = (message, chatGuidHint, timestamp, phone) => {
|
|
828
910
|
const chat = resolveChatGuid(message, chatGuidHint);
|
|
829
911
|
return {
|
|
@@ -868,73 +950,114 @@ const buildAttachmentMessage = async (client, base, info, id, partIndex, parentI
|
|
|
868
950
|
if (parentId !== void 0) msg.parentId = parentId;
|
|
869
951
|
return msg;
|
|
870
952
|
};
|
|
871
|
-
const
|
|
872
|
-
const
|
|
873
|
-
|
|
953
|
+
const buildTextMessage = (base, text, id, partIndex, parentId) => {
|
|
954
|
+
const msg = {
|
|
955
|
+
...base,
|
|
956
|
+
id,
|
|
957
|
+
content: asText(text),
|
|
958
|
+
partIndex
|
|
959
|
+
};
|
|
960
|
+
if (parentId !== void 0) msg.parentId = parentId;
|
|
961
|
+
return msg;
|
|
962
|
+
};
|
|
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);
|
|
964
|
+
const buildUnwrappedContentMessage = async (client, base, message, messageGuidStr) => {
|
|
874
965
|
const attachments = messageAttachments(message);
|
|
875
|
-
if (attachments.length ===
|
|
876
|
-
const
|
|
877
|
-
if (!info) throw new Error("Unreachable: attachments.length === 1 but no element");
|
|
878
|
-
return buildAttachmentMessage(client, base, info, messageGuidStr, 0);
|
|
879
|
-
}
|
|
880
|
-
if (attachments.length > 1) {
|
|
881
|
-
const items = [];
|
|
882
|
-
for (let i = 0; i < attachments.length; i++) {
|
|
883
|
-
const info = attachments[i];
|
|
884
|
-
if (!info) continue;
|
|
885
|
-
items.push(await buildAttachmentMessage(client, base, info, formatChildId(i, messageGuidStr), i, messageGuidStr));
|
|
886
|
-
}
|
|
966
|
+
if (attachments.length === 0) {
|
|
967
|
+
const text = message.content.text;
|
|
887
968
|
return {
|
|
888
969
|
...base,
|
|
889
970
|
id: messageGuidStr,
|
|
890
|
-
content:
|
|
971
|
+
content: text ? asText(text) : asCustom(message)
|
|
891
972
|
};
|
|
892
973
|
}
|
|
893
|
-
const
|
|
974
|
+
const parts = toOrderedParts(message.content.text, attachments);
|
|
975
|
+
if (parts.length === 0) return {
|
|
976
|
+
...base,
|
|
977
|
+
id: messageGuidStr,
|
|
978
|
+
content: asCustom(message)
|
|
979
|
+
};
|
|
980
|
+
if (parts.length === 1) {
|
|
981
|
+
const part = parts[0];
|
|
982
|
+
if (!part) throw new Error("Unreachable: parts.length === 1 but no element");
|
|
983
|
+
return buildOrderedPartMessage(client, base, part, messageGuidStr, 0);
|
|
984
|
+
}
|
|
985
|
+
const items = [];
|
|
986
|
+
for (let i = 0; i < parts.length; i++) {
|
|
987
|
+
const part = parts[i];
|
|
988
|
+
if (!part) continue;
|
|
989
|
+
items.push(await buildOrderedPartMessage(client, base, part, formatChildId(i, messageGuidStr), i, messageGuidStr));
|
|
990
|
+
}
|
|
894
991
|
return {
|
|
895
992
|
...base,
|
|
896
993
|
id: messageGuidStr,
|
|
897
|
-
content:
|
|
994
|
+
content: asProviderGroup(items)
|
|
898
995
|
};
|
|
899
996
|
};
|
|
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) => {
|
|
1040
|
+
const messageGuidStr = message.guid;
|
|
1041
|
+
return buildContentMessage(client, buildMessageBase(message, chatGuidHint, message.dateCreated ?? /* @__PURE__ */ new Date(), phone), message, messageGuidStr, {
|
|
1042
|
+
cache,
|
|
1043
|
+
phone,
|
|
1044
|
+
visitedReplyGuids
|
|
1045
|
+
});
|
|
1046
|
+
};
|
|
900
1047
|
const cacheMessage = (cache, message) => {
|
|
901
1048
|
cache.set(message.id, message);
|
|
902
|
-
|
|
903
|
-
|
|
1049
|
+
const group = messageGroupContent(message);
|
|
1050
|
+
if (group) {
|
|
1051
|
+
for (const item of group.items) if (isIMessageMessage(item)) cache.set(item.id, item);
|
|
904
1052
|
}
|
|
905
1053
|
};
|
|
906
1054
|
const toInboundMessages = async (client, cache, event, phone) => {
|
|
907
1055
|
const base = buildMessageBase(event.message, event.chatGuid, event.occurredAt, phone);
|
|
908
1056
|
const messageGuidStr = event.message.guid;
|
|
909
|
-
const
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
const msg = await buildAttachmentMessage(client, base, info, messageGuidStr, 0);
|
|
914
|
-
cacheMessage(cache, msg);
|
|
915
|
-
return [msg];
|
|
916
|
-
}
|
|
917
|
-
if (attachments.length > 1) {
|
|
918
|
-
const items = [];
|
|
919
|
-
for (let i = 0; i < attachments.length; i++) {
|
|
920
|
-
const info = attachments[i];
|
|
921
|
-
if (!info) continue;
|
|
922
|
-
items.push(await buildAttachmentMessage(client, base, info, formatChildId(i, messageGuidStr), i, messageGuidStr));
|
|
923
|
-
}
|
|
924
|
-
const parent = {
|
|
925
|
-
...base,
|
|
926
|
-
id: messageGuidStr,
|
|
927
|
-
content: asProviderGroup(items)
|
|
928
|
-
};
|
|
929
|
-
cacheMessage(cache, parent);
|
|
930
|
-
return [parent];
|
|
931
|
-
}
|
|
932
|
-
const text = event.message.content.text;
|
|
933
|
-
const msg = {
|
|
934
|
-
...base,
|
|
935
|
-
id: messageGuidStr,
|
|
936
|
-
content: text ? asText(text) : asCustom(event.message)
|
|
937
|
-
};
|
|
1057
|
+
const msg = await buildContentMessage(client, base, event.message, messageGuidStr, {
|
|
1058
|
+
cache,
|
|
1059
|
+
phone
|
|
1060
|
+
});
|
|
938
1061
|
cacheMessage(cache, msg);
|
|
939
1062
|
return [msg];
|
|
940
1063
|
};
|
|
@@ -944,17 +1067,18 @@ const getMessage$1 = async (remote, spaceId, msgId, phone) => {
|
|
|
944
1067
|
if (cached) return cached;
|
|
945
1068
|
const childRef = parseChildId(msgId);
|
|
946
1069
|
if (childRef) try {
|
|
947
|
-
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);
|
|
948
1071
|
cacheMessage(cache, parent);
|
|
949
|
-
|
|
950
|
-
|
|
1072
|
+
const group = messageGroupContent(parent);
|
|
1073
|
+
if (!group) return;
|
|
1074
|
+
const item = group.items[childRef.partIndex];
|
|
951
1075
|
return isIMessageMessage(item) ? item : void 0;
|
|
952
1076
|
} catch (err) {
|
|
953
1077
|
if (err instanceof NotFoundError) return;
|
|
954
1078
|
throw err;
|
|
955
1079
|
}
|
|
956
1080
|
try {
|
|
957
|
-
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);
|
|
958
1082
|
cacheMessage(cache, rebuilt);
|
|
959
1083
|
return rebuilt;
|
|
960
1084
|
} catch (err) {
|
|
@@ -2073,8 +2197,15 @@ const imessage = definePlatform("iMessage", {
|
|
|
2073
2197
|
schema: spaceSchema,
|
|
2074
2198
|
params: spaceParamsSchema,
|
|
2075
2199
|
create: async ({ input, client }) => {
|
|
2076
|
-
if (isLocal(client)) throw UnsupportedError.action("space.create", "iMessage (local mode)", "local mode only supports replying to existing messages");
|
|
2077
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
|
+
}
|
|
2078
2209
|
if (client.length === 0) throw new Error("No iMessage clients configured");
|
|
2079
2210
|
const addresses = input.users.map((u) => u.id);
|
|
2080
2211
|
if (isSharedMode(client)) {
|
|
@@ -2094,7 +2225,11 @@ const imessage = definePlatform("iMessage", {
|
|
|
2094
2225
|
};
|
|
2095
2226
|
},
|
|
2096
2227
|
get: async ({ input, client }) => {
|
|
2097
|
-
if (isLocal(client))
|
|
2228
|
+
if (isLocal(client)) return {
|
|
2229
|
+
id: input.id,
|
|
2230
|
+
type: chatTypeFromGuid(input.id),
|
|
2231
|
+
phone: ""
|
|
2232
|
+
};
|
|
2098
2233
|
if (client.length === 0) throw new Error("No iMessage clients configured");
|
|
2099
2234
|
const phone = isSharedMode(client) ? SHARED_PHONE : input.params?.phone ?? (client.length === 1 ? client[0]?.phone : void 0);
|
|
2100
2235
|
if (!phone) throw new Error(`iMessage space.get requires params.phone when multiple clients are configured. Available: ${availablePhones(client).join(", ")}`);
|