@spectrum-ts/imessage 8.2.2 → 9.1.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.d.ts CHANGED
@@ -1,9 +1,23 @@
1
- import { AdvancedIMessage, MessageEffect } from "@photon-ai/advanced-imessage";
1
+ import { AdvancedIMessage, ChatServiceType, MessageEffect } from "@photon-ai/advanced-imessage";
2
2
  import { IMessageSDK } from "@photon-ai/imessage-kit";
3
3
  import { Attachment, ContentBuilder, ContentInput, SchemaMessage, Space, read } from "@spectrum-ts/core";
4
4
  import { PhotoInput } from "@spectrum-ts/core/authoring";
5
5
  import z from "zod";
6
6
 
7
+ //#region src/remote/members.d.ts
8
+ /**
9
+ * A group participant mapped to spectrum's user shape. `id` is the canonical
10
+ * address (E.164 phone or email — the same handle format `space.create`
11
+ * accepts); `address`/`country`/`service` mirror the SDK record and match
12
+ * the extras declared by the provider's `userSchema`.
13
+ */
14
+ type IMessageParticipant = {
15
+ id: string;
16
+ address: string;
17
+ country?: string;
18
+ service: ChatServiceType;
19
+ };
20
+ //#endregion
7
21
  //#region src/content/background.d.ts
8
22
  type BackgroundInput = PhotoInput;
9
23
  /**
@@ -259,6 +273,87 @@ declare const imessage: import("@spectrum-ts/core").Platform<import("@spectrum-t
259
273
  id: string;
260
274
  __platform: string;
261
275
  }, messageId: string) => Promise<IMessageMessage | undefined>;
276
+ getMembers: ({
277
+ client
278
+ }: {
279
+ client: IMessageClient;
280
+ config: {
281
+ local: true;
282
+ } | {
283
+ local: false;
284
+ clients?: {
285
+ address: string;
286
+ token: string;
287
+ phone: string;
288
+ } | {
289
+ address: string;
290
+ token: string;
291
+ phone: string;
292
+ }[] | undefined;
293
+ };
294
+ store: import("@spectrum-ts/core").Store;
295
+ }, space: {
296
+ id: string;
297
+ type: "dm" | "group";
298
+ phone: string;
299
+ } & {
300
+ id: string;
301
+ __platform: string;
302
+ }) => Promise<IMessageParticipant[]>;
303
+ getAvatar: ({
304
+ client
305
+ }: {
306
+ client: IMessageClient;
307
+ config: {
308
+ local: true;
309
+ } | {
310
+ local: false;
311
+ clients?: {
312
+ address: string;
313
+ token: string;
314
+ phone: string;
315
+ } | {
316
+ address: string;
317
+ token: string;
318
+ phone: string;
319
+ }[] | undefined;
320
+ };
321
+ store: import("@spectrum-ts/core").Store;
322
+ }, space: {
323
+ id: string;
324
+ type: "dm" | "group";
325
+ phone: string;
326
+ } & {
327
+ id: string;
328
+ __platform: string;
329
+ }) => Promise<import("@spectrum-ts/core").AvatarData | undefined>;
330
+ getDisplayName: ({
331
+ client
332
+ }: {
333
+ client: IMessageClient;
334
+ config: {
335
+ local: true;
336
+ } | {
337
+ local: false;
338
+ clients?: {
339
+ address: string;
340
+ token: string;
341
+ phone: string;
342
+ } | {
343
+ address: string;
344
+ token: string;
345
+ phone: string;
346
+ }[] | undefined;
347
+ };
348
+ store: import("@spectrum-ts/core").Store;
349
+ }, space: {
350
+ id: string;
351
+ type: "dm" | "group";
352
+ phone: string;
353
+ } & {
354
+ id: string;
355
+ __platform: string;
356
+ }) => Promise<string | undefined>;
262
357
  getAttachment: ({
263
358
  client
264
359
  }: {
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, asReply, asText, buildPhotoAction, createLogger, ensureM4a, errorAttrs, groupSchema, messageEffectSchema, photoActionSchema, reactionSchema, resumableOrderedStream, sanitizeErrorMessage } from "@spectrum-ts/core/authoring";
5
+ 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";
6
6
  import z from "zod";
7
7
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
8
8
  import { createReadStream } from "node:fs";
@@ -216,7 +216,7 @@ const messageSchema = z.object({
216
216
  });
217
217
  //#endregion
218
218
  //#region src/auth.ts
219
- const log$3 = createLogger("spectrum.imessage.auth");
219
+ const log$4 = createLogger("spectrum.imessage.auth");
220
220
  const RENEWAL_RATIO = .8;
221
221
  const EXPIRY_BUFFER_MS = 3e4;
222
222
  const RETRY_DELAY_MS = 3e4;
@@ -241,13 +241,13 @@ async function createCloudClients(projectId, projectSecret) {
241
241
  };
242
242
  const onRefreshSuccess = () => {
243
243
  if (refreshFailures > 0) {
244
- log$3.info("imessage token refresh recovered", { "spectrum.imessage.auth.attempt": refreshFailures });
244
+ log$4.info("imessage token refresh recovered", { "spectrum.imessage.auth.attempt": refreshFailures });
245
245
  refreshFailures = 0;
246
246
  }
247
247
  };
248
248
  const onRefreshFailure = (error) => {
249
249
  refreshFailures += 1;
250
- log$3.warn("imessage token refresh failed; retrying", {
250
+ log$4.warn("imessage token refresh failed; retrying", {
251
251
  "spectrum.imessage.auth.attempt": refreshFailures,
252
252
  "spectrum.imessage.auth.retry_in_ms": RETRY_DELAY_MS,
253
253
  ...errorAttrs(error)
@@ -732,6 +732,26 @@ const setIcon$1 = async (remote, spaceId, content) => {
732
732
  const buffer = await content.action.read();
733
733
  await remote.groups.setIcon(chat, new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength));
734
734
  };
735
+ /**
736
+ * Download the current icon of a remote iMessage group chat. Resolves
737
+ * `undefined` when the group has no icon (the SDK's `NotFoundError` with
738
+ * code `groupIconNotFound`); any other error — including a `chatNotFound`
739
+ * `NotFoundError` — propagates. Bytes are copied into a Buffer so the result
740
+ * round-trips into `setIcon` / `space.avatar(...)`. The caller (`getAvatar`
741
+ * in the iMessage provider) owns the group-only / remote-only guards.
742
+ */
743
+ const getIcon$1 = async (remote, spaceId) => {
744
+ try {
745
+ const icon = await remote.groups.getIcon(toChatGuid(spaceId));
746
+ return {
747
+ data: Buffer.from(icon.data),
748
+ mimeType: icon.mimeType
749
+ };
750
+ } catch (err) {
751
+ if (err instanceof NotFoundError && err.code === "groupIconNotFound") return;
752
+ throw err;
753
+ }
754
+ };
735
755
  //#endregion
736
756
  //#region src/remote/background.ts
737
757
  /**
@@ -875,7 +895,7 @@ const getRemoteAttachment = async (client, guid) => {
875
895
  };
876
896
  //#endregion
877
897
  //#region src/remote/inbound.ts
878
- const log$2 = createLogger("spectrum.imessage.inbound");
898
+ const log$3 = createLogger("spectrum.imessage.inbound");
879
899
  const messageAttachments = (message) => message.content.attachments;
880
900
  const resolveChatGuid = (message, hint) => {
881
901
  if (hint) return hint;
@@ -931,7 +951,7 @@ const toVCardContent = async (client, info) => {
931
951
  try {
932
952
  return asContact(fromVCard((await downloadPrimaryAttachment(client, info.guid)).toString("utf8")));
933
953
  } catch (err) {
934
- log$2.warn("failed to parse vCard attachment; falling back to attachment content", {
954
+ log$3.warn("failed to parse vCard attachment; falling back to attachment content", {
935
955
  "spectrum.imessage.attachment.guid": info.guid,
936
956
  ...errorAttrs(err)
937
957
  }, err);
@@ -1014,7 +1034,7 @@ const resolveReplyTarget = async (client, base, targetGuid, currentGuid, options
1014
1034
  if (options.cache) cacheMessage(options.cache, rebuilt);
1015
1035
  return rebuilt;
1016
1036
  } catch (err) {
1017
- if (!(err instanceof NotFoundError)) log$2.warn("failed to resolve iMessage reply target; falling back to stub target", {
1037
+ if (!(err instanceof NotFoundError)) log$3.warn("failed to resolve iMessage reply target; falling back to stub target", {
1018
1038
  "spectrum.imessage.message.guid": currentGuid,
1019
1039
  "spectrum.imessage.reply.target_guid": targetGuid,
1020
1040
  ...errorAttrs(err)
@@ -1087,6 +1107,48 @@ const getMessage$1 = async (remote, spaceId, msgId, phone) => {
1087
1107
  }
1088
1108
  };
1089
1109
  //#endregion
1110
+ //#region src/remote/members.ts
1111
+ /**
1112
+ * Apply an `AddMember` content value to a remote iMessage group chat.
1113
+ * Fire-and-forget — the `Chat` returned by `addParticipants` is discarded.
1114
+ * The caller (`handleAddMember` in the iMessage provider) is responsible
1115
+ * for the group-only / remote-only guards.
1116
+ */
1117
+ const addParticipants$1 = async (remote, spaceId, content) => {
1118
+ await remote.groups.addParticipants(toChatGuid(spaceId), content.members);
1119
+ };
1120
+ /**
1121
+ * Apply a `RemoveMember` content value to a remote iMessage group chat.
1122
+ * Fire-and-forget — the `Chat` returned by `removeParticipants` is
1123
+ * discarded.
1124
+ */
1125
+ const removeParticipants$1 = async (remote, spaceId, content) => {
1126
+ await remote.groups.removeParticipants(toChatGuid(spaceId), content.members);
1127
+ };
1128
+ /**
1129
+ * Make the agent's own account leave a remote iMessage group chat.
1130
+ * Fire-and-forget.
1131
+ */
1132
+ const leaveGroup$1 = async (remote, spaceId) => {
1133
+ await remote.groups.leave(toChatGuid(spaceId));
1134
+ };
1135
+ /**
1136
+ * List a remote group chat's current participants, excluding the agent's own
1137
+ * handle (`selfPhone` — the dedicated number that owns the chat; the shared
1138
+ * sentinel never matches a canonical address, so shared mode returns the
1139
+ * full roster). The caller (`getMembers` in the iMessage provider) owns the
1140
+ * group-only / remote-only guards.
1141
+ */
1142
+ const listParticipants$1 = async (remote, spaceId, selfPhone) => {
1143
+ const { participants } = await remote.chats.get(toChatGuid(spaceId));
1144
+ return participants.filter((p) => p.address !== selfPhone).map((p) => ({
1145
+ id: p.address,
1146
+ address: p.address,
1147
+ country: p.country,
1148
+ service: p.service
1149
+ }));
1150
+ };
1151
+ //#endregion
1090
1152
  //#region src/remote/reactions.ts
1091
1153
  const EMOJI_TO_TAPBACK = {
1092
1154
  "❤️": "love",
@@ -1186,6 +1248,15 @@ const markRead$1 = async (remote, spaceId) => {
1186
1248
  const setDisplayName$1 = async (remote, spaceId, content) => {
1187
1249
  await remote.groups.setDisplayName(toChatGuid(spaceId), content.displayName);
1188
1250
  };
1251
+ /**
1252
+ * Read a remote iMessage group chat's title. The SDK returns an empty
1253
+ * `Chat.displayName` for an unnamed group; normalized to `undefined`. The
1254
+ * group-only guard lives at the action layer (see `remoteGroupClient`).
1255
+ */
1256
+ const getDisplayName$1 = async (remote, spaceId) => {
1257
+ const { displayName } = await remote.chats.get(toChatGuid(spaceId));
1258
+ return displayName || void 0;
1259
+ };
1189
1260
  //#endregion
1190
1261
  //#region src/remote/markdown.ts
1191
1262
  const markdownLexer = new Marked();
@@ -1616,7 +1687,7 @@ const unsendMessage$1 = async (remote, spaceId, msgId) => {
1616
1687
  };
1617
1688
  //#endregion
1618
1689
  //#region src/remote/contact-share.ts
1619
- const log$1 = createLogger("spectrum.imessage.contact");
1690
+ const log$2 = createLogger("spectrum.imessage.contact");
1620
1691
  const SHARE_TTL_MS = 1440 * 60 * 1e3;
1621
1692
  const MAX_TRACKED_CHATS = 1e4;
1622
1693
  /**
@@ -1653,10 +1724,10 @@ var ContactShareTracker = class {
1653
1724
  this.cache.set(chatGuid, true);
1654
1725
  const safeChatGuid = sanitizeErrorMessage(chatGuid);
1655
1726
  this.client.chats.shareContactInfo(chatGuid).then(() => {
1656
- log$1.info("shared contact card", { "spectrum.imessage.contact.chat": safeChatGuid });
1727
+ log$2.info("shared contact card", { "spectrum.imessage.contact.chat": safeChatGuid });
1657
1728
  }).catch((error) => {
1658
1729
  this.cache.delete(chatGuid);
1659
- log$1.warn("failed to share contact card", {
1730
+ log$2.warn("failed to share contact card", {
1660
1731
  "spectrum.imessage.contact.chat": safeChatGuid,
1661
1732
  ...errorAttrs(error)
1662
1733
  }, error);
@@ -1681,6 +1752,91 @@ const getContactShareTracker = (client) => {
1681
1752
  return tracker;
1682
1753
  };
1683
1754
  //#endregion
1755
+ //#region src/remote/group-events.ts
1756
+ const log$1 = createLogger("spectrum.imessage.group");
1757
+ /**
1758
+ * Synthetic id for a `group.changed` event — shared between the stream item
1759
+ * (the dedup key across live/catch-up) and the surfaced message. `sequence`
1760
+ * is monotonic per line, so the id is unique across all change kinds.
1761
+ */
1762
+ const groupEventMessageId = (event) => `${event.chatGuid}:group:${event.sequence}`;
1763
+ /**
1764
+ * The acting party of a group change. For `participantLeft` that is the
1765
+ * leaver (`change.participant`) — nobody leaves on someone else's behalf
1766
+ * (third-party removal is `participantRemoved`), and `leaveSpace` content
1767
+ * carries no members, so the leaver's identity can only travel on
1768
+ * `message.sender`. Every other change acts through `event.actor`, which the
1769
+ * platform doesn't always record.
1770
+ */
1771
+ const groupEventActor = (event) => event.change.type === "participantLeft" ? event.change.participant : event.actor;
1772
+ const toOptionalSenderRef = (addr) => addr?.address ? toSenderRef(addr) : void 0;
1773
+ const fetchIconContent = async (client, event) => {
1774
+ try {
1775
+ const icon = await getIcon$1(client, event.chatGuid);
1776
+ if (!icon) return;
1777
+ return avatarSchema.parse({
1778
+ type: "avatar",
1779
+ action: {
1780
+ kind: "set",
1781
+ mimeType: icon.mimeType,
1782
+ read: () => Promise.resolve(icon.data)
1783
+ }
1784
+ });
1785
+ } catch (e) {
1786
+ log$1.error("failed to fetch changed group icon", {
1787
+ "spectrum.imessage.group.chat": event.chatGuid,
1788
+ ...errorAttrs(e)
1789
+ }, e);
1790
+ return;
1791
+ }
1792
+ };
1793
+ const toGroupChangeContent = async (client, event) => {
1794
+ const change = event.change;
1795
+ switch (change.type) {
1796
+ case "participantAdded": return change.participant.address ? addMemberSchema.parse({
1797
+ type: "addMember",
1798
+ members: [change.participant.address]
1799
+ }) : void 0;
1800
+ case "participantRemoved": return change.participant.address ? removeMemberSchema.parse({
1801
+ type: "removeMember",
1802
+ members: [change.participant.address]
1803
+ }) : void 0;
1804
+ case "participantLeft": return leaveSpaceSchema.parse({ type: "leaveSpace" });
1805
+ case "displayNameChanged": return change.displayName ? renameSchema.parse({
1806
+ type: "rename",
1807
+ displayName: change.displayName
1808
+ }) : void 0;
1809
+ case "iconChanged": return await fetchIconContent(client, event);
1810
+ case "iconRemoved": return avatarSchema.parse({
1811
+ type: "avatar",
1812
+ action: { kind: "clear" }
1813
+ });
1814
+ default: return;
1815
+ }
1816
+ };
1817
+ /**
1818
+ * Convert a `group.changed` event into inbound spectrum messages. Unlike
1819
+ * reactions — where an event without an actor is dropped because the actor
1820
+ * is itself the substance — membership/rename/avatar changes surface even
1821
+ * when the platform recorded no actor: the state change is the payload, so
1822
+ * the message ships with `sender: undefined`.
1823
+ */
1824
+ const toGroupEventMessages = async (client, event, phone) => {
1825
+ const content = await toGroupChangeContent(client, event);
1826
+ if (!content) return [];
1827
+ return [{
1828
+ id: groupEventMessageId(event),
1829
+ content,
1830
+ sender: toOptionalSenderRef(groupEventActor(event)),
1831
+ space: {
1832
+ id: event.chatGuid,
1833
+ type: chatTypeFromGuid(event.chatGuid),
1834
+ phone
1835
+ },
1836
+ timestamp: event.occurredAt
1837
+ }];
1838
+ };
1839
+ //#endregion
1684
1840
  //#region src/remote/polls.ts
1685
1841
  const log = createLogger("spectrum.imessage.poll");
1686
1842
  const isVotedPollEvent = (event) => event.delta.type === "voted";
@@ -1851,6 +2007,22 @@ const toPollItem = async (client, pollCache, event, phone, cursor) => {
1851
2007
  values: await toPollDeltaMessages(client, pollCache, event, phone)
1852
2008
  };
1853
2009
  };
2010
+ const toGroupItem = async (client, event, phone, cursor) => {
2011
+ const id = groupEventMessageId(event);
2012
+ if (isEventFromCurrentAccount({
2013
+ actor: groupEventActor(event),
2014
+ isFromMe: event.isFromMe
2015
+ }, phone)) return {
2016
+ cursor,
2017
+ id,
2018
+ values: []
2019
+ };
2020
+ return {
2021
+ cursor,
2022
+ id,
2023
+ values: await toGroupEventMessages(client, event, phone)
2024
+ };
2025
+ };
1854
2026
  const toCatchUpCompleteItem = (event) => ({
1855
2027
  cursor: String(event.headSequence),
1856
2028
  id: `${event.type}:${event.headSequence}`,
@@ -1858,6 +2030,7 @@ const toCatchUpCompleteItem = (event) => ({
1858
2030
  });
1859
2031
  const isMessageEvent = (event) => event.type.startsWith("message.");
1860
2032
  const isPollEvent = (event) => event.type === "poll.changed";
2033
+ const isGroupEvent = (event) => event.type === "group.changed";
1861
2034
  async function* catchUpEvents(client, cursor, isWanted) {
1862
2035
  const since = toResumeAfter(cursor);
1863
2036
  if (since === void 0) return;
@@ -1906,7 +2079,20 @@ const pollStream = (client, pollCache, phone, recover) => resumableOrderedStream
1906
2079
  processMissed: (event) => event.type === "catchup.complete" ? Promise.resolve(toCatchUpCompleteItem(event)) : toPollItem(client, pollCache, event, phone, String(event.sequence)),
1907
2080
  subscribeLive: (cursor) => withClose(client.polls.subscribeEvents(), cursor)
1908
2081
  });
1909
- const clientStream = (client, pollCache, phone, onInbound, recover) => mergeStreams([messageStream(client, phone, onInbound, recover), pollStream(client, pollCache, phone, recover)]);
2082
+ const groupStream = (client, phone, recover) => resumableOrderedStream({
2083
+ fetchMissed: (cursor) => catchUpEvents(client, cursor, isGroupEvent),
2084
+ isCursorRejectedError: isCursorRejectedIMessageError,
2085
+ label: streamLabel("groups", phone),
2086
+ recover,
2087
+ processLive: (event) => toGroupItem(client, event, phone, String(event.sequence)),
2088
+ processMissed: (event) => event.type === "catchup.complete" ? Promise.resolve(toCatchUpCompleteItem(event)) : toGroupItem(client, event, phone, String(event.sequence)),
2089
+ subscribeLive: (cursor) => withClose(client.groups.subscribeEvents(), cursor)
2090
+ });
2091
+ const clientStream = (client, pollCache, phone, onInbound, recover) => mergeStreams([
2092
+ messageStream(client, phone, onInbound, recover),
2093
+ pollStream(client, pollCache, phone, recover),
2094
+ groupStream(client, phone, recover)
2095
+ ]);
1910
2096
  const messages$1 = (clients, projectConfig) => {
1911
2097
  const pollCache = getPollCache(clients);
1912
2098
  const shareEnabled = projectConfig?.profile?.imessageSynced === true;
@@ -1978,6 +2164,12 @@ const messages = (clients, projectConfig) => messages$1(clients, projectConfig);
1978
2164
  const setBackground = async (remote, spaceId, content) => setBackground$1(remote, spaceId, content);
1979
2165
  const sendCustomizedMiniApp = async (remote, spaceId, content) => sendCustomizedMiniApp$1(remote, spaceId, content);
1980
2166
  const setDisplayName = async (remote, spaceId, content) => setDisplayName$1(remote, spaceId, content);
2167
+ const getDisplayName = async (remote, spaceId) => getDisplayName$1(remote, spaceId);
2168
+ const addParticipants = async (remote, spaceId, content) => addParticipants$1(remote, spaceId, content);
2169
+ const removeParticipants = async (remote, spaceId, content) => removeParticipants$1(remote, spaceId, content);
2170
+ const leaveGroup = async (remote, spaceId) => leaveGroup$1(remote, spaceId);
2171
+ const listParticipants = async (remote, spaceId, selfPhone) => listParticipants$1(remote, spaceId, selfPhone);
2172
+ const getIcon = async (remote, spaceId) => getIcon$1(remote, spaceId);
1981
2173
  const setIcon = async (remote, spaceId, content) => setIcon$1(remote, spaceId, content);
1982
2174
  const markRead = async (remote, spaceId) => {
1983
2175
  await markRead$1(remote, spaceId);
@@ -2133,6 +2325,34 @@ const handleAvatar = async (client, space, content) => {
2133
2325
  await setIcon(clientForPhone(client, space.phone), space.id, content);
2134
2326
  };
2135
2327
  /**
2328
+ * Shared guard for the membership handlers: remote-only, group-only, then
2329
+ * per-phone client resolution. Mirrors the `handleRename` / `handleAvatar`
2330
+ * guard sequence.
2331
+ */
2332
+ const remoteGroupClient = (client, space, action, detail) => {
2333
+ if (isLocal(client)) throw UnsupportedError.action(action, "iMessage (local mode)", detail.local);
2334
+ if (space.type !== "group") throw UnsupportedError.action(action, "iMessage", detail.dm);
2335
+ return clientForPhone(client, space.phone);
2336
+ };
2337
+ const handleAddMember = async (client, space, content) => {
2338
+ await addParticipants(remoteGroupClient(client, space, "addMember", {
2339
+ dm: "only group chats can add members (this space is a DM — iMessage cannot convert a DM into a group; create a group via space.create instead)",
2340
+ local: "adding members requires remote iMessage"
2341
+ }), space.id, content);
2342
+ };
2343
+ const handleRemoveMember = async (client, space, content) => {
2344
+ await removeParticipants(remoteGroupClient(client, space, "removeMember", {
2345
+ dm: "only group chats can remove members (this space is a DM — iMessage cannot convert a DM into a group; create a group via space.create instead)",
2346
+ local: "removing members requires remote iMessage"
2347
+ }), space.id, content);
2348
+ };
2349
+ const handleLeaveSpace = async (client, space) => {
2350
+ await leaveGroup(remoteGroupClient(client, space, "leaveSpace", {
2351
+ dm: "only group chats can be left (this space is a DM)",
2352
+ local: "leaving chats requires remote iMessage"
2353
+ }), space.id);
2354
+ };
2355
+ /**
2136
2356
  * Dispatch the iMessage-only fire-and-forget control signals that live outside
2137
2357
  * the universal `Content` union (`background`, `contactCard`). Each is narrowed
2138
2358
  * via a runtime guard rather than a `content.type ===` check — the literals
@@ -2280,6 +2500,18 @@ const imessage = definePlatform("iMessage", {
2280
2500
  await handleAvatar(client, space, content);
2281
2501
  return;
2282
2502
  }
2503
+ if (content.type === "addMember") {
2504
+ await handleAddMember(client, space, content);
2505
+ return;
2506
+ }
2507
+ if (content.type === "removeMember") {
2508
+ await handleRemoveMember(client, space, content);
2509
+ return;
2510
+ }
2511
+ if (content.type === "leaveSpace") {
2512
+ await handleLeaveSpace(client, space);
2513
+ return;
2514
+ }
2283
2515
  if (content.type === "read") {
2284
2516
  await handleRead(client, space);
2285
2517
  return;
@@ -2296,6 +2528,24 @@ const imessage = definePlatform("iMessage", {
2296
2528
  if (isLocal(client)) return getMessage$2(client, messageId);
2297
2529
  return getMessage(clientForPhone(client, space.phone), space.id, messageId, space.phone);
2298
2530
  },
2531
+ getMembers: async ({ client }, space) => {
2532
+ return await listParticipants(remoteGroupClient(client, space, "getMembers", {
2533
+ dm: "only group chats support listing members (this space is a DM)",
2534
+ local: "listing members requires remote iMessage"
2535
+ }), space.id, space.phone);
2536
+ },
2537
+ getAvatar: async ({ client }, space) => {
2538
+ return await getIcon(remoteGroupClient(client, space, "getAvatar", {
2539
+ dm: "only group chats have avatars (this space is a DM)",
2540
+ local: "fetching group avatars requires remote iMessage"
2541
+ }), space.id);
2542
+ },
2543
+ getDisplayName: async ({ client }, space) => {
2544
+ return await getDisplayName(remoteGroupClient(client, space, "getDisplayName", {
2545
+ dm: "only group chats have display names (this space is a DM)",
2546
+ local: "reading chat display names requires remote iMessage"
2547
+ }), space.id);
2548
+ },
2299
2549
  getAttachment: async ({ client }, guid, phone) => {
2300
2550
  if (isLocal(client)) throw UnsupportedError.action("getAttachment", "iMessage (local mode)", "fetching attachments by GUID requires remote iMessage");
2301
2551
  if (client.length === 0) throw new Error("No iMessage clients configured");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spectrum-ts/imessage",
3
- "version": "8.2.2",
3
+ "version": "9.1.0",
4
4
  "description": "iMessage provider for spectrum-ts — local and remote (advanced) modes.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -39,7 +39,7 @@
39
39
  "zod": "^4.2.1"
40
40
  },
41
41
  "peerDependencies": {
42
- "@spectrum-ts/core": "^8.0.0",
42
+ "@spectrum-ts/core": "^9.0.0",
43
43
  "typescript": "^5 || ^6.0.0"
44
44
  },
45
45
  "license": "MIT"