@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.
Files changed (2) hide show
  1. package/dist/index.js +61 -17
  2. 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.mimeType,
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.mimeType,
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) => isVCardAttachment(info.mimeType, info.fileName) ? await toVCardContent(client, info) : toAttachmentContent(client, info);
782
- const buildAttachmentMessage = async (client, base, info, id, partIndex, parentId) => {
783
- const content = await attachmentContent(client, info);
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: await toInboundMessages(client, cache, event, phone)
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) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spectrum-ts/imessage",
3
- "version": "11.0.0",
3
+ "version": "11.2.0",
4
4
  "description": "iMessage provider for spectrum-ts.",
5
5
  "repository": {
6
6
  "type": "git",