@spectrum-ts/imessage 9.3.0 → 10.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/README.md +10 -2
- package/dist/index.d.ts +27 -28
- package/dist/index.js +123 -367
- package/package.json +3 -4
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @spectrum-ts/imessage
|
|
2
2
|
|
|
3
|
-
iMessage provider for [spectrum-ts](https://github.com/photon-hq/spectrum-ts),
|
|
3
|
+
iMessage provider for [spectrum-ts](https://github.com/photon-hq/spectrum-ts), including tapbacks, special effects, polls, and mini-apps.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -15,7 +15,7 @@ import { Spectrum } from "spectrum-ts";
|
|
|
15
15
|
import { imessage } from "@spectrum-ts/imessage";
|
|
16
16
|
|
|
17
17
|
const spectrum = Spectrum({
|
|
18
|
-
|
|
18
|
+
platforms: [imessage.config()],
|
|
19
19
|
});
|
|
20
20
|
```
|
|
21
21
|
|
|
@@ -32,3 +32,11 @@ await space.shareContactCard();
|
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
See the [spectrum-ts documentation](https://photon.codes/spectrum) for the full guide.
|
|
35
|
+
|
|
36
|
+
For direct access to the local macOS Messages database, install and import
|
|
37
|
+
`@spectrum-ts/imessage-local` separately. The local provider is intentionally
|
|
38
|
+
not included in the batteries-included `spectrum-ts` package.
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
bun add @spectrum-ts/imessage-local
|
|
42
|
+
```
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { AdvancedIMessage, ChatServiceType
|
|
2
|
-
import { IMessageSDK } from "@photon-ai/imessage-kit";
|
|
1
|
+
import { AdvancedIMessage, ChatServiceType } from "@photon-ai/advanced-imessage";
|
|
3
2
|
import { Attachment, ContentBuilder, ContentInput, SchemaMessage, Space, read } from "@spectrum-ts/core";
|
|
4
3
|
import { PhotoInput } from "@spectrum-ts/core/authoring";
|
|
5
4
|
import z from "zod";
|
|
@@ -168,7 +167,22 @@ type CustomizedMiniAppInput = Omit<CustomizedMiniApp, "type" | "__platform">;
|
|
|
168
167
|
declare function customizedMiniApp(input: CustomizedMiniAppInput): ContentBuilder;
|
|
169
168
|
//#endregion
|
|
170
169
|
//#region src/content/effect.d.ts
|
|
171
|
-
|
|
170
|
+
declare const messageEffects: {
|
|
171
|
+
readonly balloons: "com.apple.messages.effect.CKBalloonEffect";
|
|
172
|
+
readonly celebration: "com.apple.messages.effect.CKHappyBirthdayEffect";
|
|
173
|
+
readonly confetti: "com.apple.messages.effect.CKConfettiEffect";
|
|
174
|
+
readonly echo: "com.apple.messages.effect.CKEchoEffect";
|
|
175
|
+
readonly fireworks: "com.apple.messages.effect.CKFireworksEffect";
|
|
176
|
+
readonly gentle: "com.apple.MobileSMS.expressivesend.gentle";
|
|
177
|
+
readonly heart: "com.apple.messages.effect.CKHeartEffect";
|
|
178
|
+
readonly invisible: "com.apple.MobileSMS.expressivesend.invisibleink";
|
|
179
|
+
readonly lasers: "com.apple.messages.effect.CKLasersEffect";
|
|
180
|
+
readonly loud: "com.apple.MobileSMS.expressivesend.loud";
|
|
181
|
+
readonly slam: "com.apple.MobileSMS.expressivesend.impact";
|
|
182
|
+
readonly sparkles: "com.apple.messages.effect.CKSparklesEffect";
|
|
183
|
+
readonly spotlight: "com.apple.messages.effect.CKSpotlightEffect";
|
|
184
|
+
};
|
|
185
|
+
type IMessageMessageEffect = (typeof messageEffects)[keyof typeof messageEffects];
|
|
172
186
|
declare function effect(input: ContentInput, messageEffect: IMessageMessageEffect): ContentBuilder;
|
|
173
187
|
//#endregion
|
|
174
188
|
//#region src/types.d.ts
|
|
@@ -176,7 +190,7 @@ interface RemoteClient {
|
|
|
176
190
|
client: AdvancedIMessage;
|
|
177
191
|
phone: string;
|
|
178
192
|
}
|
|
179
|
-
type IMessageClient =
|
|
193
|
+
type IMessageClient = RemoteClient[];
|
|
180
194
|
declare const userSchema: z.ZodObject<{
|
|
181
195
|
address: z.ZodOptional<z.ZodString>;
|
|
182
196
|
country: z.ZodOptional<z.ZodString>;
|
|
@@ -219,10 +233,7 @@ type IMessageMessage = SchemaMessage<typeof userSchema, typeof spaceSchema> & {
|
|
|
219
233
|
};
|
|
220
234
|
//#endregion
|
|
221
235
|
//#region src/index.d.ts
|
|
222
|
-
declare const imessage: import("@spectrum-ts/core").Platform<import("@spectrum-ts/core").PlatformDef<"iMessage", import("zod").
|
|
223
|
-
local: import("zod").ZodLiteral<true>;
|
|
224
|
-
}, import("zod/v4/core").$strip>, import("zod").ZodObject<{
|
|
225
|
-
local: import("zod").ZodDefault<import("zod").ZodOptional<import("zod").ZodLiteral<false>>>;
|
|
236
|
+
declare const imessage: import("@spectrum-ts/core").Platform<import("@spectrum-ts/core").PlatformDef<"iMessage", import("zod").ZodObject<{
|
|
226
237
|
clients: import("zod").ZodOptional<import("zod").ZodUnion<[import("zod").ZodObject<{
|
|
227
238
|
address: import("zod").ZodString;
|
|
228
239
|
token: import("zod").ZodString;
|
|
@@ -232,7 +243,7 @@ declare const imessage: import("@spectrum-ts/core").Platform<import("@spectrum-t
|
|
|
232
243
|
token: import("zod").ZodString;
|
|
233
244
|
phone: import("zod").ZodString;
|
|
234
245
|
}, import("zod/v4/core").$strip>>]>>;
|
|
235
|
-
}, import("zod/v4/core").$
|
|
246
|
+
}, import("zod/v4/core").$strict>, import("zod").ZodObject<{
|
|
236
247
|
address: import("zod").ZodOptional<import("zod").ZodString>;
|
|
237
248
|
country: import("zod").ZodOptional<import("zod").ZodString>;
|
|
238
249
|
service: import("zod").ZodOptional<import("zod").ZodEnum<{
|
|
@@ -276,9 +287,6 @@ declare const imessage: import("@spectrum-ts/core").Platform<import("@spectrum-t
|
|
|
276
287
|
}: {
|
|
277
288
|
client: IMessageClient;
|
|
278
289
|
config: {
|
|
279
|
-
local: true;
|
|
280
|
-
} | {
|
|
281
|
-
local: false;
|
|
282
290
|
clients?: {
|
|
283
291
|
address: string;
|
|
284
292
|
token: string;
|
|
@@ -303,9 +311,6 @@ declare const imessage: import("@spectrum-ts/core").Platform<import("@spectrum-t
|
|
|
303
311
|
}: {
|
|
304
312
|
client: IMessageClient;
|
|
305
313
|
config: {
|
|
306
|
-
local: true;
|
|
307
|
-
} | {
|
|
308
|
-
local: false;
|
|
309
314
|
clients?: {
|
|
310
315
|
address: string;
|
|
311
316
|
token: string;
|
|
@@ -330,9 +335,6 @@ declare const imessage: import("@spectrum-ts/core").Platform<import("@spectrum-t
|
|
|
330
335
|
}: {
|
|
331
336
|
client: IMessageClient;
|
|
332
337
|
config: {
|
|
333
|
-
local: true;
|
|
334
|
-
} | {
|
|
335
|
-
local: false;
|
|
336
338
|
clients?: {
|
|
337
339
|
address: string;
|
|
338
340
|
token: string;
|
|
@@ -357,9 +359,6 @@ declare const imessage: import("@spectrum-ts/core").Platform<import("@spectrum-t
|
|
|
357
359
|
}: {
|
|
358
360
|
client: IMessageClient;
|
|
359
361
|
config: {
|
|
360
|
-
local: true;
|
|
361
|
-
} | {
|
|
362
|
-
local: false;
|
|
363
362
|
clients?: {
|
|
364
363
|
address: string;
|
|
365
364
|
token: string;
|
|
@@ -387,19 +386,19 @@ declare const imessage: import("@spectrum-ts/core").Platform<import("@spectrum-t
|
|
|
387
386
|
}>> & Readonly<{
|
|
388
387
|
effect: {
|
|
389
388
|
message: {
|
|
390
|
-
readonly
|
|
391
|
-
readonly
|
|
392
|
-
readonly gentle: "com.apple.MobileSMS.expressivesend.gentle";
|
|
393
|
-
readonly invisible: "com.apple.MobileSMS.expressivesend.invisibleink";
|
|
389
|
+
readonly balloons: "com.apple.messages.effect.CKBalloonEffect";
|
|
390
|
+
readonly celebration: "com.apple.messages.effect.CKHappyBirthdayEffect";
|
|
394
391
|
readonly confetti: "com.apple.messages.effect.CKConfettiEffect";
|
|
392
|
+
readonly echo: "com.apple.messages.effect.CKEchoEffect";
|
|
395
393
|
readonly fireworks: "com.apple.messages.effect.CKFireworksEffect";
|
|
396
|
-
readonly
|
|
394
|
+
readonly gentle: "com.apple.MobileSMS.expressivesend.gentle";
|
|
397
395
|
readonly heart: "com.apple.messages.effect.CKHeartEffect";
|
|
396
|
+
readonly invisible: "com.apple.MobileSMS.expressivesend.invisibleink";
|
|
398
397
|
readonly lasers: "com.apple.messages.effect.CKLasersEffect";
|
|
399
|
-
readonly
|
|
398
|
+
readonly loud: "com.apple.MobileSMS.expressivesend.loud";
|
|
399
|
+
readonly slam: "com.apple.MobileSMS.expressivesend.impact";
|
|
400
400
|
readonly sparkles: "com.apple.messages.effect.CKSparklesEffect";
|
|
401
401
|
readonly spotlight: "com.apple.messages.effect.CKSpotlightEffect";
|
|
402
|
-
readonly echo: "com.apple.messages.effect.CKEchoEffect";
|
|
403
402
|
};
|
|
404
403
|
};
|
|
405
404
|
}>;
|
package/dist/index.js
CHANGED
|
@@ -1,15 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { IMessageSDK } from "@photon-ai/imessage-kit";
|
|
1
|
+
import { NotFoundError, ValidationError, createClient } from "@photon-ai/advanced-imessage";
|
|
3
2
|
import { sanitizePhone, withSpan } from "@photon-ai/otel";
|
|
4
|
-
import { UnsupportedError, appLayoutSchema, cloud, definePlatform, fromVCard, mergeStreams, read,
|
|
3
|
+
import { UnsupportedError, appLayoutSchema, cloud, definePlatform, fromVCard, mergeStreams, read, text, toVCard } from "@spectrum-ts/core";
|
|
5
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";
|
|
6
5
|
import z from "zod";
|
|
7
|
-
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
8
|
-
import { createReadStream } from "node:fs";
|
|
9
|
-
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
10
|
-
import { Readable } from "node:stream";
|
|
11
|
-
import { tmpdir } from "node:os";
|
|
12
|
-
import { basename, join } from "node:path";
|
|
13
6
|
import { Marked } from "marked";
|
|
14
7
|
import { LRUCache } from "lru-cache";
|
|
15
8
|
//#region src/content/background.ts
|
|
@@ -156,7 +149,22 @@ function customizedMiniApp(input) {
|
|
|
156
149
|
}
|
|
157
150
|
//#endregion
|
|
158
151
|
//#region src/content/effect.ts
|
|
159
|
-
const
|
|
152
|
+
const messageEffects = {
|
|
153
|
+
balloons: "com.apple.messages.effect.CKBalloonEffect",
|
|
154
|
+
celebration: "com.apple.messages.effect.CKHappyBirthdayEffect",
|
|
155
|
+
confetti: "com.apple.messages.effect.CKConfettiEffect",
|
|
156
|
+
echo: "com.apple.messages.effect.CKEchoEffect",
|
|
157
|
+
fireworks: "com.apple.messages.effect.CKFireworksEffect",
|
|
158
|
+
gentle: "com.apple.MobileSMS.expressivesend.gentle",
|
|
159
|
+
heart: "com.apple.messages.effect.CKHeartEffect",
|
|
160
|
+
invisible: "com.apple.MobileSMS.expressivesend.invisibleink",
|
|
161
|
+
lasers: "com.apple.messages.effect.CKLasersEffect",
|
|
162
|
+
loud: "com.apple.MobileSMS.expressivesend.loud",
|
|
163
|
+
slam: "com.apple.MobileSMS.expressivesend.impact",
|
|
164
|
+
sparkles: "com.apple.messages.effect.CKSparklesEffect",
|
|
165
|
+
spotlight: "com.apple.messages.effect.CKSpotlightEffect"
|
|
166
|
+
};
|
|
167
|
+
const SUPPORTED_EFFECTS = new Set(Object.values(messageEffects));
|
|
160
168
|
const resolveContent = (input) => typeof input === "string" ? text(input).build() : input.build();
|
|
161
169
|
function effect(input, messageEffect) {
|
|
162
170
|
return { build: async () => {
|
|
@@ -178,16 +186,12 @@ function effect(input, messageEffect) {
|
|
|
178
186
|
* no `from` parameter on send), so all routing through it tags this sentinel.
|
|
179
187
|
*/
|
|
180
188
|
const SHARED_PHONE = "shared";
|
|
181
|
-
const isLocal = (client) => client instanceof IMessageSDK;
|
|
182
189
|
const clientEntry = z.object({
|
|
183
190
|
address: z.string(),
|
|
184
191
|
token: z.string(),
|
|
185
192
|
phone: z.string()
|
|
186
193
|
});
|
|
187
|
-
const configSchema = z.
|
|
188
|
-
local: z.literal(false).optional().default(false),
|
|
189
|
-
clients: clientEntry.or(z.array(clientEntry)).optional()
|
|
190
|
-
})]);
|
|
194
|
+
const configSchema = z.strictObject({ clients: clientEntry.or(z.array(clientEntry)).optional() });
|
|
191
195
|
const userSchema = z.object({
|
|
192
196
|
address: z.string().optional(),
|
|
193
197
|
country: z.string().optional(),
|
|
@@ -436,10 +440,9 @@ var PollCache = class {
|
|
|
436
440
|
const messageCaches = /* @__PURE__ */ new WeakMap();
|
|
437
441
|
const pollCaches = /* @__PURE__ */ new WeakMap();
|
|
438
442
|
/**
|
|
439
|
-
* Returns a per-client message cache. Keyed by
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
* state accidentally.
|
|
443
|
+
* Returns a per-client message cache. Keyed by the client array so each
|
|
444
|
+
* iMessage provider instance has its own cache and multiple providers don't
|
|
445
|
+
* share state accidentally.
|
|
443
446
|
*/
|
|
444
447
|
const getMessageCache = (owner) => {
|
|
445
448
|
let cache = messageCaches.get(owner);
|
|
@@ -457,257 +460,6 @@ const getPollCache = (owner) => {
|
|
|
457
460
|
}
|
|
458
461
|
return cache;
|
|
459
462
|
};
|
|
460
|
-
const addTextPart = (parts, text) => {
|
|
461
|
-
const trimmed = text?.trim();
|
|
462
|
-
if (trimmed) parts.push({
|
|
463
|
-
type: "text",
|
|
464
|
-
text: trimmed
|
|
465
|
-
});
|
|
466
|
-
};
|
|
467
|
-
const hasUsableTextPart = (text) => text?.split("").some((segment) => segment.trim()) ?? false;
|
|
468
|
-
const addAttachmentParts = (parts, attachments) => {
|
|
469
|
-
for (const attachment of attachments) if (attachment) parts.push({
|
|
470
|
-
type: "attachment",
|
|
471
|
-
attachment
|
|
472
|
-
});
|
|
473
|
-
};
|
|
474
|
-
const toOrderedParts = (text, attachments) => {
|
|
475
|
-
const parts = [];
|
|
476
|
-
if (!text) {
|
|
477
|
-
addAttachmentParts(parts, attachments);
|
|
478
|
-
return parts;
|
|
479
|
-
}
|
|
480
|
-
if (!text.includes("")) {
|
|
481
|
-
addAttachmentParts(parts, attachments);
|
|
482
|
-
addTextPart(parts, text);
|
|
483
|
-
return parts;
|
|
484
|
-
}
|
|
485
|
-
const textSegments = text.split("");
|
|
486
|
-
for (let i = 0; i < attachments.length; i++) {
|
|
487
|
-
addTextPart(parts, textSegments[i]);
|
|
488
|
-
const attachment = attachments[i];
|
|
489
|
-
if (attachment) parts.push({
|
|
490
|
-
type: "attachment",
|
|
491
|
-
attachment
|
|
492
|
-
});
|
|
493
|
-
}
|
|
494
|
-
addTextPart(parts, textSegments.slice(attachments.length).join(""));
|
|
495
|
-
return parts;
|
|
496
|
-
};
|
|
497
|
-
//#endregion
|
|
498
|
-
//#region src/shared/vcard.ts
|
|
499
|
-
const VCARD_MIME_TYPES = new Set([
|
|
500
|
-
"text/vcard",
|
|
501
|
-
"text/x-vcard",
|
|
502
|
-
"text/directory",
|
|
503
|
-
"application/vcard",
|
|
504
|
-
"application/x-vcard"
|
|
505
|
-
]);
|
|
506
|
-
const normalizeMimeType = (mimeType) => (mimeType.split(";")[0] ?? "").trim().toLowerCase();
|
|
507
|
-
const isVCardAttachment = (mimeType, fileName) => {
|
|
508
|
-
if (mimeType && VCARD_MIME_TYPES.has(normalizeMimeType(mimeType))) return true;
|
|
509
|
-
return Boolean(fileName?.toLowerCase().endsWith(".vcf"));
|
|
510
|
-
};
|
|
511
|
-
const vcardFileName = (contact) => {
|
|
512
|
-
return `${(contact.name?.formatted ?? contact.user?.id ?? "contact").replace(/[^a-zA-Z0-9_\-.]/g, "_")}.vcf`;
|
|
513
|
-
};
|
|
514
|
-
const readLocalAttachment = async (att) => {
|
|
515
|
-
if (!att.localPath) throw new Error(`iMessage attachment ${att.id} has no local file available on disk`);
|
|
516
|
-
return readFile(att.localPath);
|
|
517
|
-
};
|
|
518
|
-
const toAttachmentContent$1 = (att) => {
|
|
519
|
-
const { localPath } = att;
|
|
520
|
-
return asAttachment({
|
|
521
|
-
id: att.id,
|
|
522
|
-
name: att.fileName ?? "attachment",
|
|
523
|
-
mimeType: att.mimeType,
|
|
524
|
-
size: att.sizeBytes,
|
|
525
|
-
read: () => readLocalAttachment(att),
|
|
526
|
-
stream: localPath ? async () => Readable.toWeb(createReadStream(localPath)) : void 0
|
|
527
|
-
});
|
|
528
|
-
};
|
|
529
|
-
const toVCardContent$1 = async (att) => {
|
|
530
|
-
try {
|
|
531
|
-
return asContact(fromVCard((await readLocalAttachment(att)).toString("utf8")));
|
|
532
|
-
} catch {
|
|
533
|
-
return toAttachmentContent$1(att);
|
|
534
|
-
}
|
|
535
|
-
};
|
|
536
|
-
const localAttachmentContent = async (att) => isVCardAttachment(att.mimeType, att.fileName) ? await toVCardContent$1(att) : toAttachmentContent$1(att);
|
|
537
|
-
//#endregion
|
|
538
|
-
//#region src/local/inbound.ts
|
|
539
|
-
const ATTACHMENT_JOIN_RETRY_DELAY_MS = 250;
|
|
540
|
-
const ATTACHMENT_JOIN_RETRY_LIMIT = 8;
|
|
541
|
-
const ATTACHMENT_JOIN_FETCH_LIMIT = 10;
|
|
542
|
-
const hasAttachmentPlaceholder = (message) => message.text?.includes("") ?? false;
|
|
543
|
-
const isPendingAttachmentJoin = (message) => message.attachments.length === 0 && (message.hasAttachments || hasAttachmentPlaceholder(message));
|
|
544
|
-
const replyTargetId = (message) => message.threadRootMessageId ?? void 0;
|
|
545
|
-
const stubReplyTarget$1 = (space, targetId) => ({
|
|
546
|
-
id: targetId,
|
|
547
|
-
content: asCustom({
|
|
548
|
-
imessage_type: "reply-target",
|
|
549
|
-
stub: true
|
|
550
|
-
}),
|
|
551
|
-
space
|
|
552
|
-
});
|
|
553
|
-
const asProviderReply$1 = (content, target) => asReply({
|
|
554
|
-
content,
|
|
555
|
-
target
|
|
556
|
-
});
|
|
557
|
-
const wrapReply = (messages, targetId) => {
|
|
558
|
-
if (!targetId) return messages;
|
|
559
|
-
return messages.map((message) => ({
|
|
560
|
-
...message,
|
|
561
|
-
content: asProviderReply$1(message.content, stubReplyTarget$1(message.space, targetId))
|
|
562
|
-
}));
|
|
563
|
-
};
|
|
564
|
-
const refetchUntilAttachmentsSettle = async (client, message) => {
|
|
565
|
-
if (!message.chatId) return message;
|
|
566
|
-
for (let attempt = 0; attempt < ATTACHMENT_JOIN_RETRY_LIMIT; attempt += 1) {
|
|
567
|
-
await setTimeout$1(ATTACHMENT_JOIN_RETRY_DELAY_MS);
|
|
568
|
-
let rows;
|
|
569
|
-
try {
|
|
570
|
-
rows = await client.getMessages({
|
|
571
|
-
chatId: message.chatId,
|
|
572
|
-
limit: ATTACHMENT_JOIN_FETCH_LIMIT,
|
|
573
|
-
since: message.createdAt
|
|
574
|
-
});
|
|
575
|
-
} catch {
|
|
576
|
-
continue;
|
|
577
|
-
}
|
|
578
|
-
const refreshed = rows.find((row) => row.id === message.id);
|
|
579
|
-
if (refreshed && !isPendingAttachmentJoin(refreshed)) return refreshed;
|
|
580
|
-
}
|
|
581
|
-
return message;
|
|
582
|
-
};
|
|
583
|
-
const toMessages = async (message) => {
|
|
584
|
-
const { chatId, chatKind } = message;
|
|
585
|
-
if (!chatId || chatKind === "unknown") return [];
|
|
586
|
-
if (message.reaction !== null || message.kind !== "text" || message.retractedAt !== null) return [];
|
|
587
|
-
if (isPendingAttachmentJoin(message)) return [];
|
|
588
|
-
const base = {
|
|
589
|
-
sender: {
|
|
590
|
-
id: message.participant ?? "",
|
|
591
|
-
...message.participant ? { address: message.participant } : {}
|
|
592
|
-
},
|
|
593
|
-
space: {
|
|
594
|
-
id: chatId,
|
|
595
|
-
type: chatKind === "group" ? "group" : "dm",
|
|
596
|
-
phone: ""
|
|
597
|
-
},
|
|
598
|
-
timestamp: message.createdAt
|
|
599
|
-
};
|
|
600
|
-
const targetId = replyTargetId(message);
|
|
601
|
-
if (message.attachments.length > 0) {
|
|
602
|
-
if (!hasUsableTextPart(message.text)) return wrapReply(await Promise.all(message.attachments.map(async (att) => ({
|
|
603
|
-
...base,
|
|
604
|
-
id: `${message.id}:${att.id}`,
|
|
605
|
-
content: await localAttachmentContent(att)
|
|
606
|
-
}))), targetId);
|
|
607
|
-
const parts = toOrderedParts(message.text, message.attachments);
|
|
608
|
-
const messages = [];
|
|
609
|
-
for (let i = 0; i < parts.length; i++) {
|
|
610
|
-
const part = parts[i];
|
|
611
|
-
if (!part) continue;
|
|
612
|
-
messages.push(part.type === "text" ? {
|
|
613
|
-
...base,
|
|
614
|
-
id: `${message.id}:text:${i}`,
|
|
615
|
-
content: asText(part.text),
|
|
616
|
-
partIndex: i
|
|
617
|
-
} : {
|
|
618
|
-
...base,
|
|
619
|
-
id: `${message.id}:${part.attachment.id}`,
|
|
620
|
-
content: await localAttachmentContent(part.attachment),
|
|
621
|
-
partIndex: i
|
|
622
|
-
});
|
|
623
|
-
}
|
|
624
|
-
return wrapReply(messages, targetId);
|
|
625
|
-
}
|
|
626
|
-
return wrapReply([{
|
|
627
|
-
...base,
|
|
628
|
-
id: message.id,
|
|
629
|
-
content: {
|
|
630
|
-
type: "text",
|
|
631
|
-
text: message.text ?? ""
|
|
632
|
-
}
|
|
633
|
-
}], targetId);
|
|
634
|
-
};
|
|
635
|
-
const messages$3 = (client) => stream((emit, end) => {
|
|
636
|
-
let lastPromise = Promise.resolve();
|
|
637
|
-
const handleIncoming = async (message) => {
|
|
638
|
-
const ms = await toMessages(isPendingAttachmentJoin(message) ? await refetchUntilAttachmentsSettle(client, message) : message);
|
|
639
|
-
for (const m of ms) await emit(m);
|
|
640
|
-
};
|
|
641
|
-
const startPromise = client.startWatching({
|
|
642
|
-
onIncomingMessage: (message) => {
|
|
643
|
-
lastPromise = lastPromise.then(() => handleIncoming(message)).catch(end);
|
|
644
|
-
},
|
|
645
|
-
onError: end
|
|
646
|
-
}).catch(end);
|
|
647
|
-
return async () => {
|
|
648
|
-
await startPromise.catch(() => {});
|
|
649
|
-
await client.stopWatching();
|
|
650
|
-
await lastPromise.catch(() => {});
|
|
651
|
-
};
|
|
652
|
-
});
|
|
653
|
-
//#endregion
|
|
654
|
-
//#region src/shared/errors.ts
|
|
655
|
-
const IMESSAGE_PLATFORM = "iMessage";
|
|
656
|
-
const LOCAL_IMESSAGE_PLATFORM = "iMessage (local mode)";
|
|
657
|
-
const unsupportedRemoteContent = (type, detail) => UnsupportedError.content(type, IMESSAGE_PLATFORM, detail);
|
|
658
|
-
const unsupportedLocalContent = (type, detail) => UnsupportedError.content(type, LOCAL_IMESSAGE_PLATFORM, detail);
|
|
659
|
-
//#endregion
|
|
660
|
-
//#region src/local/send.ts
|
|
661
|
-
const synthRecord = (spaceId, content) => ({
|
|
662
|
-
id: crypto.randomUUID(),
|
|
663
|
-
content,
|
|
664
|
-
space: { id: spaceId },
|
|
665
|
-
timestamp: /* @__PURE__ */ new Date()
|
|
666
|
-
});
|
|
667
|
-
const sendTempFile = async (client, spaceId, name, data) => {
|
|
668
|
-
const safeName = basename(name) || "attachment";
|
|
669
|
-
const dir = await mkdtemp(join(tmpdir(), "spectrum-"));
|
|
670
|
-
const tmp = join(dir, safeName);
|
|
671
|
-
await writeFile(tmp, data);
|
|
672
|
-
try {
|
|
673
|
-
await client.send({
|
|
674
|
-
to: spaceId,
|
|
675
|
-
attachments: [tmp]
|
|
676
|
-
});
|
|
677
|
-
} finally {
|
|
678
|
-
await rm(dir, {
|
|
679
|
-
recursive: true,
|
|
680
|
-
force: true
|
|
681
|
-
}).catch(() => {});
|
|
682
|
-
}
|
|
683
|
-
};
|
|
684
|
-
const send$3 = async (client, spaceId, content) => {
|
|
685
|
-
switch (content.type) {
|
|
686
|
-
case "text":
|
|
687
|
-
await client.send({
|
|
688
|
-
to: spaceId,
|
|
689
|
-
text: content.text
|
|
690
|
-
});
|
|
691
|
-
return synthRecord(spaceId, content);
|
|
692
|
-
case "attachment":
|
|
693
|
-
await sendTempFile(client, spaceId, content.name, await content.read());
|
|
694
|
-
return synthRecord(spaceId, content);
|
|
695
|
-
case "contact": {
|
|
696
|
-
const vcf = await toVCard(content);
|
|
697
|
-
await sendTempFile(client, spaceId, vcardFileName(content), Buffer.from(vcf, "utf8"));
|
|
698
|
-
return synthRecord(spaceId, content);
|
|
699
|
-
}
|
|
700
|
-
case "effect": throw unsupportedLocalContent("effect", "message effects require remote iMessage");
|
|
701
|
-
case "poll": throw unsupportedLocalContent("poll");
|
|
702
|
-
default: throw unsupportedLocalContent(content.type);
|
|
703
|
-
}
|
|
704
|
-
};
|
|
705
|
-
const getMessage$3 = async (_client, _id) => void 0;
|
|
706
|
-
//#endregion
|
|
707
|
-
//#region src/local/api.ts
|
|
708
|
-
const messages$2 = (client) => messages$3(client);
|
|
709
|
-
const send$2 = async (client, spaceId, content) => send$3(client, spaceId, content);
|
|
710
|
-
const getMessage$2 = async (client, id) => getMessage$3(client, id);
|
|
711
463
|
//#endregion
|
|
712
464
|
//#region src/remote/ids.ts
|
|
713
465
|
const PART_PREFIX = /^p:(\d+)\//;
|
|
@@ -824,6 +576,59 @@ const sendCustomizedMiniApp$1 = async (remote, spaceId, content) => {
|
|
|
824
576
|
const updateCustomizedMiniApp$1 = async (remote, spaceId, session, content) => {
|
|
825
577
|
return toProviderRecord(await remote.messages.updateCustomizedMiniApp(session, content), content, spaceId);
|
|
826
578
|
};
|
|
579
|
+
const addTextPart = (parts, text) => {
|
|
580
|
+
const trimmed = text?.trim();
|
|
581
|
+
if (trimmed) parts.push({
|
|
582
|
+
type: "text",
|
|
583
|
+
text: trimmed
|
|
584
|
+
});
|
|
585
|
+
};
|
|
586
|
+
const addAttachmentParts = (parts, attachments) => {
|
|
587
|
+
for (const attachment of attachments) if (attachment) parts.push({
|
|
588
|
+
type: "attachment",
|
|
589
|
+
attachment
|
|
590
|
+
});
|
|
591
|
+
};
|
|
592
|
+
const toOrderedParts = (text, attachments) => {
|
|
593
|
+
const parts = [];
|
|
594
|
+
if (!text) {
|
|
595
|
+
addAttachmentParts(parts, attachments);
|
|
596
|
+
return parts;
|
|
597
|
+
}
|
|
598
|
+
if (!text.includes("")) {
|
|
599
|
+
addAttachmentParts(parts, attachments);
|
|
600
|
+
addTextPart(parts, text);
|
|
601
|
+
return parts;
|
|
602
|
+
}
|
|
603
|
+
const textSegments = text.split("");
|
|
604
|
+
for (let i = 0; i < attachments.length; i++) {
|
|
605
|
+
addTextPart(parts, textSegments[i]);
|
|
606
|
+
const attachment = attachments[i];
|
|
607
|
+
if (attachment) parts.push({
|
|
608
|
+
type: "attachment",
|
|
609
|
+
attachment
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
addTextPart(parts, textSegments.slice(attachments.length).join(""));
|
|
613
|
+
return parts;
|
|
614
|
+
};
|
|
615
|
+
//#endregion
|
|
616
|
+
//#region src/shared/vcard.ts
|
|
617
|
+
const VCARD_MIME_TYPES = new Set([
|
|
618
|
+
"text/vcard",
|
|
619
|
+
"text/x-vcard",
|
|
620
|
+
"text/directory",
|
|
621
|
+
"application/vcard",
|
|
622
|
+
"application/x-vcard"
|
|
623
|
+
]);
|
|
624
|
+
const normalizeMimeType = (mimeType) => (mimeType.split(";")[0] ?? "").trim().toLowerCase();
|
|
625
|
+
const isVCardAttachment = (mimeType, fileName) => {
|
|
626
|
+
if (mimeType && VCARD_MIME_TYPES.has(normalizeMimeType(mimeType))) return true;
|
|
627
|
+
return Boolean(fileName?.toLowerCase().endsWith(".vcf"));
|
|
628
|
+
};
|
|
629
|
+
const vcardFileName = (contact) => {
|
|
630
|
+
return `${(contact.name?.formatted ?? contact.user?.id ?? "contact").replace(/[^a-zA-Z0-9_\-.]/g, "_")}.vcf`;
|
|
631
|
+
};
|
|
827
632
|
//#endregion
|
|
828
633
|
//#region src/remote/attachments.ts
|
|
829
634
|
/**
|
|
@@ -1273,6 +1078,10 @@ const getDisplayName$1 = async (remote, spaceId) => {
|
|
|
1273
1078
|
return displayName || void 0;
|
|
1274
1079
|
};
|
|
1275
1080
|
//#endregion
|
|
1081
|
+
//#region src/shared/errors.ts
|
|
1082
|
+
const IMESSAGE_PLATFORM = "iMessage";
|
|
1083
|
+
const unsupportedRemoteContent = (type, detail) => UnsupportedError.content(type, IMESSAGE_PLATFORM, detail);
|
|
1084
|
+
//#endregion
|
|
1276
1085
|
//#region src/remote/markdown.ts
|
|
1277
1086
|
const markdownLexer = new Marked();
|
|
1278
1087
|
const BULLET = "• ";
|
|
@@ -1701,6 +1510,30 @@ const unsendMessage$1 = async (remote, spaceId, msgId) => {
|
|
|
1701
1510
|
await remote.messages.unsend(toChatGuid(spaceId), toMessageGuid(childRef?.parentGuid ?? msgId), childRef ? { partIndex: childRef.partIndex } : void 0);
|
|
1702
1511
|
};
|
|
1703
1512
|
//#endregion
|
|
1513
|
+
//#region src/remote/client.ts
|
|
1514
|
+
const isSharedMode = (clients) => clients.length === 1 && clients[0]?.phone === "shared";
|
|
1515
|
+
const availablePhones = (clients) => clients.map((c) => c.phone);
|
|
1516
|
+
const clientForPhone = (clients, phone) => {
|
|
1517
|
+
if (isSharedMode(clients)) {
|
|
1518
|
+
const entry = clients[0];
|
|
1519
|
+
if (!entry) throw new Error("No iMessage clients configured");
|
|
1520
|
+
return entry.client;
|
|
1521
|
+
}
|
|
1522
|
+
const entry = clients.find((c) => c.phone === phone);
|
|
1523
|
+
if (!entry) {
|
|
1524
|
+
const list = availablePhones(clients).join(", ") || "<none>";
|
|
1525
|
+
throw new Error(`No iMessage client serves phone ${phone}. Available: ${list}`);
|
|
1526
|
+
}
|
|
1527
|
+
return entry.client;
|
|
1528
|
+
};
|
|
1529
|
+
const randomPhone = (clients) => {
|
|
1530
|
+
if (clients.length === 0) throw new Error("No iMessage phones configured for this account");
|
|
1531
|
+
if (isSharedMode(clients)) return SHARED_PHONE;
|
|
1532
|
+
const entry = clients[Math.floor(Math.random() * clients.length)];
|
|
1533
|
+
if (!entry) throw new Error("No iMessage phones configured for this account");
|
|
1534
|
+
return entry.phone;
|
|
1535
|
+
};
|
|
1536
|
+
//#endregion
|
|
1704
1537
|
//#region src/remote/contact-share.ts
|
|
1705
1538
|
const log$2 = createLogger("spectrum.imessage.contact");
|
|
1706
1539
|
const SHARE_TTL_MS = 1440 * 60 * 1e3;
|
|
@@ -2103,18 +1936,19 @@ const groupStream = (client, phone, recover) => resumableOrderedStream({
|
|
|
2103
1936
|
processMissed: (event) => event.type === "catchup.complete" ? Promise.resolve(toCatchUpCompleteItem(event)) : toGroupItem(client, event, phone, String(event.sequence)),
|
|
2104
1937
|
subscribeLive: (cursor) => withClose(client.groups.subscribeEvents(), cursor)
|
|
2105
1938
|
});
|
|
2106
|
-
const clientStream = (client, pollCache, phone, onInbound, recover) =>
|
|
2107
|
-
messageStream(client, phone, onInbound, recover),
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
1939
|
+
const clientStream = (client, pollCache, phone, includeGroupEvents, onInbound, recover) => {
|
|
1940
|
+
const streams = [messageStream(client, phone, onInbound, recover), pollStream(client, pollCache, phone, recover)];
|
|
1941
|
+
if (includeGroupEvents) streams.push(groupStream(client, phone, recover));
|
|
1942
|
+
return mergeStreams(streams);
|
|
1943
|
+
};
|
|
2111
1944
|
const messages$1 = (clients, projectConfig) => {
|
|
2112
1945
|
const pollCache = getPollCache(clients);
|
|
2113
1946
|
const shareEnabled = projectConfig?.profile?.imessageSynced === true;
|
|
2114
1947
|
const recover = getCloudRecover(clients);
|
|
1948
|
+
const includeGroupEvents = !isSharedMode(clients);
|
|
2115
1949
|
return mergeStreams(clients.map((entry) => {
|
|
2116
1950
|
const tracker = shareEnabled ? getContactShareTracker(entry.client) : void 0;
|
|
2117
|
-
return clientStream(entry.client, pollCache, entry.phone, tracker ? (chatGuid) => tracker.maybeShare(chatGuid) : void 0, recover);
|
|
1951
|
+
return clientStream(entry.client, pollCache, entry.phone, includeGroupEvents, tracker ? (chatGuid) => tracker.maybeShare(chatGuid) : void 0, recover);
|
|
2118
1952
|
}));
|
|
2119
1953
|
};
|
|
2120
1954
|
//#endregion
|
|
@@ -2234,30 +2068,6 @@ const toSpectrumMiniApp = (url, layout, live) => asCustomizedMiniApp({
|
|
|
2234
2068
|
...live === void 0 ? {} : { live }
|
|
2235
2069
|
});
|
|
2236
2070
|
//#endregion
|
|
2237
|
-
//#region src/remote/client.ts
|
|
2238
|
-
const isSharedMode = (clients) => clients.length === 1 && clients[0]?.phone === "shared";
|
|
2239
|
-
const availablePhones = (clients) => clients.map((c) => c.phone);
|
|
2240
|
-
const clientForPhone = (clients, phone) => {
|
|
2241
|
-
if (isSharedMode(clients)) {
|
|
2242
|
-
const entry = clients[0];
|
|
2243
|
-
if (!entry) throw new Error("No iMessage clients configured");
|
|
2244
|
-
return entry.client;
|
|
2245
|
-
}
|
|
2246
|
-
const entry = clients.find((c) => c.phone === phone);
|
|
2247
|
-
if (!entry) {
|
|
2248
|
-
const list = availablePhones(clients).join(", ") || "<none>";
|
|
2249
|
-
throw new Error(`No iMessage client serves phone ${phone}. Available: ${list}`);
|
|
2250
|
-
}
|
|
2251
|
-
return entry.client;
|
|
2252
|
-
};
|
|
2253
|
-
const randomPhone = (clients) => {
|
|
2254
|
-
if (clients.length === 0) throw new Error("No iMessage phones configured for this account");
|
|
2255
|
-
if (isSharedMode(clients)) return SHARED_PHONE;
|
|
2256
|
-
const entry = clients[Math.floor(Math.random() * clients.length)];
|
|
2257
|
-
if (!entry) throw new Error("No iMessage phones configured for this account");
|
|
2258
|
-
return entry.phone;
|
|
2259
|
-
};
|
|
2260
|
-
//#endregion
|
|
2261
2071
|
//#region src/index.ts
|
|
2262
2072
|
const isPollContent = (content) => content.type === "poll" || content.type === "poll_option";
|
|
2263
2073
|
const cacheRemoteOutbound = (remote, space, record) => {
|
|
@@ -2275,7 +2085,6 @@ const cacheRemoteOutbound = (remote, space, record) => {
|
|
|
2275
2085
|
return record;
|
|
2276
2086
|
};
|
|
2277
2087
|
const handleEdit = async (client, space, content) => {
|
|
2278
|
-
if (isLocal(client)) throw UnsupportedError.action("edit", "iMessage (local mode)");
|
|
2279
2088
|
const miniAppCardSession = content.target.miniAppCardSession;
|
|
2280
2089
|
const updateMiniAppCardSession = (record) => {
|
|
2281
2090
|
const nextSession = record?.miniAppCardSession;
|
|
@@ -2299,7 +2108,6 @@ const handleEdit = async (client, space, content) => {
|
|
|
2299
2108
|
await editMessage(clientForPhone(client, space.phone), space.id, content.target.id, content.content);
|
|
2300
2109
|
};
|
|
2301
2110
|
const handleUnsend = async (client, space, content) => {
|
|
2302
|
-
if (isLocal(client)) throw UnsupportedError.action("unsend", "iMessage (local mode)");
|
|
2303
2111
|
if (isPollContent(content.target.content)) throw UnsupportedError.action("unsend", "iMessage", "iMessage polls cannot be unsent");
|
|
2304
2112
|
const remote = clientForPhone(client, space.phone);
|
|
2305
2113
|
const targetContent = content.target.content;
|
|
@@ -2310,53 +2118,43 @@ const handleUnsend = async (client, space, content) => {
|
|
|
2310
2118
|
await unsendMessage(remote, space.id, content.target.id);
|
|
2311
2119
|
};
|
|
2312
2120
|
const handleStreamText = async (client, space, content) => {
|
|
2313
|
-
if (isLocal(client)) throw UnsupportedError.action("streamText", "iMessage (local mode)", "streaming text responses require remote iMessage");
|
|
2314
2121
|
const remote = clientForPhone(client, space.phone);
|
|
2315
2122
|
return cacheRemoteOutbound(remote, space, await sendStreamText(remote, space.id, content));
|
|
2316
2123
|
};
|
|
2317
2124
|
const handleBackground = async (client, space, content) => {
|
|
2318
|
-
if (isLocal(client)) throw UnsupportedError.action("background", "iMessage (local mode)", "chat backgrounds require remote iMessage");
|
|
2319
2125
|
await setBackground(clientForPhone(client, space.phone), space.id, content);
|
|
2320
2126
|
};
|
|
2321
2127
|
const handleCustomizedMiniApp = async (client, space, content) => {
|
|
2322
|
-
if (isLocal(client)) throw UnsupportedError.action("customized-mini-app", "iMessage (local mode)", "mini app cards require remote iMessage");
|
|
2323
2128
|
const remote = clientForPhone(client, space.phone);
|
|
2324
2129
|
return cacheRemoteOutbound(remote, space, await sendCustomizedMiniApp(remote, space.id, content));
|
|
2325
2130
|
};
|
|
2326
2131
|
/**
|
|
2327
2132
|
* Render the universal `app` content. On remote it becomes a native Spectrum
|
|
2328
2133
|
* mini-app card (fixed `SPECTRUM_MINI_APP` identity + the URL + the layout
|
|
2329
|
-
* already parsed from the URL's link metadata).
|
|
2330
|
-
* so it degrades to the bare URL as a text message.
|
|
2134
|
+
* already parsed from the URL's link metadata).
|
|
2331
2135
|
*/
|
|
2332
2136
|
const handleApp = async (client, space, content) => {
|
|
2333
2137
|
const url = await content.url();
|
|
2334
|
-
if (isLocal(client)) return await send$2(client, space.id, await text(url).build());
|
|
2335
2138
|
const layout = await content.layout();
|
|
2336
2139
|
const remote = clientForPhone(client, space.phone);
|
|
2337
2140
|
return cacheRemoteOutbound(remote, space, await sendCustomizedMiniApp(remote, space.id, toSpectrumMiniApp(url, layout, content.live)));
|
|
2338
2141
|
};
|
|
2339
2142
|
const handleRead = async (client, space) => {
|
|
2340
|
-
if (isLocal(client)) throw UnsupportedError.action("read", "iMessage (local mode)", "marking chats as read requires remote iMessage");
|
|
2341
2143
|
await markRead(clientForPhone(client, space.phone), space.id);
|
|
2342
2144
|
};
|
|
2343
2145
|
const handleShareContactCard = async (client, space) => {
|
|
2344
|
-
if (isLocal(client)) throw UnsupportedError.action("shareContactCard", "iMessage (local mode)", "sharing the contact card requires remote iMessage");
|
|
2345
2146
|
await shareContactCard(clientForPhone(client, space.phone), space.id);
|
|
2346
2147
|
};
|
|
2347
2148
|
const handleTyping = async (client, space, state) => {
|
|
2348
|
-
if (isLocal(client)) return;
|
|
2349
2149
|
const remote = clientForPhone(client, space.phone);
|
|
2350
2150
|
if (state === "start") await startTyping(remote, space.id);
|
|
2351
2151
|
else await stopTyping(remote, space.id);
|
|
2352
2152
|
};
|
|
2353
2153
|
const handleRename = async (client, space, content) => {
|
|
2354
|
-
if (isLocal(client)) throw UnsupportedError.action("rename", "iMessage (local mode)", "renaming chats requires remote iMessage");
|
|
2355
2154
|
if (space.type !== "group") throw UnsupportedError.action("rename", "iMessage", "only group chats can be renamed (this space is a DM)");
|
|
2356
2155
|
await setDisplayName(clientForPhone(client, space.phone), space.id, content);
|
|
2357
2156
|
};
|
|
2358
2157
|
const handleAvatar = async (client, space, content) => {
|
|
2359
|
-
if (isLocal(client)) throw UnsupportedError.action("avatar", "iMessage (local mode)", "setting group avatars requires remote iMessage");
|
|
2360
2158
|
if (space.type !== "group") throw UnsupportedError.action("avatar", "iMessage", "only group chats have avatars (this space is a DM)");
|
|
2361
2159
|
await setIcon(clientForPhone(client, space.phone), space.id, content);
|
|
2362
2160
|
};
|
|
@@ -2366,27 +2164,17 @@ const handleAvatar = async (client, space, content) => {
|
|
|
2366
2164
|
* guard sequence.
|
|
2367
2165
|
*/
|
|
2368
2166
|
const remoteGroupClient = (client, space, action, detail) => {
|
|
2369
|
-
if (
|
|
2370
|
-
if (space.type !== "group") throw UnsupportedError.action(action, "iMessage", detail.dm);
|
|
2167
|
+
if (space.type !== "group") throw UnsupportedError.action(action, "iMessage", detail);
|
|
2371
2168
|
return clientForPhone(client, space.phone);
|
|
2372
2169
|
};
|
|
2373
2170
|
const handleAddMember = async (client, space, content) => {
|
|
2374
|
-
await addParticipants(remoteGroupClient(client, space, "addMember",
|
|
2375
|
-
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)",
|
|
2376
|
-
local: "adding members requires remote iMessage"
|
|
2377
|
-
}), space.id, content);
|
|
2171
|
+
await addParticipants(remoteGroupClient(client, space, "addMember", "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)"), space.id, content);
|
|
2378
2172
|
};
|
|
2379
2173
|
const handleRemoveMember = async (client, space, content) => {
|
|
2380
|
-
await removeParticipants(remoteGroupClient(client, space, "removeMember",
|
|
2381
|
-
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)",
|
|
2382
|
-
local: "removing members requires remote iMessage"
|
|
2383
|
-
}), space.id, content);
|
|
2174
|
+
await removeParticipants(remoteGroupClient(client, space, "removeMember", "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)"), space.id, content);
|
|
2384
2175
|
};
|
|
2385
2176
|
const handleLeaveSpace = async (client, space) => {
|
|
2386
|
-
await leaveGroup(remoteGroupClient(client, space, "leaveSpace",
|
|
2387
|
-
dm: "only group chats can be left (this space is a DM)",
|
|
2388
|
-
local: "leaving chats requires remote iMessage"
|
|
2389
|
-
}), space.id);
|
|
2177
|
+
await leaveGroup(remoteGroupClient(client, space, "leaveSpace", "only group chats can be left (this space is a DM)"), space.id);
|
|
2390
2178
|
};
|
|
2391
2179
|
/**
|
|
2392
2180
|
* Dispatch the iMessage-only fire-and-forget control signals that live outside
|
|
@@ -2408,21 +2196,18 @@ const handleProviderControlSignal = async (client, space, content) => {
|
|
|
2408
2196
|
};
|
|
2409
2197
|
/**
|
|
2410
2198
|
* Resolve the remote client for a `reply` / `reaction` whose target is another
|
|
2411
|
-
* message.
|
|
2412
|
-
*
|
|
2413
|
-
* `pollNoun` is the plural used in the poll-unsupported message.
|
|
2199
|
+
* message. `action` labels the error and `pollNoun` is the plural used in the
|
|
2200
|
+
* poll-unsupported message.
|
|
2414
2201
|
*/
|
|
2415
2202
|
const remoteForMessageTarget = (client, space, target, action, pollNoun) => {
|
|
2416
|
-
if (isLocal(client)) throw UnsupportedError.action(action, "iMessage (local mode)");
|
|
2417
2203
|
if (isPollContent(target.content)) throw UnsupportedError.action(action, "iMessage", `iMessage polls do not support ${pollNoun}`);
|
|
2418
2204
|
return clientForPhone(client, space.phone);
|
|
2419
2205
|
};
|
|
2420
2206
|
const imessage = definePlatform("iMessage", {
|
|
2421
2207
|
config: configSchema,
|
|
2422
|
-
static: { effect: { message:
|
|
2208
|
+
static: { effect: { message: messageEffects } },
|
|
2423
2209
|
lifecycle: {
|
|
2424
2210
|
createClient: async ({ config, projectId, projectSecret }) => {
|
|
2425
|
-
if (config.local) return new IMessageSDK();
|
|
2426
2211
|
if (config.clients) return (Array.isArray(config.clients) ? config.clients : [config.clients]).map((e) => ({
|
|
2427
2212
|
phone: e.phone,
|
|
2428
2213
|
client: createClient({
|
|
@@ -2433,14 +2218,10 @@ const imessage = definePlatform("iMessage", {
|
|
|
2433
2218
|
token: e.token
|
|
2434
2219
|
})
|
|
2435
2220
|
}));
|
|
2436
|
-
if (!(projectId && projectSecret)) throw new Error("iMessage requires projectId and projectSecret.
|
|
2221
|
+
if (!(projectId && projectSecret)) throw new Error("Cloud iMessage requires projectId and projectSecret. Pass credentials to Spectrum() or provide explicit clients with imessage.config({ clients: [...] }). For local Messages access, install @spectrum-ts/imessage-local and use its imessage.config().");
|
|
2437
2222
|
return await createCloudClients(projectId, projectSecret);
|
|
2438
2223
|
},
|
|
2439
2224
|
destroyClient: async ({ client }) => {
|
|
2440
|
-
if (isLocal(client)) {
|
|
2441
|
-
await client.close();
|
|
2442
|
-
return;
|
|
2443
|
-
}
|
|
2444
2225
|
await disposeCloudAuth(client);
|
|
2445
2226
|
await Promise.all(client.map((entry) => entry.client.close()));
|
|
2446
2227
|
}
|
|
@@ -2454,14 +2235,6 @@ const imessage = definePlatform("iMessage", {
|
|
|
2454
2235
|
params: spaceParamsSchema,
|
|
2455
2236
|
create: async ({ input, client }) => {
|
|
2456
2237
|
if (input.users.length === 0) throw new Error("iMessage space creation requires at least one user");
|
|
2457
|
-
if (isLocal(client)) {
|
|
2458
|
-
if (input.users.length > 1) throw UnsupportedError.action("space.create", "iMessage (local mode)", "local mode cannot create group chats — use space.get(chatGuid) for an existing group");
|
|
2459
|
-
return {
|
|
2460
|
-
id: dmChatGuid(input.users[0]?.id ?? ""),
|
|
2461
|
-
type: "dm",
|
|
2462
|
-
phone: ""
|
|
2463
|
-
};
|
|
2464
|
-
}
|
|
2465
2238
|
if (client.length === 0) throw new Error("No iMessage clients configured");
|
|
2466
2239
|
const addresses = input.users.map((u) => u.id);
|
|
2467
2240
|
if (isSharedMode(client)) {
|
|
@@ -2481,11 +2254,6 @@ const imessage = definePlatform("iMessage", {
|
|
|
2481
2254
|
};
|
|
2482
2255
|
},
|
|
2483
2256
|
get: async ({ input, client }) => {
|
|
2484
|
-
if (isLocal(client)) return {
|
|
2485
|
-
id: input.id,
|
|
2486
|
-
type: chatTypeFromGuid(input.id),
|
|
2487
|
-
phone: ""
|
|
2488
|
-
};
|
|
2489
2257
|
if (client.length === 0) throw new Error("No iMessage clients configured");
|
|
2490
2258
|
const phone = isSharedMode(client) ? SHARED_PHONE : input.params?.phone ?? (client.length === 1 ? client[0]?.phone : void 0);
|
|
2491
2259
|
if (!phone) throw new Error(`iMessage space.get requires params.phone when multiple clients are configured. Available: ${availablePhones(client).join(", ")}`);
|
|
@@ -2505,7 +2273,7 @@ const imessage = definePlatform("iMessage", {
|
|
|
2505
2273
|
}
|
|
2506
2274
|
},
|
|
2507
2275
|
message: { schema: messageSchema },
|
|
2508
|
-
messages: ({ client, projectConfig }) =>
|
|
2276
|
+
messages: ({ client, projectConfig }) => messages(client, projectConfig),
|
|
2509
2277
|
send: async ({ space, content, client }) => {
|
|
2510
2278
|
if (content.type === "reply") {
|
|
2511
2279
|
const remote = remoteForMessageTarget(client, space, content.target, "reply", "replies");
|
|
@@ -2555,35 +2323,23 @@ const imessage = definePlatform("iMessage", {
|
|
|
2555
2323
|
if (content.type === "app") return await handleApp(client, space, content);
|
|
2556
2324
|
if (await handleProviderControlSignal(client, space, content)) return;
|
|
2557
2325
|
if (isCustomizedMiniApp(content)) return await handleCustomizedMiniApp(client, space, content);
|
|
2558
|
-
if (isLocal(client)) return await send$2(client, space.id, content);
|
|
2559
2326
|
const remote = clientForPhone(client, space.phone);
|
|
2560
2327
|
return cacheRemoteOutbound(remote, space, await send(remote, space.id, content));
|
|
2561
2328
|
},
|
|
2562
2329
|
actions: {
|
|
2563
2330
|
getMessage: async ({ client }, space, messageId) => {
|
|
2564
|
-
if (isLocal(client)) return getMessage$2(client, messageId);
|
|
2565
2331
|
return getMessage(clientForPhone(client, space.phone), space.id, messageId, space.phone);
|
|
2566
2332
|
},
|
|
2567
2333
|
getMembers: async ({ client }, space) => {
|
|
2568
|
-
return await listParticipants(remoteGroupClient(client, space, "getMembers",
|
|
2569
|
-
dm: "only group chats support listing members (this space is a DM)",
|
|
2570
|
-
local: "listing members requires remote iMessage"
|
|
2571
|
-
}), space.id, space.phone);
|
|
2334
|
+
return await listParticipants(remoteGroupClient(client, space, "getMembers", "only group chats support listing members (this space is a DM)"), space.id, space.phone);
|
|
2572
2335
|
},
|
|
2573
2336
|
getAvatar: async ({ client }, space) => {
|
|
2574
|
-
return await getIcon(remoteGroupClient(client, space, "getAvatar",
|
|
2575
|
-
dm: "only group chats have avatars (this space is a DM)",
|
|
2576
|
-
local: "fetching group avatars requires remote iMessage"
|
|
2577
|
-
}), space.id);
|
|
2337
|
+
return await getIcon(remoteGroupClient(client, space, "getAvatar", "only group chats have avatars (this space is a DM)"), space.id);
|
|
2578
2338
|
},
|
|
2579
2339
|
getDisplayName: async ({ client }, space) => {
|
|
2580
|
-
return await getDisplayName(remoteGroupClient(client, space, "getDisplayName",
|
|
2581
|
-
dm: "only group chats have display names (this space is a DM)",
|
|
2582
|
-
local: "reading chat display names requires remote iMessage"
|
|
2583
|
-
}), space.id);
|
|
2340
|
+
return await getDisplayName(remoteGroupClient(client, space, "getDisplayName", "only group chats have display names (this space is a DM)"), space.id);
|
|
2584
2341
|
},
|
|
2585
2342
|
getAttachment: async ({ client }, guid, phone) => {
|
|
2586
|
-
if (isLocal(client)) throw UnsupportedError.action("getAttachment", "iMessage (local mode)", "fetching attachments by GUID requires remote iMessage");
|
|
2587
2343
|
if (client.length === 0) throw new Error("No iMessage clients configured");
|
|
2588
2344
|
const routedPhone = (() => {
|
|
2589
2345
|
if (isSharedMode(client)) return SHARED_PHONE;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spectrum-ts/imessage",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "iMessage provider for spectrum-ts
|
|
3
|
+
"version": "10.0.0",
|
|
4
|
+
"description": "iMessage provider for spectrum-ts.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/photon-hq/spectrum-ts.git",
|
|
@@ -32,14 +32,13 @@
|
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@photon-ai/advanced-imessage": "^1.0.0",
|
|
35
|
-
"@photon-ai/imessage-kit": "^3.0.0",
|
|
36
35
|
"@photon-ai/otel": "^3.1.0",
|
|
37
36
|
"lru-cache": "^11.0.0",
|
|
38
37
|
"marked": "^18.0.5",
|
|
39
38
|
"zod": "^4.2.1"
|
|
40
39
|
},
|
|
41
40
|
"peerDependencies": {
|
|
42
|
-
"@spectrum-ts/core": "^
|
|
41
|
+
"@spectrum-ts/core": "^10.0.0",
|
|
43
42
|
"typescript": "^5 || ^6.0.0"
|
|
44
43
|
},
|
|
45
44
|
"license": "MIT"
|