@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 +96 -1
- package/dist/index.js +261 -11
- package/package.json +2 -2
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
1727
|
+
log$2.info("shared contact card", { "spectrum.imessage.contact.chat": safeChatGuid });
|
|
1657
1728
|
}).catch((error) => {
|
|
1658
1729
|
this.cache.delete(chatGuid);
|
|
1659
|
-
log$
|
|
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
|
|
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": "
|
|
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": "^
|
|
42
|
+
"@spectrum-ts/core": "^9.0.0",
|
|
43
43
|
"typescript": "^5 || ^6.0.0"
|
|
44
44
|
},
|
|
45
45
|
"license": "MIT"
|