@spectrum-ts/imessage 8.2.1 → 9.0.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,60 @@ 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>;
262
330
  getAttachment: ({
263
331
  client
264
332
  }: {
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",
@@ -1616,7 +1678,7 @@ const unsendMessage$1 = async (remote, spaceId, msgId) => {
1616
1678
  };
1617
1679
  //#endregion
1618
1680
  //#region src/remote/contact-share.ts
1619
- const log$1 = createLogger("spectrum.imessage.contact");
1681
+ const log$2 = createLogger("spectrum.imessage.contact");
1620
1682
  const SHARE_TTL_MS = 1440 * 60 * 1e3;
1621
1683
  const MAX_TRACKED_CHATS = 1e4;
1622
1684
  /**
@@ -1653,10 +1715,10 @@ var ContactShareTracker = class {
1653
1715
  this.cache.set(chatGuid, true);
1654
1716
  const safeChatGuid = sanitizeErrorMessage(chatGuid);
1655
1717
  this.client.chats.shareContactInfo(chatGuid).then(() => {
1656
- log$1.info("shared contact card", { "spectrum.imessage.contact.chat": safeChatGuid });
1718
+ log$2.info("shared contact card", { "spectrum.imessage.contact.chat": safeChatGuid });
1657
1719
  }).catch((error) => {
1658
1720
  this.cache.delete(chatGuid);
1659
- log$1.warn("failed to share contact card", {
1721
+ log$2.warn("failed to share contact card", {
1660
1722
  "spectrum.imessage.contact.chat": safeChatGuid,
1661
1723
  ...errorAttrs(error)
1662
1724
  }, error);
@@ -1681,6 +1743,91 @@ const getContactShareTracker = (client) => {
1681
1743
  return tracker;
1682
1744
  };
1683
1745
  //#endregion
1746
+ //#region src/remote/group-events.ts
1747
+ const log$1 = createLogger("spectrum.imessage.group");
1748
+ /**
1749
+ * Synthetic id for a `group.changed` event — shared between the stream item
1750
+ * (the dedup key across live/catch-up) and the surfaced message. `sequence`
1751
+ * is monotonic per line, so the id is unique across all change kinds.
1752
+ */
1753
+ const groupEventMessageId = (event) => `${event.chatGuid}:group:${event.sequence}`;
1754
+ /**
1755
+ * The acting party of a group change. For `participantLeft` that is the
1756
+ * leaver (`change.participant`) — nobody leaves on someone else's behalf
1757
+ * (third-party removal is `participantRemoved`), and `leaveSpace` content
1758
+ * carries no members, so the leaver's identity can only travel on
1759
+ * `message.sender`. Every other change acts through `event.actor`, which the
1760
+ * platform doesn't always record.
1761
+ */
1762
+ const groupEventActor = (event) => event.change.type === "participantLeft" ? event.change.participant : event.actor;
1763
+ const toOptionalSenderRef = (addr) => addr?.address ? toSenderRef(addr) : void 0;
1764
+ const fetchIconContent = async (client, event) => {
1765
+ try {
1766
+ const icon = await getIcon$1(client, event.chatGuid);
1767
+ if (!icon) return;
1768
+ return avatarSchema.parse({
1769
+ type: "avatar",
1770
+ action: {
1771
+ kind: "set",
1772
+ mimeType: icon.mimeType,
1773
+ read: () => Promise.resolve(icon.data)
1774
+ }
1775
+ });
1776
+ } catch (e) {
1777
+ log$1.error("failed to fetch changed group icon", {
1778
+ "spectrum.imessage.group.chat": event.chatGuid,
1779
+ ...errorAttrs(e)
1780
+ }, e);
1781
+ return;
1782
+ }
1783
+ };
1784
+ const toGroupChangeContent = async (client, event) => {
1785
+ const change = event.change;
1786
+ switch (change.type) {
1787
+ case "participantAdded": return change.participant.address ? addMemberSchema.parse({
1788
+ type: "addMember",
1789
+ members: [change.participant.address]
1790
+ }) : void 0;
1791
+ case "participantRemoved": return change.participant.address ? removeMemberSchema.parse({
1792
+ type: "removeMember",
1793
+ members: [change.participant.address]
1794
+ }) : void 0;
1795
+ case "participantLeft": return leaveSpaceSchema.parse({ type: "leaveSpace" });
1796
+ case "displayNameChanged": return change.displayName ? renameSchema.parse({
1797
+ type: "rename",
1798
+ displayName: change.displayName
1799
+ }) : void 0;
1800
+ case "iconChanged": return await fetchIconContent(client, event);
1801
+ case "iconRemoved": return avatarSchema.parse({
1802
+ type: "avatar",
1803
+ action: { kind: "clear" }
1804
+ });
1805
+ default: return;
1806
+ }
1807
+ };
1808
+ /**
1809
+ * Convert a `group.changed` event into inbound spectrum messages. Unlike
1810
+ * reactions — where an event without an actor is dropped because the actor
1811
+ * is itself the substance — membership/rename/avatar changes surface even
1812
+ * when the platform recorded no actor: the state change is the payload, so
1813
+ * the message ships with `sender: undefined`.
1814
+ */
1815
+ const toGroupEventMessages = async (client, event, phone) => {
1816
+ const content = await toGroupChangeContent(client, event);
1817
+ if (!content) return [];
1818
+ return [{
1819
+ id: groupEventMessageId(event),
1820
+ content,
1821
+ sender: toOptionalSenderRef(groupEventActor(event)),
1822
+ space: {
1823
+ id: event.chatGuid,
1824
+ type: chatTypeFromGuid(event.chatGuid),
1825
+ phone
1826
+ },
1827
+ timestamp: event.occurredAt
1828
+ }];
1829
+ };
1830
+ //#endregion
1684
1831
  //#region src/remote/polls.ts
1685
1832
  const log = createLogger("spectrum.imessage.poll");
1686
1833
  const isVotedPollEvent = (event) => event.delta.type === "voted";
@@ -1851,6 +1998,22 @@ const toPollItem = async (client, pollCache, event, phone, cursor) => {
1851
1998
  values: await toPollDeltaMessages(client, pollCache, event, phone)
1852
1999
  };
1853
2000
  };
2001
+ const toGroupItem = async (client, event, phone, cursor) => {
2002
+ const id = groupEventMessageId(event);
2003
+ if (isEventFromCurrentAccount({
2004
+ actor: groupEventActor(event),
2005
+ isFromMe: event.isFromMe
2006
+ }, phone)) return {
2007
+ cursor,
2008
+ id,
2009
+ values: []
2010
+ };
2011
+ return {
2012
+ cursor,
2013
+ id,
2014
+ values: await toGroupEventMessages(client, event, phone)
2015
+ };
2016
+ };
1854
2017
  const toCatchUpCompleteItem = (event) => ({
1855
2018
  cursor: String(event.headSequence),
1856
2019
  id: `${event.type}:${event.headSequence}`,
@@ -1858,6 +2021,7 @@ const toCatchUpCompleteItem = (event) => ({
1858
2021
  });
1859
2022
  const isMessageEvent = (event) => event.type.startsWith("message.");
1860
2023
  const isPollEvent = (event) => event.type === "poll.changed";
2024
+ const isGroupEvent = (event) => event.type === "group.changed";
1861
2025
  async function* catchUpEvents(client, cursor, isWanted) {
1862
2026
  const since = toResumeAfter(cursor);
1863
2027
  if (since === void 0) return;
@@ -1906,7 +2070,20 @@ const pollStream = (client, pollCache, phone, recover) => resumableOrderedStream
1906
2070
  processMissed: (event) => event.type === "catchup.complete" ? Promise.resolve(toCatchUpCompleteItem(event)) : toPollItem(client, pollCache, event, phone, String(event.sequence)),
1907
2071
  subscribeLive: (cursor) => withClose(client.polls.subscribeEvents(), cursor)
1908
2072
  });
1909
- const clientStream = (client, pollCache, phone, onInbound, recover) => mergeStreams([messageStream(client, phone, onInbound, recover), pollStream(client, pollCache, phone, recover)]);
2073
+ const groupStream = (client, phone, recover) => resumableOrderedStream({
2074
+ fetchMissed: (cursor) => catchUpEvents(client, cursor, isGroupEvent),
2075
+ isCursorRejectedError: isCursorRejectedIMessageError,
2076
+ label: streamLabel("groups", phone),
2077
+ recover,
2078
+ processLive: (event) => toGroupItem(client, event, phone, String(event.sequence)),
2079
+ processMissed: (event) => event.type === "catchup.complete" ? Promise.resolve(toCatchUpCompleteItem(event)) : toGroupItem(client, event, phone, String(event.sequence)),
2080
+ subscribeLive: (cursor) => withClose(client.groups.subscribeEvents(), cursor)
2081
+ });
2082
+ const clientStream = (client, pollCache, phone, onInbound, recover) => mergeStreams([
2083
+ messageStream(client, phone, onInbound, recover),
2084
+ pollStream(client, pollCache, phone, recover),
2085
+ groupStream(client, phone, recover)
2086
+ ]);
1910
2087
  const messages$1 = (clients, projectConfig) => {
1911
2088
  const pollCache = getPollCache(clients);
1912
2089
  const shareEnabled = projectConfig?.profile?.imessageSynced === true;
@@ -1978,6 +2155,11 @@ const messages = (clients, projectConfig) => messages$1(clients, projectConfig);
1978
2155
  const setBackground = async (remote, spaceId, content) => setBackground$1(remote, spaceId, content);
1979
2156
  const sendCustomizedMiniApp = async (remote, spaceId, content) => sendCustomizedMiniApp$1(remote, spaceId, content);
1980
2157
  const setDisplayName = async (remote, spaceId, content) => setDisplayName$1(remote, spaceId, content);
2158
+ const addParticipants = async (remote, spaceId, content) => addParticipants$1(remote, spaceId, content);
2159
+ const removeParticipants = async (remote, spaceId, content) => removeParticipants$1(remote, spaceId, content);
2160
+ const leaveGroup = async (remote, spaceId) => leaveGroup$1(remote, spaceId);
2161
+ const listParticipants = async (remote, spaceId, selfPhone) => listParticipants$1(remote, spaceId, selfPhone);
2162
+ const getIcon = async (remote, spaceId) => getIcon$1(remote, spaceId);
1981
2163
  const setIcon = async (remote, spaceId, content) => setIcon$1(remote, spaceId, content);
1982
2164
  const markRead = async (remote, spaceId) => {
1983
2165
  await markRead$1(remote, spaceId);
@@ -2133,6 +2315,34 @@ const handleAvatar = async (client, space, content) => {
2133
2315
  await setIcon(clientForPhone(client, space.phone), space.id, content);
2134
2316
  };
2135
2317
  /**
2318
+ * Shared guard for the membership handlers: remote-only, group-only, then
2319
+ * per-phone client resolution. Mirrors the `handleRename` / `handleAvatar`
2320
+ * guard sequence.
2321
+ */
2322
+ const remoteGroupClient = (client, space, action, detail) => {
2323
+ if (isLocal(client)) throw UnsupportedError.action(action, "iMessage (local mode)", detail.local);
2324
+ if (space.type !== "group") throw UnsupportedError.action(action, "iMessage", detail.dm);
2325
+ return clientForPhone(client, space.phone);
2326
+ };
2327
+ const handleAddMember = async (client, space, content) => {
2328
+ await addParticipants(remoteGroupClient(client, space, "addMember", {
2329
+ 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)",
2330
+ local: "adding members requires remote iMessage"
2331
+ }), space.id, content);
2332
+ };
2333
+ const handleRemoveMember = async (client, space, content) => {
2334
+ await removeParticipants(remoteGroupClient(client, space, "removeMember", {
2335
+ 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)",
2336
+ local: "removing members requires remote iMessage"
2337
+ }), space.id, content);
2338
+ };
2339
+ const handleLeaveSpace = async (client, space) => {
2340
+ await leaveGroup(remoteGroupClient(client, space, "leaveSpace", {
2341
+ dm: "only group chats can be left (this space is a DM)",
2342
+ local: "leaving chats requires remote iMessage"
2343
+ }), space.id);
2344
+ };
2345
+ /**
2136
2346
  * Dispatch the iMessage-only fire-and-forget control signals that live outside
2137
2347
  * the universal `Content` union (`background`, `contactCard`). Each is narrowed
2138
2348
  * via a runtime guard rather than a `content.type ===` check — the literals
@@ -2280,6 +2490,18 @@ const imessage = definePlatform("iMessage", {
2280
2490
  await handleAvatar(client, space, content);
2281
2491
  return;
2282
2492
  }
2493
+ if (content.type === "addMember") {
2494
+ await handleAddMember(client, space, content);
2495
+ return;
2496
+ }
2497
+ if (content.type === "removeMember") {
2498
+ await handleRemoveMember(client, space, content);
2499
+ return;
2500
+ }
2501
+ if (content.type === "leaveSpace") {
2502
+ await handleLeaveSpace(client, space);
2503
+ return;
2504
+ }
2283
2505
  if (content.type === "read") {
2284
2506
  await handleRead(client, space);
2285
2507
  return;
@@ -2296,6 +2518,18 @@ const imessage = definePlatform("iMessage", {
2296
2518
  if (isLocal(client)) return getMessage$2(client, messageId);
2297
2519
  return getMessage(clientForPhone(client, space.phone), space.id, messageId, space.phone);
2298
2520
  },
2521
+ getMembers: async ({ client }, space) => {
2522
+ return await listParticipants(remoteGroupClient(client, space, "getMembers", {
2523
+ dm: "only group chats support listing members (this space is a DM)",
2524
+ local: "listing members requires remote iMessage"
2525
+ }), space.id, space.phone);
2526
+ },
2527
+ getAvatar: async ({ client }, space) => {
2528
+ return await getIcon(remoteGroupClient(client, space, "getAvatar", {
2529
+ dm: "only group chats have avatars (this space is a DM)",
2530
+ local: "fetching group avatars requires remote iMessage"
2531
+ }), space.id);
2532
+ },
2299
2533
  getAttachment: async ({ client }, guid, phone) => {
2300
2534
  if (isLocal(client)) throw UnsupportedError.action("getAttachment", "iMessage (local mode)", "fetching attachments by GUID requires remote iMessage");
2301
2535
  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.1",
3
+ "version": "9.0.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"