@spectrum-ts/imessage 11.0.0 → 11.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 +61 -17
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NotFoundError, ValidationError, createClient } from "@photon-ai/advanced-imessage";
|
|
2
2
|
import { sanitizePhone, withSpan } from "@photon-ai/otel";
|
|
3
3
|
import { UnsupportedError, appLayoutSchema, cloud, definePlatform, fromVCard, mergeStreams, read, text, toVCard } from "@spectrum-ts/core";
|
|
4
|
-
import { addMemberSchema, asAttachment, asContact, asCustom, asGroup, asPoll, asPollOption, asReply, asText, avatarSchema, buildPhotoAction, createLogger, ensureM4a, errorAttrs, groupSchema, leaveSpaceSchema, messageEffectSchema, photoActionSchema, reactionSchema, removeMemberSchema, renameSchema, resumableOrderedStream, sanitizeErrorMessage } from "@spectrum-ts/core/authoring";
|
|
4
|
+
import { addMemberSchema, asAttachment, asContact, asCustom, asGroup, asPoll, asPollOption, asReply, asText, asVoice, avatarSchema, buildPhotoAction, createLogger, ensureM4a, errorAttrs, groupSchema, leaveSpaceSchema, messageEffectSchema, photoActionSchema, reactionSchema, removeMemberSchema, renameSchema, resumableOrderedStream, sanitizeErrorMessage } from "@spectrum-ts/core/authoring";
|
|
5
5
|
import z from "zod";
|
|
6
6
|
import { Marked } from "marked";
|
|
7
7
|
import { LRUCache } from "lru-cache";
|
|
@@ -576,6 +576,18 @@ const sendCustomizedMiniApp$1 = async (remote, spaceId, content) => {
|
|
|
576
576
|
const updateCustomizedMiniApp$1 = async (remote, spaceId, session, content) => {
|
|
577
577
|
return toProviderRecord(await remote.messages.updateCustomizedMiniApp(session, content), content, spaceId);
|
|
578
578
|
};
|
|
579
|
+
//#endregion
|
|
580
|
+
//#region src/shared/audio.ts
|
|
581
|
+
const AUDIO_MIME_PATTERN = /^audio\//i;
|
|
582
|
+
const CAF_MIME_TYPE = "audio/x-caf";
|
|
583
|
+
const CAF_UTI = "com.apple.coreaudio-format";
|
|
584
|
+
const GENERIC_BINARY_MIME_TYPE = "application/octet-stream";
|
|
585
|
+
const isCafAttachment = (attachment) => attachment.uti?.toLowerCase() === CAF_UTI || attachment.fileName?.toLowerCase().endsWith(".caf") === true;
|
|
586
|
+
const normalizeAppleAttachmentMimeType = (attachment) => attachment.mimeType.toLowerCase() === GENERIC_BINARY_MIME_TYPE && isCafAttachment(attachment) ? CAF_MIME_TYPE : attachment.mimeType;
|
|
587
|
+
const appleAudioMimeType = (attachment) => {
|
|
588
|
+
const mimeType = normalizeAppleAttachmentMimeType(attachment);
|
|
589
|
+
return AUDIO_MIME_PATTERN.test(mimeType) ? mimeType : void 0;
|
|
590
|
+
};
|
|
579
591
|
const addTextPart = (parts, text) => {
|
|
580
592
|
const trimmed = text?.trim();
|
|
581
593
|
if (trimmed) parts.push({
|
|
@@ -707,7 +719,7 @@ const getRemoteAttachment = async (client, guid) => {
|
|
|
707
719
|
return asAttachment({
|
|
708
720
|
id: info.guid,
|
|
709
721
|
name: info.fileName,
|
|
710
|
-
mimeType: info
|
|
722
|
+
mimeType: normalizeAppleAttachmentMimeType(info),
|
|
711
723
|
size: info.totalBytes,
|
|
712
724
|
read: () => downloadPrimaryAttachment(client, info.guid),
|
|
713
725
|
stream: async () => downloadPrimaryAttachmentStream(client, info.guid)
|
|
@@ -762,7 +774,15 @@ const buildMessageBase = (message, chatGuidHint, timestamp, phone) => {
|
|
|
762
774
|
const toAttachmentContent = (client, info) => asAttachment({
|
|
763
775
|
id: info.guid,
|
|
764
776
|
name: info.fileName,
|
|
765
|
-
mimeType: info
|
|
777
|
+
mimeType: normalizeAppleAttachmentMimeType(info),
|
|
778
|
+
size: info.totalBytes,
|
|
779
|
+
read: async () => await downloadPrimaryAttachment(client, info.guid),
|
|
780
|
+
stream: async () => downloadPrimaryAttachmentStream(client, info.guid)
|
|
781
|
+
});
|
|
782
|
+
const toVoiceContent = (client, info, mimeType) => asVoice({
|
|
783
|
+
id: info.guid,
|
|
784
|
+
name: info.fileName,
|
|
785
|
+
mimeType,
|
|
766
786
|
size: info.totalBytes,
|
|
767
787
|
read: async () => await downloadPrimaryAttachment(client, info.guid),
|
|
768
788
|
stream: async () => downloadPrimaryAttachmentStream(client, info.guid)
|
|
@@ -778,9 +798,13 @@ const toVCardContent = async (client, info) => {
|
|
|
778
798
|
return toAttachmentContent(client, info);
|
|
779
799
|
}
|
|
780
800
|
};
|
|
781
|
-
const attachmentContent = async (client, info
|
|
782
|
-
|
|
783
|
-
const
|
|
801
|
+
const attachmentContent = async (client, info, isVoice) => {
|
|
802
|
+
if (isVCardAttachment(info.mimeType, info.fileName)) return await toVCardContent(client, info);
|
|
803
|
+
const audioMimeType = isVoice ? appleAudioMimeType(info) : void 0;
|
|
804
|
+
return audioMimeType ? toVoiceContent(client, info, audioMimeType) : toAttachmentContent(client, info);
|
|
805
|
+
};
|
|
806
|
+
const buildAttachmentMessage = async (client, base, info, id, partIndex, parentId, isVoice = false) => {
|
|
807
|
+
const content = await attachmentContent(client, info, isVoice);
|
|
784
808
|
const msg = {
|
|
785
809
|
...base,
|
|
786
810
|
id,
|
|
@@ -800,9 +824,10 @@ const buildTextMessage = (base, text, id, partIndex, parentId) => {
|
|
|
800
824
|
if (parentId !== void 0) msg.parentId = parentId;
|
|
801
825
|
return msg;
|
|
802
826
|
};
|
|
803
|
-
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);
|
|
827
|
+
const buildOrderedPartMessage = async (client, base, part, id, partIndex, parentId, voiceAttachmentGuid) => part.type === "text" ? buildTextMessage(base, part.text, id, partIndex, parentId) : await buildAttachmentMessage(client, base, part.attachment, id, partIndex, parentId, part.attachment.guid === voiceAttachmentGuid);
|
|
804
828
|
const buildUnwrappedContentMessage = async (client, base, message, messageGuidStr) => {
|
|
805
829
|
const attachments = messageAttachments(message);
|
|
830
|
+
const voiceAttachmentGuid = message.isAudioMessage ? attachments.find((attachment) => appleAudioMimeType(attachment))?.guid : void 0;
|
|
806
831
|
if (attachments.length === 0) {
|
|
807
832
|
const text = message.content.text;
|
|
808
833
|
return {
|
|
@@ -820,13 +845,13 @@ const buildUnwrappedContentMessage = async (client, base, message, messageGuidSt
|
|
|
820
845
|
if (parts.length === 1) {
|
|
821
846
|
const part = parts[0];
|
|
822
847
|
if (!part) throw new Error("Unreachable: parts.length === 1 but no element");
|
|
823
|
-
return buildOrderedPartMessage(client, base, part, messageGuidStr, 0);
|
|
848
|
+
return buildOrderedPartMessage(client, base, part, messageGuidStr, 0, void 0, voiceAttachmentGuid);
|
|
824
849
|
}
|
|
825
850
|
const items = [];
|
|
826
851
|
for (let i = 0; i < parts.length; i++) {
|
|
827
852
|
const part = parts[i];
|
|
828
853
|
if (!part) continue;
|
|
829
|
-
items.push(await buildOrderedPartMessage(client, base, part, formatChildId(i, messageGuidStr), i, messageGuidStr));
|
|
854
|
+
items.push(await buildOrderedPartMessage(client, base, part, formatChildId(i, messageGuidStr), i, messageGuidStr, voiceAttachmentGuid));
|
|
830
855
|
}
|
|
831
856
|
return {
|
|
832
857
|
...base,
|
|
@@ -1807,6 +1832,25 @@ const toPollDeltaMessages = async (client, pollCache, event, phone) => {
|
|
|
1807
1832
|
const isCursorRejectedIMessageError = (error) => error instanceof ValidationError;
|
|
1808
1833
|
const streamLabel = (kind, phone) => `imessage.${kind}:${phone === "shared" ? phone : sanitizePhone(phone)}`;
|
|
1809
1834
|
const isEventFromCurrentAccount = (event, phone) => event.isFromMe || phone !== "shared" && event.actor?.address !== void 0 && event.actor.address === phone;
|
|
1835
|
+
const streamLog = createLogger("spectrum.imessage.stream");
|
|
1836
|
+
const isRetryableMappingError = (error) => typeof error === "object" && error !== null && error.retryable === true;
|
|
1837
|
+
const skipUnmappable = async (label, cursor, map) => {
|
|
1838
|
+
try {
|
|
1839
|
+
return await map();
|
|
1840
|
+
} catch (error) {
|
|
1841
|
+
if (isRetryableMappingError(error)) throw error;
|
|
1842
|
+
streamLog.warn("skipping unmappable imessage event", {
|
|
1843
|
+
"spectrum.imessage.stream": label,
|
|
1844
|
+
"spectrum.imessage.cursor": cursor,
|
|
1845
|
+
...errorAttrs(error)
|
|
1846
|
+
}, error instanceof Error ? error : void 0);
|
|
1847
|
+
return {
|
|
1848
|
+
cursor,
|
|
1849
|
+
id: `unmappable:${cursor}`,
|
|
1850
|
+
values: []
|
|
1851
|
+
};
|
|
1852
|
+
}
|
|
1853
|
+
};
|
|
1810
1854
|
const toMessageItem = async (client, event, phone, cursor, onInbound) => {
|
|
1811
1855
|
if (event.type === "message.received") {
|
|
1812
1856
|
if (event.message.isFromMe) return {
|
|
@@ -1814,13 +1858,13 @@ const toMessageItem = async (client, event, phone, cursor, onInbound) => {
|
|
|
1814
1858
|
id: event.message.guid,
|
|
1815
1859
|
values: []
|
|
1816
1860
|
};
|
|
1861
|
+
const values = await toInboundMessages(client, getMessageCache(client), event, phone);
|
|
1817
1862
|
const inboundChatGuid = event.message.chatGuids?.[0];
|
|
1818
1863
|
if (inboundChatGuid) onInbound?.(inboundChatGuid);
|
|
1819
|
-
const cache = getMessageCache(client);
|
|
1820
1864
|
return {
|
|
1821
1865
|
cursor,
|
|
1822
1866
|
id: event.message.guid,
|
|
1823
|
-
values
|
|
1867
|
+
values
|
|
1824
1868
|
};
|
|
1825
1869
|
}
|
|
1826
1870
|
if (event.type === "message.reactionAdded") {
|
|
@@ -1914,8 +1958,8 @@ const messageStream = (client, phone, onInbound, recover) => resumableOrderedStr
|
|
|
1914
1958
|
isCursorRejectedError: isCursorRejectedIMessageError,
|
|
1915
1959
|
label: streamLabel("messages", phone),
|
|
1916
1960
|
recover,
|
|
1917
|
-
processLive: (event) => toMessageItem(client, event, phone, String(event.sequence), onInbound),
|
|
1918
|
-
processMissed: (event) => event.type === "catchup.complete" ? Promise.resolve(toCatchUpCompleteItem(event)) : toMessageItem(client, event, phone, String(event.sequence), onInbound),
|
|
1961
|
+
processLive: (event) => skipUnmappable(streamLabel("messages", phone), String(event.sequence), () => toMessageItem(client, event, phone, String(event.sequence), onInbound)),
|
|
1962
|
+
processMissed: (event) => event.type === "catchup.complete" ? Promise.resolve(toCatchUpCompleteItem(event)) : skipUnmappable(streamLabel("messages", phone), String(event.sequence), () => toMessageItem(client, event, phone, String(event.sequence), onInbound)),
|
|
1919
1963
|
subscribeLive: (cursor) => withClose(client.messages.subscribeEvents(), cursor)
|
|
1920
1964
|
});
|
|
1921
1965
|
const pollStream = (client, pollCache, phone, recover) => resumableOrderedStream({
|
|
@@ -1923,8 +1967,8 @@ const pollStream = (client, pollCache, phone, recover) => resumableOrderedStream
|
|
|
1923
1967
|
isCursorRejectedError: isCursorRejectedIMessageError,
|
|
1924
1968
|
label: streamLabel("polls", phone),
|
|
1925
1969
|
recover,
|
|
1926
|
-
processLive: (event) => toPollItem(client, pollCache, event, phone, String(event.sequence)),
|
|
1927
|
-
processMissed: (event) => event.type === "catchup.complete" ? Promise.resolve(toCatchUpCompleteItem(event)) : toPollItem(client, pollCache, event, phone, String(event.sequence)),
|
|
1970
|
+
processLive: (event) => skipUnmappable(streamLabel("polls", phone), String(event.sequence), () => toPollItem(client, pollCache, event, phone, String(event.sequence))),
|
|
1971
|
+
processMissed: (event) => event.type === "catchup.complete" ? Promise.resolve(toCatchUpCompleteItem(event)) : skipUnmappable(streamLabel("polls", phone), String(event.sequence), () => toPollItem(client, pollCache, event, phone, String(event.sequence))),
|
|
1928
1972
|
subscribeLive: (cursor) => withClose(client.polls.subscribeEvents(), cursor)
|
|
1929
1973
|
});
|
|
1930
1974
|
const groupStream = (client, phone, recover) => resumableOrderedStream({
|
|
@@ -1932,8 +1976,8 @@ const groupStream = (client, phone, recover) => resumableOrderedStream({
|
|
|
1932
1976
|
isCursorRejectedError: isCursorRejectedIMessageError,
|
|
1933
1977
|
label: streamLabel("groups", phone),
|
|
1934
1978
|
recover,
|
|
1935
|
-
processLive: (event) => toGroupItem(client, event, phone, String(event.sequence)),
|
|
1936
|
-
processMissed: (event) => event.type === "catchup.complete" ? Promise.resolve(toCatchUpCompleteItem(event)) : toGroupItem(client, event, phone, String(event.sequence)),
|
|
1979
|
+
processLive: (event) => skipUnmappable(streamLabel("groups", phone), String(event.sequence), () => toGroupItem(client, event, phone, String(event.sequence))),
|
|
1980
|
+
processMissed: (event) => event.type === "catchup.complete" ? Promise.resolve(toCatchUpCompleteItem(event)) : skipUnmappable(streamLabel("groups", phone), String(event.sequence), () => toGroupItem(client, event, phone, String(event.sequence))),
|
|
1937
1981
|
subscribeLive: (cursor) => withClose(client.groups.subscribeEvents(), cursor)
|
|
1938
1982
|
});
|
|
1939
1983
|
const clientStream = (client, pollCache, phone, includeGroupEvents, onInbound, recover) => {
|