spectrum-ts 4.1.0 → 5.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.
Files changed (46) hide show
  1. package/README.md +29 -67
  2. package/dist/authoring.d.ts +1 -6
  3. package/dist/authoring.js +2 -36
  4. package/dist/elysia.d.ts +1 -0
  5. package/dist/elysia.js +2 -0
  6. package/dist/express.d.ts +1 -0
  7. package/dist/express.js +2 -0
  8. package/dist/hono.d.ts +1 -0
  9. package/dist/hono.js +2 -0
  10. package/dist/index.d.ts +1 -2837
  11. package/dist/index.js +2 -3373
  12. package/dist/manifest.json +5 -5
  13. package/dist/providers/imessage/index.d.ts +1 -222
  14. package/dist/providers/imessage/index.js +2 -25
  15. package/dist/providers/index.d.ts +6 -19
  16. package/dist/providers/index.js +6 -34
  17. package/dist/providers/slack/index.d.ts +1 -46
  18. package/dist/providers/slack/index.js +2 -11
  19. package/dist/providers/telegram/index.d.ts +1 -45
  20. package/dist/providers/telegram/index.js +2 -13
  21. package/dist/providers/terminal/index.d.ts +1 -119
  22. package/dist/providers/terminal/index.js +2 -13
  23. package/dist/providers/whatsapp-business/index.d.ts +1 -27
  24. package/dist/providers/whatsapp-business/index.js +2 -14
  25. package/package.json +23 -26
  26. package/dist/attachment-CnivEhr6.d.ts +0 -29
  27. package/dist/authoring-b9AhXgPI.d.ts +0 -304
  28. package/dist/chunk-2D27WW5B.js +0 -63
  29. package/dist/chunk-34FQGGD7.js +0 -34
  30. package/dist/chunk-3GEJYGZK.js +0 -84
  31. package/dist/chunk-3KWFP4L2.js +0 -864
  32. package/dist/chunk-5XEFJBN2.js +0 -197
  33. package/dist/chunk-6UZFVXQF.js +0 -374
  34. package/dist/chunk-A37PM5N2.js +0 -91
  35. package/dist/chunk-B52VPQO3.js +0 -1379
  36. package/dist/chunk-DMT6BFJV.js +0 -2980
  37. package/dist/chunk-FAIFTUV2.js +0 -139
  38. package/dist/chunk-IM5ADDZS.js +0 -887
  39. package/dist/chunk-LZXPLXZF.js +0 -35
  40. package/dist/chunk-U3QQ56YZ.js +0 -929
  41. package/dist/chunk-UXAKIXVM.js +0 -409
  42. package/dist/chunk-WXLQNANA.js +0 -539
  43. package/dist/chunk-ZR3TKZMT.js +0 -129
  44. package/dist/read-C4uvozGX.d.ts +0 -53
  45. package/dist/types-BIta6Kxi.d.ts +0 -82
  46. package/dist/types-CyfLJXgu.d.ts +0 -1530
@@ -1,2980 +0,0 @@
1
- import { createRequire as __spectrumCreateRequire } from "node:module"; const require = __spectrumCreateRequire(import.meta.url);
2
- import {
3
- asRichlink
4
- } from "./chunk-ZR3TKZMT.js";
5
- import {
6
- asGroup,
7
- groupSchema
8
- } from "./chunk-LZXPLXZF.js";
9
- import {
10
- asPoll,
11
- asPollOption
12
- } from "./chunk-2D27WW5B.js";
13
- import {
14
- cloud
15
- } from "./chunk-3GEJYGZK.js";
16
- import {
17
- asContact
18
- } from "./chunk-A37PM5N2.js";
19
- import {
20
- mergeStreams,
21
- stream
22
- } from "./chunk-5XEFJBN2.js";
23
- import {
24
- fromVCard,
25
- toVCard
26
- } from "./chunk-6UZFVXQF.js";
27
- import {
28
- UnsupportedError,
29
- buildPhotoAction,
30
- definePlatform,
31
- markdownSchema,
32
- photoActionSchema
33
- } from "./chunk-B52VPQO3.js";
34
- import {
35
- asAttachment,
36
- asCustom,
37
- asText,
38
- attachmentSchema,
39
- reactionSchema,
40
- text,
41
- textSchema
42
- } from "./chunk-UXAKIXVM.js";
43
-
44
- // src/providers/imessage/index.ts
45
- import {
46
- createClient as createClient2,
47
- MessageEffect as MessageEffect2
48
- } from "@photon-ai/advanced-imessage";
49
- import { IMessageSDK as IMessageSDK2 } from "@photon-ai/imessage-kit";
50
- import { withSpan } from "@photon-ai/otel";
51
-
52
- // src/providers/imessage/content/background.ts
53
- import z from "zod";
54
- var backgroundSchema = z.object({
55
- type: z.literal("background"),
56
- __platform: z.literal("iMessage"),
57
- __fireAndForget: z.literal(true),
58
- action: photoActionSchema
59
- });
60
- var isBackground = (v) => backgroundSchema.safeParse(v).success;
61
- function background(input, options) {
62
- const action = buildPhotoAction(input, options, "background");
63
- return {
64
- build: async () => backgroundSchema.parse({
65
- type: "background",
66
- __platform: "iMessage",
67
- __fireAndForget: true,
68
- action
69
- })
70
- };
71
- }
72
-
73
- // src/providers/imessage/content/customized-mini-app.ts
74
- import z2 from "zod";
75
- var layoutSchema = z2.object({
76
- caption: z2.string().nonempty().optional(),
77
- subcaption: z2.string().nonempty().optional(),
78
- trailingCaption: z2.string().nonempty().optional(),
79
- trailingSubcaption: z2.string().nonempty().optional(),
80
- image: z2.instanceof(Uint8Array).optional(),
81
- imageTitle: z2.string().nonempty().optional(),
82
- imageSubtitle: z2.string().nonempty().optional(),
83
- summary: z2.string().nonempty().optional()
84
- }).refine(
85
- (layout) => layout.caption !== void 0 || layout.subcaption !== void 0 || layout.trailingCaption !== void 0 || layout.trailingSubcaption !== void 0 || layout.image !== void 0,
86
- {
87
- message: "layout must set at least one of caption, subcaption, trailingCaption, trailingSubcaption, image"
88
- }
89
- ).refine(
90
- (layout) => layout.image === void 0 === (layout.imageTitle === void 0),
91
- {
92
- message: "layout.image and layout.imageTitle must be set together",
93
- path: ["imageTitle"]
94
- }
95
- ).refine(
96
- (layout) => layout.imageSubtitle === void 0 || layout.image !== void 0,
97
- {
98
- message: "layout.imageSubtitle requires layout.image",
99
- path: ["imageSubtitle"]
100
- }
101
- );
102
- var customizedMiniAppSchema = z2.object({
103
- type: z2.literal("customized-mini-app"),
104
- __platform: z2.literal("iMessage"),
105
- // Display name of the owning app, shown by Messages fallback UI.
106
- appName: z2.string().nonempty(),
107
- // Apple App Store numeric id of the owning app. Positive when set; omit to
108
- // send a card whose extension is not published on the App Store.
109
- appStoreId: z2.number().int().positive().optional(),
110
- // Bundle identifier of the iMessage extension target.
111
- extensionBundleId: z2.string().nonempty(),
112
- // Visible card layout.
113
- layout: layoutSchema,
114
- // 10-character uppercase alphanumeric Apple Team ID.
115
- teamId: z2.string(),
116
- // Absolute URL delivered to the installed extension on tap.
117
- url: z2.url()
118
- });
119
- var isCustomizedMiniApp = (v) => customizedMiniAppSchema.safeParse(v).success;
120
- var asCustomizedMiniApp = (input) => customizedMiniAppSchema.parse({
121
- type: "customized-mini-app",
122
- __platform: "iMessage",
123
- ...input
124
- });
125
- function customizedMiniApp(input) {
126
- return {
127
- build: async () => asCustomizedMiniApp(input)
128
- };
129
- }
130
-
131
- // src/providers/imessage/content/effect.ts
132
- import {
133
- MessageEffect
134
- } from "@photon-ai/advanced-imessage";
135
-
136
- // src/content/effect.ts
137
- import z3 from "zod";
138
- var effectInnerSchema = z3.discriminatedUnion("type", [
139
- textSchema,
140
- markdownSchema,
141
- attachmentSchema
142
- ]);
143
- var messageEffectSchema = z3.object({
144
- type: z3.literal("effect"),
145
- content: effectInnerSchema,
146
- effect: z3.string().nonempty()
147
- });
148
-
149
- // src/providers/imessage/content/effect.ts
150
- var SUPPORTED_EFFECTS = new Set(Object.values(MessageEffect));
151
- var resolveContent = (input) => typeof input === "string" ? text(input).build() : input.build();
152
- function effect(input, messageEffect) {
153
- return {
154
- build: async () => {
155
- if (!SUPPORTED_EFFECTS.has(messageEffect)) {
156
- throw new Error(
157
- `Unsupported iMessage message effect "${messageEffect}"`
158
- );
159
- }
160
- const inner = await resolveContent(input);
161
- if (inner.type !== "text" && inner.type !== "markdown" && inner.type !== "attachment") {
162
- throw new Error(
163
- `imessage effect() only supports text, markdown, and attachment content, got "${inner.type}"`
164
- );
165
- }
166
- return messageEffectSchema.parse({
167
- type: "effect",
168
- content: inner,
169
- effect: messageEffect
170
- });
171
- }
172
- };
173
- }
174
-
175
- // src/providers/imessage/auth.ts
176
- import { createClient } from "@photon-ai/advanced-imessage";
177
-
178
- // src/providers/imessage/types.ts
179
- import { IMessageSDK } from "@photon-ai/imessage-kit";
180
- import z4 from "zod";
181
- var SHARED_PHONE = "shared";
182
- var isLocal = (client) => client instanceof IMessageSDK;
183
- var clientEntry = z4.object({
184
- address: z4.string(),
185
- token: z4.string(),
186
- phone: z4.string()
187
- });
188
- var configSchema = z4.union([
189
- z4.object({ local: z4.literal(true) }),
190
- z4.object({
191
- local: z4.literal(false).optional().default(false),
192
- clients: clientEntry.or(z4.array(clientEntry)).optional()
193
- })
194
- ]);
195
- var userSchema = z4.object({});
196
- var spaceSchema = z4.object({
197
- id: z4.string(),
198
- type: z4.enum(["dm", "group"]),
199
- phone: z4.string()
200
- });
201
- var spaceParamsSchema = z4.object({
202
- phone: z4.string().optional()
203
- });
204
- var messageSchema = z4.object({
205
- partIndex: z4.number().int().nonnegative().optional(),
206
- parentId: z4.string().optional()
207
- });
208
-
209
- // src/providers/imessage/auth.ts
210
- var RENEWAL_RATIO = 0.8;
211
- var EXPIRY_BUFFER_MS = 3e4;
212
- var RETRY_DELAY_MS = 3e4;
213
- var cloudAuthState = /* @__PURE__ */ new WeakMap();
214
- var requirePhone = (data, instanceId) => {
215
- const phone = data.numbers?.[instanceId];
216
- if (!phone) {
217
- throw new Error(`iMessage instance ${instanceId} has no phone assigned`);
218
- }
219
- return phone;
220
- };
221
- async function createCloudClients(projectId, projectSecret) {
222
- let tokenData = await cloud.issueImessageTokens(projectId, projectSecret);
223
- let tokenExpiresAt = Date.now() + tokenData.expiresIn * 1e3;
224
- let disposed = false;
225
- let renewalTimer;
226
- const records = [];
227
- const syncPhones = (data) => {
228
- for (const { entry, instanceId } of records) {
229
- entry.phone = requirePhone(data, instanceId);
230
- }
231
- };
232
- const scheduleRenewal = () => {
233
- if (disposed) {
234
- return;
235
- }
236
- const ttlMs = tokenData.expiresIn * 1e3;
237
- const renewInMs = Math.max(ttlMs * RENEWAL_RATIO, 5e3);
238
- renewalTimer = setTimeout(async () => {
239
- try {
240
- tokenData = await cloud.issueImessageTokens(projectId, projectSecret);
241
- tokenExpiresAt = Date.now() + tokenData.expiresIn * 1e3;
242
- if (tokenData.type === "dedicated") {
243
- syncPhones(tokenData);
244
- }
245
- scheduleRenewal();
246
- } catch {
247
- renewalTimer = setTimeout(() => scheduleRenewal(), RETRY_DELAY_MS);
248
- renewalTimer?.unref?.();
249
- }
250
- }, renewInMs);
251
- renewalTimer?.unref?.();
252
- };
253
- scheduleRenewal();
254
- const refreshIfNeeded = async () => {
255
- if (Date.now() < tokenExpiresAt - EXPIRY_BUFFER_MS) {
256
- return;
257
- }
258
- tokenData = await cloud.issueImessageTokens(projectId, projectSecret);
259
- tokenExpiresAt = Date.now() + tokenData.expiresIn * 1e3;
260
- if (tokenData.type === "dedicated") {
261
- syncPhones(tokenData);
262
- }
263
- scheduleRenewal();
264
- };
265
- if (tokenData.type === "shared") {
266
- const address = process.env.SPECTRUM_IMESSAGE_ADDRESS ?? "imessage.spectrum.photon.codes:443";
267
- const entries2 = [
268
- {
269
- phone: SHARED_PHONE,
270
- client: createClient({
271
- address,
272
- tls: true,
273
- token: async () => {
274
- await refreshIfNeeded();
275
- return tokenData.token;
276
- }
277
- })
278
- }
279
- ];
280
- cloudAuthState.set(entries2, {
281
- dispose: () => {
282
- disposed = true;
283
- if (renewalTimer !== void 0) {
284
- clearTimeout(renewalTimer);
285
- renewalTimer = void 0;
286
- }
287
- }
288
- });
289
- return entries2;
290
- }
291
- const dedicated = tokenData;
292
- for (const [instanceId, token] of Object.entries(dedicated.auth)) {
293
- const entry = {
294
- phone: requirePhone(dedicated, instanceId),
295
- client: createClient({
296
- address: `${instanceId}.imsg.photon.codes:443`,
297
- tls: true,
298
- token: async () => {
299
- await refreshIfNeeded();
300
- const data = tokenData;
301
- return data.auth[instanceId] ?? token;
302
- }
303
- })
304
- };
305
- records.push({ entry, instanceId });
306
- }
307
- const entries = records.map((r) => r.entry);
308
- cloudAuthState.set(entries, {
309
- dispose: () => {
310
- disposed = true;
311
- if (renewalTimer !== void 0) {
312
- clearTimeout(renewalTimer);
313
- renewalTimer = void 0;
314
- }
315
- }
316
- });
317
- return entries;
318
- }
319
- async function disposeCloudAuth(clients) {
320
- const auth = cloudAuthState.get(clients);
321
- if (auth) {
322
- auth.dispose();
323
- cloudAuthState.delete(clients);
324
- }
325
- }
326
-
327
- // src/providers/imessage/cache.ts
328
- var DEFAULT_MAX = 1e3;
329
- var MessageCache = class {
330
- map = /* @__PURE__ */ new Map();
331
- max;
332
- constructor(max = DEFAULT_MAX) {
333
- this.max = max;
334
- }
335
- get(id) {
336
- return this.map.get(id);
337
- }
338
- set(id, message) {
339
- if (this.map.has(id)) {
340
- this.map.delete(id);
341
- }
342
- this.map.set(id, message);
343
- if (this.map.size > this.max) {
344
- const first = this.map.keys().next().value;
345
- if (first !== void 0) {
346
- this.map.delete(first);
347
- }
348
- }
349
- }
350
- clear() {
351
- this.map.clear();
352
- }
353
- };
354
- var PollCache = class {
355
- map = /* @__PURE__ */ new Map();
356
- max;
357
- constructor(max = DEFAULT_MAX) {
358
- this.max = max;
359
- }
360
- get(id) {
361
- return this.map.get(id);
362
- }
363
- set(id, poll) {
364
- if (this.map.has(id)) {
365
- this.map.delete(id);
366
- }
367
- this.map.set(id, poll);
368
- if (this.map.size > this.max) {
369
- const first = this.map.keys().next().value;
370
- if (first !== void 0) {
371
- this.map.delete(first);
372
- }
373
- }
374
- }
375
- clear() {
376
- this.map.clear();
377
- }
378
- };
379
- var messageCaches = /* @__PURE__ */ new WeakMap();
380
- var pollCaches = /* @__PURE__ */ new WeakMap();
381
- var getMessageCache = (owner) => {
382
- let cache = messageCaches.get(owner);
383
- if (!cache) {
384
- cache = new MessageCache();
385
- messageCaches.set(owner, cache);
386
- }
387
- return cache;
388
- };
389
- var getPollCache = (owner) => {
390
- let cache = pollCaches.get(owner);
391
- if (!cache) {
392
- cache = new PollCache();
393
- pollCaches.set(owner, cache);
394
- }
395
- return cache;
396
- };
397
-
398
- // src/providers/imessage/local/inbound.ts
399
- import { setTimeout as sleep } from "timers/promises";
400
-
401
- // src/providers/imessage/local/attachments.ts
402
- import { createReadStream } from "fs";
403
- import { readFile } from "fs/promises";
404
- import { Readable } from "stream";
405
-
406
- // src/providers/imessage/shared/vcard.ts
407
- var VCARD_MIME_TYPES = /* @__PURE__ */ new Set([
408
- "text/vcard",
409
- "text/x-vcard",
410
- "text/directory",
411
- "application/vcard",
412
- "application/x-vcard"
413
- ]);
414
- var normalizeMimeType = (mimeType) => (mimeType.split(";")[0] ?? "").trim().toLowerCase();
415
- var isVCardAttachment = (mimeType, fileName) => {
416
- if (mimeType && VCARD_MIME_TYPES.has(normalizeMimeType(mimeType))) {
417
- return true;
418
- }
419
- return Boolean(fileName?.toLowerCase().endsWith(".vcf"));
420
- };
421
- var vcardFileName = (contact) => {
422
- const base = contact.name?.formatted ?? contact.user?.id ?? "contact";
423
- return `${base.replace(/[^a-zA-Z0-9_\-.]/g, "_")}.vcf`;
424
- };
425
-
426
- // src/providers/imessage/local/attachments.ts
427
- var DEFAULT_ATTACHMENT_NAME = "attachment";
428
- var readLocalAttachment = async (att) => {
429
- if (!att.localPath) {
430
- throw new Error(
431
- `iMessage attachment ${att.id} has no local file available on disk`
432
- );
433
- }
434
- return readFile(att.localPath);
435
- };
436
- var toAttachmentContent = (att) => {
437
- const { localPath } = att;
438
- return asAttachment({
439
- id: att.id,
440
- name: att.fileName ?? DEFAULT_ATTACHMENT_NAME,
441
- mimeType: att.mimeType,
442
- size: att.sizeBytes,
443
- read: () => readLocalAttachment(att),
444
- stream: localPath ? async () => Readable.toWeb(
445
- createReadStream(localPath)
446
- ) : void 0
447
- });
448
- };
449
- var toVCardContent = async (att) => {
450
- try {
451
- const buf = await readLocalAttachment(att);
452
- return asContact(fromVCard(buf.toString("utf8")));
453
- } catch {
454
- return toAttachmentContent(att);
455
- }
456
- };
457
- var localAttachmentContent = async (att) => isVCardAttachment(att.mimeType, att.fileName) ? await toVCardContent(att) : toAttachmentContent(att);
458
-
459
- // src/providers/imessage/local/inbound.ts
460
- var ATTACHMENT_PLACEHOLDER = "\uFFFC";
461
- var ATTACHMENT_JOIN_RETRY_DELAY_MS = 250;
462
- var ATTACHMENT_JOIN_RETRY_LIMIT = 8;
463
- var ATTACHMENT_JOIN_FETCH_LIMIT = 10;
464
- var hasAttachmentPlaceholder = (message) => message.text?.includes(ATTACHMENT_PLACEHOLDER) ?? false;
465
- var isPendingAttachmentJoin = (message) => message.attachments.length === 0 && (message.hasAttachments || hasAttachmentPlaceholder(message));
466
- var refetchUntilAttachmentsSettle = async (client, message) => {
467
- if (!message.chatId) {
468
- return message;
469
- }
470
- for (let attempt = 0; attempt < ATTACHMENT_JOIN_RETRY_LIMIT; attempt += 1) {
471
- await sleep(ATTACHMENT_JOIN_RETRY_DELAY_MS);
472
- let rows;
473
- try {
474
- rows = await client.getMessages({
475
- chatId: message.chatId,
476
- limit: ATTACHMENT_JOIN_FETCH_LIMIT,
477
- since: message.createdAt
478
- });
479
- } catch {
480
- continue;
481
- }
482
- const refreshed = rows.find((row) => row.id === message.id);
483
- if (refreshed && !isPendingAttachmentJoin(refreshed)) {
484
- return refreshed;
485
- }
486
- }
487
- return message;
488
- };
489
- var toMessages = async (message) => {
490
- const { chatId, chatKind } = message;
491
- if (!chatId || chatKind === "unknown") {
492
- return [];
493
- }
494
- if (message.reaction !== null || message.kind !== "text" || message.retractedAt !== null) {
495
- return [];
496
- }
497
- if (isPendingAttachmentJoin(message)) {
498
- return [];
499
- }
500
- const base = {
501
- sender: { id: message.participant ?? "" },
502
- // Local mode has no concept of "which-of-my-phones"; phone is empty.
503
- space: {
504
- id: chatId,
505
- type: chatKind === "group" ? "group" : "dm",
506
- phone: ""
507
- },
508
- timestamp: message.createdAt
509
- };
510
- if (message.attachments.length > 0) {
511
- return Promise.all(
512
- message.attachments.map(async (att) => ({
513
- ...base,
514
- id: `${message.id}:${att.id}`,
515
- content: await localAttachmentContent(att)
516
- }))
517
- );
518
- }
519
- return [
520
- {
521
- ...base,
522
- id: message.id,
523
- content: { type: "text", text: message.text ?? "" }
524
- }
525
- ];
526
- };
527
- var messages = (client) => stream((emit, end) => {
528
- let lastPromise = Promise.resolve();
529
- const handleIncoming = async (message) => {
530
- const stableMessage = isPendingAttachmentJoin(message) ? await refetchUntilAttachmentsSettle(client, message) : message;
531
- const ms = await toMessages(stableMessage);
532
- for (const m of ms) {
533
- await emit(m);
534
- }
535
- };
536
- const startPromise = client.startWatching({
537
- onIncomingMessage: (message) => {
538
- lastPromise = lastPromise.then(() => handleIncoming(message)).catch(end);
539
- },
540
- onError: end
541
- }).catch(end);
542
- return async () => {
543
- await startPromise.catch(() => {
544
- });
545
- await client.stopWatching();
546
- await lastPromise.catch(() => {
547
- });
548
- };
549
- });
550
-
551
- // src/providers/imessage/local/send.ts
552
- import { mkdtemp, rm, writeFile } from "fs/promises";
553
- import { tmpdir } from "os";
554
- import { basename, join } from "path";
555
-
556
- // src/providers/imessage/shared/errors.ts
557
- var IMESSAGE_PLATFORM = "iMessage";
558
- var LOCAL_IMESSAGE_PLATFORM = "iMessage (local mode)";
559
- var unsupportedRemoteContent = (type, detail) => UnsupportedError.content(type, IMESSAGE_PLATFORM, detail);
560
- var unsupportedLocalContent = (type, detail) => UnsupportedError.content(type, LOCAL_IMESSAGE_PLATFORM, detail);
561
-
562
- // src/providers/imessage/local/send.ts
563
- var synthRecord = (spaceId, content) => ({
564
- id: crypto.randomUUID(),
565
- content,
566
- space: { id: spaceId },
567
- timestamp: /* @__PURE__ */ new Date()
568
- });
569
- var sendTempFile = async (client, spaceId, name, data) => {
570
- const safeName = basename(name) || DEFAULT_ATTACHMENT_NAME;
571
- const dir = await mkdtemp(join(tmpdir(), "spectrum-"));
572
- const tmp = join(dir, safeName);
573
- await writeFile(tmp, data);
574
- try {
575
- await client.send({ to: spaceId, attachments: [tmp] });
576
- } finally {
577
- await rm(dir, { recursive: true, force: true }).catch(() => {
578
- });
579
- }
580
- };
581
- var send = async (client, spaceId, content) => {
582
- switch (content.type) {
583
- case "text":
584
- await client.send({ to: spaceId, text: content.text });
585
- return synthRecord(spaceId, content);
586
- case "attachment":
587
- await sendTempFile(client, spaceId, content.name, await content.read());
588
- return synthRecord(spaceId, content);
589
- case "contact": {
590
- const vcf = await toVCard(content);
591
- await sendTempFile(
592
- client,
593
- spaceId,
594
- vcardFileName(content),
595
- Buffer.from(vcf, "utf8")
596
- );
597
- return synthRecord(spaceId, content);
598
- }
599
- case "effect":
600
- throw unsupportedLocalContent(
601
- "effect",
602
- "message effects require remote iMessage"
603
- );
604
- case "poll":
605
- throw unsupportedLocalContent("poll");
606
- default:
607
- throw unsupportedLocalContent(content.type);
608
- }
609
- };
610
- var getMessage = async (_client, _id) => void 0;
611
-
612
- // src/providers/imessage/local/api.ts
613
- var messages2 = (client) => messages(client);
614
- var send2 = async (client, spaceId, content) => send(client, spaceId, content);
615
- var getMessage2 = async (client, id) => getMessage(client, id);
616
-
617
- // src/providers/imessage/remote/ids.ts
618
- var PART_PREFIX = /^p:(\d+)\//;
619
- var dmChatGuid = (address) => `any;-;${address}`;
620
- var chatTypeFromGuid = (guid) => guid.includes(";+;") ? "group" : "dm";
621
- var toChatGuid = (value) => value;
622
- var toMessageGuid = (value) => value;
623
- var formatChildId = (partIndex, parentGuid) => `p:${partIndex}/${parentGuid}`;
624
- var parseChildId = (id) => {
625
- const match = id.match(PART_PREFIX);
626
- if (!match) {
627
- return null;
628
- }
629
- return {
630
- parentGuid: id.replace(PART_PREFIX, ""),
631
- partIndex: Number(match[1])
632
- };
633
- };
634
-
635
- // src/providers/imessage/remote/avatar.ts
636
- var setIcon = async (remote, spaceId, content) => {
637
- const chat = toChatGuid(spaceId);
638
- if (content.action.kind === "clear") {
639
- await remote.groups.removeIcon(chat);
640
- return;
641
- }
642
- const buffer = await content.action.read();
643
- await remote.groups.setIcon(
644
- chat,
645
- new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength)
646
- );
647
- };
648
-
649
- // src/providers/imessage/remote/background.ts
650
- var setBackground = async (remote, spaceId, content) => {
651
- const chat = toChatGuid(spaceId);
652
- if (content.action.kind === "clear") {
653
- await remote.chats.removeBackground(chat);
654
- return;
655
- }
656
- const buffer = await content.action.read();
657
- await remote.chats.setBackground(
658
- chat,
659
- new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength)
660
- );
661
- };
662
-
663
- // src/providers/imessage/remote/customized-mini-app.ts
664
- var sendCustomizedMiniApp = async (remote, spaceId, content) => {
665
- const chat = toChatGuid(spaceId);
666
- const message = await remote.messages.sendCustomizedMiniApp(chat, content);
667
- return {
668
- id: message.guid,
669
- content,
670
- direction: "outbound",
671
- space: { id: spaceId },
672
- timestamp: message.dateCreated
673
- };
674
- };
675
-
676
- // src/providers/imessage/remote/inbound.ts
677
- import {
678
- NotFoundError as NotFoundError2
679
- } from "@photon-ai/advanced-imessage";
680
-
681
- // src/providers/imessage/remote/attachments.ts
682
- import {
683
- NotFoundError
684
- } from "@photon-ai/advanced-imessage";
685
- var downloadPrimaryAttachmentStream = (client, attachmentGuid) => {
686
- const frames = client.attachments.downloadStream(attachmentGuid);
687
- const iterator = frames[Symbol.asyncIterator]();
688
- let closed = false;
689
- const closeFrames = async () => {
690
- if (closed) {
691
- return;
692
- }
693
- closed = true;
694
- try {
695
- await iterator.return?.();
696
- } finally {
697
- await frames.close();
698
- }
699
- };
700
- return new ReadableStream({
701
- async cancel() {
702
- await closeFrames();
703
- },
704
- async pull(controller) {
705
- try {
706
- while (true) {
707
- const result = await iterator.next();
708
- if (result.done) {
709
- controller.close();
710
- await closeFrames();
711
- return;
712
- }
713
- if (result.value.type === "primaryChunk") {
714
- controller.enqueue(result.value.data);
715
- return;
716
- }
717
- }
718
- } catch (error) {
719
- await closeFrames();
720
- throw error;
721
- }
722
- }
723
- });
724
- };
725
- var downloadPrimaryAttachment = async (client, attachmentGuid) => {
726
- const chunks = [];
727
- const frames = client.attachments.downloadStream(attachmentGuid);
728
- try {
729
- for await (const frame of frames) {
730
- if (frame.type === "primaryChunk") {
731
- chunks.push(Buffer.from(frame.data));
732
- }
733
- }
734
- } finally {
735
- await frames.close();
736
- }
737
- return Buffer.concat(chunks);
738
- };
739
- var getRemoteAttachment = async (client, guid) => {
740
- let info;
741
- try {
742
- info = await client.attachments.get(guid);
743
- } catch (err) {
744
- if (err instanceof NotFoundError) {
745
- return;
746
- }
747
- throw err;
748
- }
749
- return asAttachment({
750
- id: info.guid,
751
- name: info.fileName,
752
- mimeType: info.mimeType,
753
- size: info.totalBytes,
754
- read: () => downloadPrimaryAttachment(client, info.guid),
755
- stream: async () => downloadPrimaryAttachmentStream(client, info.guid)
756
- });
757
- };
758
-
759
- // src/providers/imessage/remote/inbound.ts
760
- var URL_BALLOON_BUNDLE_ID = "com.apple.messages.URLBalloonProvider";
761
- var getBalloonBundleId = (message) => message.content.balloonBundleId;
762
- var messageAttachments = (message) => message.content.attachments;
763
- var resolveChatGuid = (message, hint) => {
764
- if (hint) {
765
- return hint;
766
- }
767
- const first = message.chatGuids?.[0];
768
- return first ?? "";
769
- };
770
- var resolveSenderId = (message) => message.sender?.address ?? "";
771
- var isIMessageMessage = (value) => {
772
- if (typeof value !== "object" || value === null) {
773
- return false;
774
- }
775
- const record = value;
776
- return typeof record.id === "string" && record.id.length > 0 && typeof record.content === "object" && record.content !== null && typeof record.space === "object" && record.space !== null;
777
- };
778
- var asProviderGroup = (items) => groupSchema.parse({ type: "group", items });
779
- var buildMessageBase = (message, chatGuidHint, timestamp, phone) => {
780
- const chat = resolveChatGuid(message, chatGuidHint);
781
- return {
782
- direction: message.isFromMe ? "outbound" : "inbound",
783
- sender: { id: resolveSenderId(message) },
784
- space: {
785
- id: chat,
786
- type: chatTypeFromGuid(chat),
787
- phone
788
- },
789
- timestamp
790
- };
791
- };
792
- var toAttachmentContent2 = (client, info) => asAttachment({
793
- id: info.guid,
794
- name: info.fileName,
795
- mimeType: info.mimeType,
796
- size: info.totalBytes,
797
- read: async () => await downloadPrimaryAttachment(client, info.guid),
798
- stream: async () => downloadPrimaryAttachmentStream(client, info.guid)
799
- });
800
- var toVCardContent2 = async (client, info) => {
801
- try {
802
- const buf = await downloadPrimaryAttachment(client, info.guid);
803
- return asContact(fromVCard(buf.toString("utf8")));
804
- } catch (err) {
805
- console.warn(
806
- "[spectrum-ts][imessage] failed to parse vCard attachment; falling back to attachment content",
807
- { error: err, guid: info.guid }
808
- );
809
- return toAttachmentContent2(client, info);
810
- }
811
- };
812
- var attachmentContent = async (client, info) => isVCardAttachment(info.mimeType, info.fileName) ? await toVCardContent2(client, info) : toAttachmentContent2(client, info);
813
- var buildAttachmentMessage = async (client, base, info, id, partIndex, parentId) => {
814
- const content = await attachmentContent(client, info);
815
- const msg = { ...base, id, content, partIndex };
816
- if (parentId !== void 0) {
817
- msg.parentId = parentId;
818
- }
819
- return msg;
820
- };
821
- var toRichlinkMessage = (message, base, id) => {
822
- const url = message.content.text ?? "";
823
- try {
824
- return { ...base, id, content: asRichlink({ url }) };
825
- } catch (err) {
826
- console.warn(
827
- "[spectrum-ts][imessage] failed to convert message to rich link; falling back to text/custom content",
828
- { error: err, message, url }
829
- );
830
- return {
831
- ...base,
832
- id,
833
- content: url ? asText(url) : asCustom(message)
834
- };
835
- }
836
- };
837
- var rebuildFromAppleMessage = async (client, message, phone, chatGuidHint) => {
838
- const messageGuidStr = message.guid;
839
- const timestamp = message.dateCreated ?? /* @__PURE__ */ new Date();
840
- const base = buildMessageBase(message, chatGuidHint, timestamp, phone);
841
- const attachments = messageAttachments(message);
842
- if (attachments.length === 1) {
843
- const info = attachments[0];
844
- if (!info) {
845
- throw new Error("Unreachable: attachments.length === 1 but no element");
846
- }
847
- return buildAttachmentMessage(client, base, info, messageGuidStr, 0);
848
- }
849
- if (attachments.length > 1) {
850
- const items = [];
851
- for (let i = 0; i < attachments.length; i++) {
852
- const info = attachments[i];
853
- if (!info) {
854
- continue;
855
- }
856
- items.push(
857
- await buildAttachmentMessage(
858
- client,
859
- base,
860
- info,
861
- formatChildId(i, messageGuidStr),
862
- i,
863
- messageGuidStr
864
- )
865
- );
866
- }
867
- return {
868
- ...base,
869
- id: messageGuidStr,
870
- content: asProviderGroup(items)
871
- };
872
- }
873
- if (getBalloonBundleId(message) === URL_BALLOON_BUNDLE_ID) {
874
- return toRichlinkMessage(message, base, messageGuidStr);
875
- }
876
- const text2 = message.content.text;
877
- return {
878
- ...base,
879
- id: messageGuidStr,
880
- content: text2 ? asText(text2) : asCustom(message)
881
- };
882
- };
883
- var cacheMessage = (cache, message) => {
884
- cache.set(message.id, message);
885
- if (message.content.type === "group") {
886
- for (const item of message.content.items) {
887
- if (isIMessageMessage(item)) {
888
- cache.set(item.id, item);
889
- }
890
- }
891
- }
892
- };
893
- var toInboundMessages = async (client, cache, event, phone) => {
894
- const base = buildMessageBase(
895
- event.message,
896
- event.chatGuid,
897
- event.occurredAt,
898
- phone
899
- );
900
- const messageGuidStr = event.message.guid;
901
- if (getBalloonBundleId(event.message) === URL_BALLOON_BUNDLE_ID) {
902
- const msg2 = toRichlinkMessage(event.message, base, messageGuidStr);
903
- cacheMessage(cache, msg2);
904
- return [msg2];
905
- }
906
- const attachments = messageAttachments(event.message);
907
- if (attachments.length === 1) {
908
- const info = attachments[0];
909
- if (!info) {
910
- throw new Error("Unreachable: attachments.length === 1 but no element");
911
- }
912
- const msg2 = await buildAttachmentMessage(
913
- client,
914
- base,
915
- info,
916
- messageGuidStr,
917
- 0
918
- );
919
- cacheMessage(cache, msg2);
920
- return [msg2];
921
- }
922
- if (attachments.length > 1) {
923
- const items = [];
924
- for (let i = 0; i < attachments.length; i++) {
925
- const info = attachments[i];
926
- if (!info) {
927
- continue;
928
- }
929
- items.push(
930
- await buildAttachmentMessage(
931
- client,
932
- base,
933
- info,
934
- formatChildId(i, messageGuidStr),
935
- i,
936
- messageGuidStr
937
- )
938
- );
939
- }
940
- const parent = {
941
- ...base,
942
- id: messageGuidStr,
943
- content: asProviderGroup(items)
944
- };
945
- cacheMessage(cache, parent);
946
- return [parent];
947
- }
948
- const text2 = event.message.content.text;
949
- const msg = {
950
- ...base,
951
- id: messageGuidStr,
952
- content: text2 ? asText(text2) : asCustom(event.message)
953
- };
954
- cacheMessage(cache, msg);
955
- return [msg];
956
- };
957
- var getMessage3 = async (remote, spaceId, msgId, phone) => {
958
- const cache = getMessageCache(remote);
959
- const cached = cache.get(msgId);
960
- if (cached) {
961
- return cached;
962
- }
963
- const childRef = parseChildId(msgId);
964
- if (childRef) {
965
- try {
966
- const fetched = await remote.messages.get(
967
- toMessageGuid(childRef.parentGuid)
968
- );
969
- const parent = await rebuildFromAppleMessage(
970
- remote,
971
- fetched,
972
- phone,
973
- spaceId
974
- );
975
- cacheMessage(cache, parent);
976
- if (parent.content.type !== "group") {
977
- return;
978
- }
979
- const item = parent.content.items[childRef.partIndex];
980
- return isIMessageMessage(item) ? item : void 0;
981
- } catch (err) {
982
- if (err instanceof NotFoundError2) {
983
- return;
984
- }
985
- throw err;
986
- }
987
- }
988
- try {
989
- const fetched = await remote.messages.get(toMessageGuid(msgId));
990
- const rebuilt = await rebuildFromAppleMessage(
991
- remote,
992
- fetched,
993
- phone,
994
- spaceId
995
- );
996
- cacheMessage(cache, rebuilt);
997
- return rebuilt;
998
- } catch (err) {
999
- if (err instanceof NotFoundError2) {
1000
- return;
1001
- }
1002
- throw err;
1003
- }
1004
- };
1005
-
1006
- // src/providers/imessage/remote/reactions.ts
1007
- var EMOJI_TO_TAPBACK = {
1008
- "\u2764\uFE0F": "love",
1009
- "\u{1F44D}": "like",
1010
- "\u{1F44E}": "dislike",
1011
- "\u{1F602}": "laugh",
1012
- "\u203C\uFE0F": "emphasize",
1013
- "\u2753": "question"
1014
- };
1015
- var TAPBACK_TO_EMOJI = Object.fromEntries(
1016
- Object.entries(EMOJI_TO_TAPBACK).map(([emoji, kind]) => [kind, emoji])
1017
- );
1018
- var reactionEmoji = (reaction) => reaction.kind === "emoji" ? reaction.emoji : TAPBACK_TO_EMOJI[reaction.kind];
1019
- var asProviderReaction = (emoji, target) => reactionSchema.parse({
1020
- emoji,
1021
- target,
1022
- type: "reaction"
1023
- });
1024
- var resolveReactionTarget = async (client, cache, chat, targetGuid, partIndex, phone) => {
1025
- let candidate = cache.get(targetGuid);
1026
- if (!candidate) {
1027
- try {
1028
- const fetched = await client.messages.get(toMessageGuid(targetGuid));
1029
- candidate = await rebuildFromAppleMessage(client, fetched, phone, chat);
1030
- cacheMessage(cache, candidate);
1031
- } catch {
1032
- return;
1033
- }
1034
- }
1035
- if (candidate.content.type === "group") {
1036
- const items = candidate.content.items;
1037
- if (!Array.isArray(items)) {
1038
- return candidate;
1039
- }
1040
- const item = items[partIndex ?? 0];
1041
- return isIMessageMessage(item) ? item : candidate;
1042
- }
1043
- return candidate;
1044
- };
1045
- var toReactionMessages = async (client, cache, event, phone) => {
1046
- const emoji = reactionEmoji(event.reaction);
1047
- if (!emoji) {
1048
- return [];
1049
- }
1050
- const senderAddress = event.actor?.address;
1051
- if (!senderAddress) {
1052
- return [];
1053
- }
1054
- const resolved = await resolveReactionTarget(
1055
- client,
1056
- cache,
1057
- event.chatGuid,
1058
- event.messageGuid,
1059
- event.targetPartIndex,
1060
- phone
1061
- );
1062
- if (!resolved) {
1063
- return [];
1064
- }
1065
- const partSuffix = typeof event.targetPartIndex === "number" ? `:${event.targetPartIndex}` : "";
1066
- return [
1067
- {
1068
- sender: { id: senderAddress },
1069
- space: {
1070
- id: event.chatGuid,
1071
- type: chatTypeFromGuid(event.chatGuid),
1072
- phone
1073
- },
1074
- timestamp: event.occurredAt,
1075
- id: `${event.messageGuid}:reaction:${event.sequence}${partSuffix}`,
1076
- content: asProviderReaction(emoji, resolved)
1077
- }
1078
- ];
1079
- };
1080
- var toSettableReaction = (emoji) => {
1081
- const native = EMOJI_TO_TAPBACK[emoji];
1082
- return native ? { kind: native } : { kind: "emoji", emoji };
1083
- };
1084
- var tapbackTarget = (target) => ({
1085
- guid: toMessageGuid(target.parentId ?? target.id),
1086
- opts: typeof target.partIndex === "number" ? { partIndex: target.partIndex } : void 0
1087
- });
1088
- var reactToMessage = async (remote, spaceId, target, reaction) => {
1089
- const { guid, opts } = tapbackTarget(target);
1090
- const sent = await remote.messages.setReaction(
1091
- toChatGuid(spaceId),
1092
- guid,
1093
- toSettableReaction(reaction),
1094
- true,
1095
- opts
1096
- );
1097
- return {
1098
- id: sent.guid,
1099
- content: asProviderReaction(reaction, target),
1100
- direction: "outbound",
1101
- space: { id: spaceId },
1102
- timestamp: sent.dateCreated
1103
- };
1104
- };
1105
- var unsendReaction = async (remote, spaceId, target, reaction) => {
1106
- const { guid, opts } = tapbackTarget(target);
1107
- await remote.messages.setReaction(
1108
- toChatGuid(spaceId),
1109
- guid,
1110
- toSettableReaction(reaction),
1111
- false,
1112
- opts
1113
- );
1114
- };
1115
-
1116
- // src/providers/imessage/remote/read.ts
1117
- var markRead = async (remote, spaceId) => {
1118
- await remote.chats.markRead(toChatGuid(spaceId));
1119
- };
1120
-
1121
- // src/providers/imessage/remote/rename.ts
1122
- var setDisplayName = async (remote, spaceId, content) => {
1123
- await remote.groups.setDisplayName(toChatGuid(spaceId), content.displayName);
1124
- };
1125
-
1126
- // src/utils/audio.ts
1127
- import { spawn } from "child_process";
1128
- import { mkdtemp as mkdtemp2, readFile as readFile2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
1129
- import { tmpdir as tmpdir2 } from "os";
1130
- import { join as join2 } from "path";
1131
- var M4A_BRANDS = /* @__PURE__ */ new Set([
1132
- "M4A ",
1133
- "M4B ",
1134
- "M4P ",
1135
- "mp42",
1136
- "mp41",
1137
- "isom",
1138
- "iso2"
1139
- ]);
1140
- var M4A_MIME_TYPES = /* @__PURE__ */ new Set([
1141
- "audio/mp4",
1142
- "audio/mp4a-latm",
1143
- "audio/x-m4a",
1144
- "audio/aac",
1145
- "audio/aacp"
1146
- ]);
1147
- var FFMPEG_MISSING_MESSAGE = "voice content: input is not m4a/aac and ffmpeg is unavailable. Install `ffmpeg-static` or ensure `ffmpeg` is on PATH.";
1148
- var isM4a = (buffer) => {
1149
- if (buffer.length < 12) {
1150
- return false;
1151
- }
1152
- if (buffer.toString("ascii", 4, 8) !== "ftyp") {
1153
- return false;
1154
- }
1155
- return M4A_BRANDS.has(buffer.toString("ascii", 8, 12));
1156
- };
1157
- var isM4aMimeType = (mimeType) => M4A_MIME_TYPES.has(mimeType.toLowerCase());
1158
- var cachedFfmpegPath;
1159
- var tryStaticBinary = async () => {
1160
- try {
1161
- const mod = await import("ffmpeg-static");
1162
- return mod.default ?? void 0;
1163
- } catch {
1164
- return;
1165
- }
1166
- };
1167
- var resolveFfmpegPath = async () => {
1168
- if (cachedFfmpegPath) {
1169
- return cachedFfmpegPath;
1170
- }
1171
- cachedFfmpegPath = await tryStaticBinary() ?? "ffmpeg";
1172
- return cachedFfmpegPath;
1173
- };
1174
- var collectStream = (stream2) => {
1175
- if (!stream2) {
1176
- return Promise.resolve("");
1177
- }
1178
- return new Promise((resolve, reject) => {
1179
- const chunks = [];
1180
- stream2.on("data", (chunk) => {
1181
- chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
1182
- });
1183
- stream2.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
1184
- stream2.on("error", reject);
1185
- });
1186
- };
1187
- var isMissingBinaryError = (err) => err?.code === "ENOENT";
1188
- var runFfmpeg = (ffmpegPath, args) => {
1189
- const proc = spawn(ffmpegPath, args, { stdio: ["ignore", "ignore", "pipe"] });
1190
- const stderr = collectStream(proc.stderr);
1191
- const exit = new Promise((resolve, reject) => {
1192
- proc.on(
1193
- "error",
1194
- (err) => reject(
1195
- isMissingBinaryError(err) ? new Error(FFMPEG_MISSING_MESSAGE) : err
1196
- )
1197
- );
1198
- proc.on("exit", (code) => resolve(code ?? -1));
1199
- });
1200
- return Promise.all([exit, stderr]).then(([code, text2]) => ({
1201
- code,
1202
- stderr: text2
1203
- }));
1204
- };
1205
- var DURATION_PATTERN = /Duration:\s*(\d+):(\d{2}):(\d{2})(?:\.(\d{1,3}))?/;
1206
- var parseDuration = (stderr) => {
1207
- const match = stderr.match(DURATION_PATTERN);
1208
- if (!match) {
1209
- return;
1210
- }
1211
- const [, hh, mm, ss, frac] = match;
1212
- const seconds = Number(hh) * 3600 + Number(mm) * 60 + Number(ss) + Number(`0.${frac ?? 0}`);
1213
- return Number.isFinite(seconds) ? seconds : void 0;
1214
- };
1215
- var transcodeToM4a = async (buffer) => {
1216
- const ffmpeg = await resolveFfmpegPath();
1217
- const dir = await mkdtemp2(join2(tmpdir2(), "spectrum-voice-"));
1218
- const inPath = join2(dir, "in");
1219
- const outPath = join2(dir, "out.m4a");
1220
- try {
1221
- await writeFile2(inPath, buffer);
1222
- const { code, stderr } = await runFfmpeg(ffmpeg, [
1223
- "-y",
1224
- "-i",
1225
- inPath,
1226
- "-f",
1227
- "ipod",
1228
- "-c:a",
1229
- "aac",
1230
- outPath
1231
- ]);
1232
- if (code !== 0) {
1233
- throw new Error(`ffmpeg conversion failed (exit ${code}): ${stderr}`);
1234
- }
1235
- const out = await readFile2(outPath);
1236
- return { buffer: out, duration: parseDuration(stderr) };
1237
- } finally {
1238
- await rm2(dir, { recursive: true, force: true }).catch(() => {
1239
- });
1240
- }
1241
- };
1242
- var ensureM4a = async (buffer, mimeType) => {
1243
- if (isM4aMimeType(mimeType) || isM4a(buffer)) {
1244
- return { buffer };
1245
- }
1246
- return transcodeToM4a(buffer);
1247
- };
1248
-
1249
- // src/providers/imessage/remote/markdown.ts
1250
- import { Marked } from "marked";
1251
- var markdownLexer = new Marked();
1252
- var BULLET = "\u2022 ";
1253
- var HR_LINE = "\u2014\u2014\u2014";
1254
- var NESTED_LIST_INDENT = " ";
1255
- var BLOCK_SEPARATOR = "\n\n";
1256
- var TABLE_CELL_SEPARATOR = " | ";
1257
- var DEFAULT_LIST_START = 1;
1258
- var LEADING_WHITESPACE = /^\s+/;
1259
- var TRAILING_WHITESPACE = /\s+$/;
1260
- var MONOSPACE_UPPER_A = 120432;
1261
- var MONOSPACE_LOWER_A = 120458;
1262
- var MONOSPACE_DIGIT_ZERO = 120822;
1263
- var UPPER_A = 65;
1264
- var UPPER_Z = 90;
1265
- var LOWER_A = 97;
1266
- var LOWER_Z = 122;
1267
- var DIGIT_ZERO = 48;
1268
- var DIGIT_NINE = 57;
1269
- var monospaceCodePoint = (codePoint) => {
1270
- if (codePoint >= UPPER_A && codePoint <= UPPER_Z) {
1271
- return MONOSPACE_UPPER_A + (codePoint - UPPER_A);
1272
- }
1273
- if (codePoint >= LOWER_A && codePoint <= LOWER_Z) {
1274
- return MONOSPACE_LOWER_A + (codePoint - LOWER_A);
1275
- }
1276
- if (codePoint >= DIGIT_ZERO && codePoint <= DIGIT_NINE) {
1277
- return MONOSPACE_DIGIT_ZERO + (codePoint - DIGIT_ZERO);
1278
- }
1279
- return codePoint;
1280
- };
1281
- var toMonospace = (text2) => {
1282
- let out = "";
1283
- for (const char of text2) {
1284
- const codePoint = char.codePointAt(0);
1285
- out += codePoint === void 0 ? char : String.fromCodePoint(monospaceCodePoint(codePoint));
1286
- }
1287
- return out;
1288
- };
1289
- var STYLE_ORDER = ["bold", "italic", "strikethrough"];
1290
- var plain = (text2) => ({ text: text2, styles: [] });
1291
- var withStyle = (spans, style) => spans.map(
1292
- (span) => span.styles.includes(style) ? span : { ...span, styles: [...span.styles, style] }
1293
- );
1294
- var asLink = (spans) => spans.map((span) => ({ ...span, link: true }));
1295
- var spanText = (spans) => {
1296
- let out = "";
1297
- for (const span of spans) {
1298
- out += span.text;
1299
- }
1300
- return out;
1301
- };
1302
- var joinSpans = (blocks, separator) => {
1303
- const out = [];
1304
- for (const [index, block] of blocks.entries()) {
1305
- if (index > 0) {
1306
- out.push(plain(separator));
1307
- }
1308
- out.push(...block);
1309
- }
1310
- return out;
1311
- };
1312
- var splitSpanLines = (spans) => {
1313
- let current = [];
1314
- const lines = [current];
1315
- for (const span of spans) {
1316
- const parts = span.text.split("\n");
1317
- for (const [index, part] of parts.entries()) {
1318
- if (index > 0) {
1319
- current = [];
1320
- lines.push(current);
1321
- }
1322
- if (part) {
1323
- current.push({ ...span, text: part });
1324
- }
1325
- }
1326
- }
1327
- return lines;
1328
- };
1329
- var asMarkedToken = (token) => token;
1330
- var checkboxPrefix = (item) => {
1331
- if (!item.task) {
1332
- return "";
1333
- }
1334
- return item.checked ? "[x] " : "[ ] ";
1335
- };
1336
- var listMarker = (list, index) => {
1337
- if (!list.ordered) {
1338
- return BULLET;
1339
- }
1340
- const start = list.start === "" ? DEFAULT_LIST_START : list.start;
1341
- return `${start + index}. `;
1342
- };
1343
- var renderLink = (token) => {
1344
- if (token.text === token.href) {
1345
- return [{ text: token.href, styles: [], link: true }];
1346
- }
1347
- return [
1348
- ...asLink(renderInlineTokens(token.tokens)),
1349
- { text: ` (${token.href})`, styles: [], link: true }
1350
- ];
1351
- };
1352
- var renderImage = (token) => [
1353
- {
1354
- text: token.text ? `${token.text} (${token.href})` : token.href,
1355
- styles: [],
1356
- link: true
1357
- }
1358
- ];
1359
- var renderInlineToken = (token) => {
1360
- switch (token.type) {
1361
- case "strong":
1362
- return withStyle(renderInlineTokens(token.tokens), "bold");
1363
- case "em":
1364
- return withStyle(renderInlineTokens(token.tokens), "italic");
1365
- case "del":
1366
- return withStyle(renderInlineTokens(token.tokens), "strikethrough");
1367
- case "codespan":
1368
- return [plain(toMonospace(token.text))];
1369
- case "br":
1370
- return [plain("\n")];
1371
- case "link":
1372
- return renderLink(token);
1373
- case "image":
1374
- return renderImage(token);
1375
- case "escape":
1376
- return [plain(token.text)];
1377
- case "text":
1378
- return token.tokens ? renderInlineTokens(token.tokens) : [plain(token.text)];
1379
- // Raw HTML in markdown source stays literal — styled text has no markup.
1380
- case "html":
1381
- return [plain(token.text)];
1382
- // Task-item checkboxes are rendered from `ListItem.task`/`checked`.
1383
- case "checkbox":
1384
- return [];
1385
- default:
1386
- return "raw" in token ? [plain(String(token.raw))] : [];
1387
- }
1388
- };
1389
- var renderInlineTokens = (tokens) => {
1390
- const out = [];
1391
- for (const token of tokens) {
1392
- out.push(...renderInlineToken(asMarkedToken(token)));
1393
- }
1394
- return out;
1395
- };
1396
- var renderBlockquote = (quote) => {
1397
- const lines = splitSpanLines(renderBlockTokens(quote.tokens));
1398
- const out = [];
1399
- for (const [index, line] of lines.entries()) {
1400
- if (index > 0) {
1401
- out.push(plain("\n"));
1402
- }
1403
- out.push(plain(line.length > 0 ? "> " : ">"), ...line);
1404
- }
1405
- return out;
1406
- };
1407
- var renderList = (list) => {
1408
- const out = [];
1409
- for (const [index, item] of list.items.entries()) {
1410
- const prefix = `${listMarker(list, index)}${checkboxPrefix(item)}`;
1411
- const blocks = [];
1412
- for (const token of item.tokens) {
1413
- const rendered = renderBlockToken(asMarkedToken(token));
1414
- if (spanText(rendered)) {
1415
- blocks.push(rendered);
1416
- }
1417
- }
1418
- const [first = [], ...rest] = splitSpanLines(joinSpans(blocks, "\n"));
1419
- if (out.length > 0) {
1420
- out.push(plain("\n"));
1421
- }
1422
- out.push(plain(prefix), ...first);
1423
- for (const line of rest) {
1424
- out.push(plain(`
1425
- ${NESTED_LIST_INDENT}`), ...line);
1426
- }
1427
- }
1428
- return out;
1429
- };
1430
- var renderTable = (table) => {
1431
- const out = [];
1432
- const pushRow = (cells, rowIndex) => {
1433
- if (rowIndex > 0) {
1434
- out.push(plain("\n"));
1435
- }
1436
- for (const [cellIndex, cell] of cells.entries()) {
1437
- if (cellIndex > 0) {
1438
- out.push(plain(TABLE_CELL_SEPARATOR));
1439
- }
1440
- out.push(...renderInlineTokens(cell.tokens));
1441
- }
1442
- };
1443
- pushRow(table.header, 0);
1444
- for (const [index, row] of table.rows.entries()) {
1445
- pushRow(row, index + 1);
1446
- }
1447
- return out;
1448
- };
1449
- var renderBlockToken = (token) => {
1450
- switch (token.type) {
1451
- // iMessage formatting has no heading sizes; bold is the conventional
1452
- // stand-in (Telegram precedent).
1453
- case "heading":
1454
- return withStyle(renderInlineTokens(token.tokens), "bold");
1455
- case "paragraph":
1456
- return renderInlineTokens(token.tokens);
1457
- case "code":
1458
- return [plain(toMonospace(token.text))];
1459
- case "blockquote":
1460
- return renderBlockquote(token);
1461
- case "list":
1462
- return renderList(token);
1463
- case "table":
1464
- return renderTable(token);
1465
- case "hr":
1466
- return [plain(HR_LINE)];
1467
- case "space":
1468
- case "def":
1469
- return [];
1470
- default:
1471
- return renderInlineToken(token);
1472
- }
1473
- };
1474
- var renderBlockTokens = (tokens) => {
1475
- const blocks = [];
1476
- for (const token of tokens) {
1477
- const rendered = renderBlockToken(asMarkedToken(token));
1478
- if (spanText(rendered)) {
1479
- blocks.push(rendered);
1480
- }
1481
- }
1482
- return joinSpans(blocks, BLOCK_SEPARATOR);
1483
- };
1484
- var trimSpans = (spans) => {
1485
- const trimmed = [...spans];
1486
- while (trimmed.length > 0) {
1487
- const first = trimmed.at(0);
1488
- const text2 = first?.text.replace(LEADING_WHITESPACE, "");
1489
- if (first && text2) {
1490
- trimmed[0] = { ...first, text: text2 };
1491
- break;
1492
- }
1493
- trimmed.shift();
1494
- }
1495
- while (trimmed.length > 0) {
1496
- const last = trimmed.at(-1);
1497
- const text2 = last?.text.replace(TRAILING_WHITESPACE, "");
1498
- if (last && text2) {
1499
- trimmed[trimmed.length - 1] = { ...last, text: text2 };
1500
- break;
1501
- }
1502
- trimmed.pop();
1503
- }
1504
- return trimmed;
1505
- };
1506
- var finalize = (spans) => {
1507
- let text2 = "";
1508
- let hasLinks = false;
1509
- const open = /* @__PURE__ */ new Map();
1510
- const ranges = [];
1511
- const close = (style, end) => {
1512
- const start = open.get(style);
1513
- open.delete(style);
1514
- if (start !== void 0 && end > start) {
1515
- ranges.push({ type: style, start, length: end - start });
1516
- }
1517
- };
1518
- for (const span of spans) {
1519
- if (!span.text) {
1520
- continue;
1521
- }
1522
- hasLinks ||= span.link === true;
1523
- const offset = text2.length;
1524
- for (const style of STYLE_ORDER) {
1525
- if (span.styles.includes(style)) {
1526
- if (!open.has(style)) {
1527
- open.set(style, offset);
1528
- }
1529
- } else {
1530
- close(style, offset);
1531
- }
1532
- }
1533
- text2 += span.text;
1534
- }
1535
- for (const style of STYLE_ORDER) {
1536
- close(style, text2.length);
1537
- }
1538
- ranges.sort(
1539
- (a, b) => a.start - b.start || STYLE_ORDER.indexOf(a.type) - STYLE_ORDER.indexOf(b.type)
1540
- );
1541
- return { text: text2, formatting: ranges, hasLinks };
1542
- };
1543
- var markdownToIMessageText = (markdown) => finalize(trimSpans(renderBlockTokens(markdownLexer.lexer(markdown))));
1544
-
1545
- // src/providers/imessage/remote/send.ts
1546
- var GROUP_ITEM_ALLOWED = /* @__PURE__ */ new Set([
1547
- "text",
1548
- "markdown",
1549
- "attachment",
1550
- "contact",
1551
- "voice"
1552
- ]);
1553
- var GROUP_TEXT_TYPES = /* @__PURE__ */ new Set([
1554
- "text",
1555
- "markdown"
1556
- ]);
1557
- var MAX_GROUP_TEXT_ITEMS = 1;
1558
- var outboundRecord = (spaceId, id, content, timestamp, extras) => ({
1559
- id,
1560
- content,
1561
- direction: "outbound",
1562
- space: { id: spaceId },
1563
- timestamp,
1564
- ...extras
1565
- });
1566
- var outboundGroupItem = (spaceId, id, content, timestamp, partIndex, parentId) => outboundRecord(spaceId, id, content, timestamp, {
1567
- partIndex,
1568
- parentId
1569
- });
1570
- var providerGroup = (items) => asGroup({ items });
1571
- var withReply = (options, replyTo) => replyTo ? { ...options, replyTo } : options;
1572
- var replyOptions = (replyTo) => replyTo ? { replyTo } : void 0;
1573
- var effectOption = (effect2) => effect2 ? { effect: effect2 } : {};
1574
- var formattingOption = (formatting) => formatting.length > 0 ? { formatting } : {};
1575
- var dataDetectionOption = (hasLinks) => hasLinks ? { enableDataDetection: true } : {};
1576
- var renderMarkdown = (markdown) => {
1577
- const rendered = markdownToIMessageText(markdown);
1578
- if (!rendered.text) {
1579
- throw unsupportedRemoteContent(
1580
- "markdown",
1581
- "renders to empty text \u2014 nothing to send"
1582
- );
1583
- }
1584
- return rendered;
1585
- };
1586
- var replyTargetFromId = (messageId) => {
1587
- const childRef = parseChildId(messageId);
1588
- if (childRef) {
1589
- return {
1590
- guid: toMessageGuid(childRef.parentGuid),
1591
- partIndex: childRef.partIndex
1592
- };
1593
- }
1594
- return toMessageGuid(messageId);
1595
- };
1596
- var outboundMessage = (spaceId, message, content) => outboundRecord(spaceId, message.guid, content, message.dateCreated);
1597
- var outboundPoll = (spaceId, poll, content) => outboundRecord(spaceId, poll.pollMessageGuid, content, /* @__PURE__ */ new Date());
1598
- var sendVCardAttachment = (remote, name, vcf) => remote.attachments.upload({
1599
- data: Buffer.from(vcf, "utf8"),
1600
- fileName: name
1601
- });
1602
- var sendContactAttachment = async (remote, content) => {
1603
- const vcf = await toVCard(content);
1604
- const name = vcardFileName(content);
1605
- const upload = await sendVCardAttachment(remote, name, vcf);
1606
- return { guid: upload.attachment.guid, name };
1607
- };
1608
- var uploadAttachment = async (remote, content) => {
1609
- const attachment = await remote.attachments.upload({
1610
- data: await content.read(),
1611
- fileName: content.name
1612
- });
1613
- return { guid: attachment.attachment.guid, name: content.name };
1614
- };
1615
- var uploadVoice = async (remote, content) => {
1616
- const { buffer } = await ensureM4a(await content.read(), content.mimeType);
1617
- const name = content.name ?? "voice.m4a";
1618
- const attachment = await remote.attachments.upload({
1619
- data: buffer,
1620
- fileName: name
1621
- });
1622
- return { guid: attachment.attachment.guid, name };
1623
- };
1624
- var sendContent = async (remote, spaceId, chat, content, replyTo, effect2) => {
1625
- switch (content.type) {
1626
- case "effect":
1627
- return sendContent(
1628
- remote,
1629
- spaceId,
1630
- chat,
1631
- content.content,
1632
- replyTo,
1633
- content.effect
1634
- );
1635
- case "text": {
1636
- const message = await remote.messages.sendText(
1637
- chat,
1638
- content.text,
1639
- withReply(effectOption(effect2), replyTo)
1640
- );
1641
- return outboundMessage(spaceId, message, content);
1642
- }
1643
- case "markdown": {
1644
- const rendered = renderMarkdown(content.markdown);
1645
- const message = await remote.messages.sendText(
1646
- chat,
1647
- rendered.text,
1648
- withReply(
1649
- {
1650
- ...effectOption(effect2),
1651
- ...formattingOption(rendered.formatting),
1652
- ...dataDetectionOption(rendered.hasLinks)
1653
- },
1654
- replyTo
1655
- )
1656
- );
1657
- return outboundMessage(spaceId, message, content);
1658
- }
1659
- case "richlink": {
1660
- const message = await remote.messages.sendText(
1661
- chat,
1662
- content.url,
1663
- withReply({ enableLinkPreview: true }, replyTo)
1664
- );
1665
- return outboundMessage(spaceId, message, content);
1666
- }
1667
- case "attachment": {
1668
- const { guid } = await uploadAttachment(remote, content);
1669
- const message = await remote.messages.sendAttachment(
1670
- chat,
1671
- guid,
1672
- withReply(effectOption(effect2), replyTo)
1673
- );
1674
- return outboundMessage(spaceId, message, content);
1675
- }
1676
- case "contact": {
1677
- const { guid } = await sendContactAttachment(remote, content);
1678
- const message = await remote.messages.sendAttachment(
1679
- chat,
1680
- guid,
1681
- replyOptions(replyTo)
1682
- );
1683
- return outboundMessage(spaceId, message, content);
1684
- }
1685
- case "voice": {
1686
- const { guid } = await uploadVoice(remote, content);
1687
- const message = await remote.messages.sendAttachment(chat, guid, {
1688
- isAudioMessage: true,
1689
- ...replyOptions(replyTo)
1690
- });
1691
- return outboundMessage(spaceId, message, content);
1692
- }
1693
- case "poll":
1694
- if (replyTo) {
1695
- throw unsupportedRemoteContent(
1696
- "poll",
1697
- "polls cannot be sent as replies"
1698
- );
1699
- }
1700
- return outboundPoll(
1701
- spaceId,
1702
- await remote.polls.create(
1703
- chat,
1704
- content.title,
1705
- content.options.map((option) => option.title)
1706
- ),
1707
- content
1708
- );
1709
- default:
1710
- throw unsupportedRemoteContent(content.type);
1711
- }
1712
- };
1713
- var validateGroupContent = (content) => {
1714
- let textCount = 0;
1715
- for (const sub of content.items) {
1716
- const itemType = sub.content.type;
1717
- if (!GROUP_ITEM_ALLOWED.has(itemType)) {
1718
- throw unsupportedRemoteContent(
1719
- "group",
1720
- `"${itemType}" items are not supported inside a group`
1721
- );
1722
- }
1723
- if (GROUP_TEXT_TYPES.has(itemType) && ++textCount > MAX_GROUP_TEXT_ITEMS) {
1724
- throw unsupportedRemoteContent(
1725
- "group",
1726
- `groups can contain at most ${MAX_GROUP_TEXT_ITEMS} text item`
1727
- );
1728
- }
1729
- }
1730
- };
1731
- var resolvePart = async (remote, content) => {
1732
- switch (content.type) {
1733
- case "text":
1734
- return { text: content.text };
1735
- case "markdown": {
1736
- const rendered = renderMarkdown(content.markdown);
1737
- return { text: rendered.text, ...formattingOption(rendered.formatting) };
1738
- }
1739
- case "attachment": {
1740
- const { guid, name } = await uploadAttachment(remote, content);
1741
- return { attachmentGuid: guid, attachmentName: name };
1742
- }
1743
- case "contact": {
1744
- const { guid, name } = await sendContactAttachment(remote, content);
1745
- return { attachmentGuid: guid, attachmentName: name };
1746
- }
1747
- case "voice": {
1748
- const { guid, name } = await uploadVoice(remote, content);
1749
- return { attachmentGuid: guid, attachmentName: name };
1750
- }
1751
- default:
1752
- throw unsupportedRemoteContent(content.type);
1753
- }
1754
- };
1755
- var send3 = async (remote, spaceId, content) => {
1756
- const chat = toChatGuid(spaceId);
1757
- if (content.type === "group") {
1758
- validateGroupContent(content);
1759
- const resolved = await Promise.all(
1760
- content.items.map((sub) => resolvePart(remote, sub.content))
1761
- );
1762
- const message = await remote.messages.sendMultipart(
1763
- chat,
1764
- resolved.map((part, idx) => ({ ...part, bubbleIndex: idx }))
1765
- );
1766
- const parentGuid = message.guid;
1767
- const timestamp = message.dateCreated;
1768
- const items = content.items.map(
1769
- (sub, idx) => outboundGroupItem(
1770
- spaceId,
1771
- formatChildId(idx, parentGuid),
1772
- sub.content,
1773
- timestamp,
1774
- idx,
1775
- parentGuid
1776
- )
1777
- );
1778
- return outboundRecord(spaceId, parentGuid, providerGroup(items), timestamp);
1779
- }
1780
- return sendContent(remote, spaceId, chat, content);
1781
- };
1782
- var replyToMessage = async (remote, spaceId, msgId, content) => {
1783
- const chat = toChatGuid(spaceId);
1784
- return sendContent(remote, spaceId, chat, content, replyTargetFromId(msgId));
1785
- };
1786
- var editMessage = async (remote, spaceId, msgId, content) => {
1787
- if (content.type !== "text") {
1788
- throw unsupportedRemoteContent(
1789
- content.type,
1790
- "only text content can be edited"
1791
- );
1792
- }
1793
- const childRef = parseChildId(msgId);
1794
- await remote.messages.edit(
1795
- toChatGuid(spaceId),
1796
- toMessageGuid(childRef?.parentGuid ?? msgId),
1797
- content.text,
1798
- childRef ? { partIndex: childRef.partIndex } : void 0
1799
- );
1800
- };
1801
- var unsendMessage = async (remote, spaceId, msgId) => {
1802
- const childRef = parseChildId(msgId);
1803
- await remote.messages.unsend(
1804
- toChatGuid(spaceId),
1805
- toMessageGuid(childRef?.parentGuid ?? msgId),
1806
- childRef ? { partIndex: childRef.partIndex } : void 0
1807
- );
1808
- };
1809
-
1810
- // src/providers/imessage/remote/stream.ts
1811
- import {
1812
- ValidationError
1813
- } from "@photon-ai/advanced-imessage";
1814
- import { sanitizePhone } from "@photon-ai/otel";
1815
-
1816
- // src/utils/resumable-stream.ts
1817
- import { createLogger } from "@photon-ai/otel";
1818
- var CATCH_UP_PAGE_SIZE = 100;
1819
- var MAX_BUFFERED_LIVE_EVENTS = 1e3;
1820
- var RECONNECT_INITIAL_DELAY_MS = 500;
1821
- var RECONNECT_MAX_DELAY_MS = 3e4;
1822
- var PERSISTENT_FAILURE_ERROR_THRESHOLD = 5;
1823
- var log = createLogger("spectrum.stream");
1824
- var RetryableStreamError = class extends Error {
1825
- constructor(message) {
1826
- super(message);
1827
- this.name = "RetryableStreamError";
1828
- }
1829
- };
1830
- var LiveBufferOverflowError = class extends RetryableStreamError {
1831
- constructor(limit) {
1832
- super(`Live stream buffer exceeded ${limit} events during catch-up`);
1833
- this.name = "LiveBufferOverflowError";
1834
- }
1835
- };
1836
- var CursorRejectedError = class extends Error {
1837
- constructor(cause) {
1838
- super("Server rejected resume cursor", { cause });
1839
- this.name = "CursorRejectedError";
1840
- }
1841
- };
1842
- var closeIterable = async (iterable) => {
1843
- if (!iterable) {
1844
- return;
1845
- }
1846
- await iterable.close?.();
1847
- };
1848
- var ignoreCleanupError = () => void 0;
1849
- var jitterDelay = (delayMs) => delayMs * (0.5 + Math.random() * 0.5);
1850
- var errorMessage = (error) => error instanceof Error ? error.message : String(error);
1851
- async function* throwOnCursorRejection(source, isCursorRejected) {
1852
- try {
1853
- yield* source;
1854
- } catch (error) {
1855
- throw isCursorRejected(error) ? new CursorRejectedError(error) : error;
1856
- }
1857
- }
1858
- var numericCursor = (cursor) => {
1859
- if (!cursor) {
1860
- return;
1861
- }
1862
- const value = Number(cursor);
1863
- return Number.isSafeInteger(value) && value >= 0 ? value : void 0;
1864
- };
1865
- var isCursorRegression = (next, current) => {
1866
- const nextValue = numericCursor(next);
1867
- const currentValue = numericCursor(current);
1868
- return nextValue !== void 0 && currentValue !== void 0 && nextValue < currentValue;
1869
- };
1870
- var resumableOrderedStream = (options) => stream((emit, end) => {
1871
- const catchUpPageSize = options.catchUpPageSize ?? CATCH_UP_PAGE_SIZE;
1872
- const bufferLimit = options.bufferLimit ?? MAX_BUFFERED_LIVE_EVENTS;
1873
- const initialRetryDelayMs = options.initialRetryDelayMs ?? RECONNECT_INITIAL_DELAY_MS;
1874
- const maxRetryDelayMs = options.maxRetryDelayMs ?? RECONNECT_MAX_DELAY_MS;
1875
- const jitter = options.jitter ?? jitterDelay;
1876
- const label = options.label;
1877
- let activeLive;
1878
- let closed = false;
1879
- let failedAttempts = 0;
1880
- let lastCursor;
1881
- let retryDelayMs = initialRetryDelayMs;
1882
- let sleepTimer;
1883
- let wakeSleep;
1884
- const deliveredSinceCursor = /* @__PURE__ */ new Set();
1885
- const noteRecovery = () => {
1886
- retryDelayMs = initialRetryDelayMs;
1887
- if (failedAttempts === 0) {
1888
- return;
1889
- }
1890
- log.info("stream recovered", { attempts: failedAttempts, label });
1891
- failedAttempts = 0;
1892
- };
1893
- const advanceCursor = (cursor, clearDelivered) => {
1894
- if (!cursor || cursor === lastCursor || isCursorRegression(cursor, lastCursor)) {
1895
- return;
1896
- }
1897
- lastCursor = cursor;
1898
- if (clearDelivered) {
1899
- deliveredSinceCursor.clear();
1900
- }
1901
- };
1902
- const deliverItem = async (item, resetRetry, clearOnCursorAdvance) => {
1903
- const alreadyDelivered = deliveredSinceCursor.has(item.id);
1904
- if (!alreadyDelivered) {
1905
- for (const value of item.values) {
1906
- await emit(value);
1907
- }
1908
- }
1909
- advanceCursor(item.cursor, clearOnCursorAdvance);
1910
- deliveredSinceCursor.add(item.id);
1911
- if (resetRetry) {
1912
- noteRecovery();
1913
- }
1914
- };
1915
- const isCursorRejected = (error) => options.isCursorRejectedError?.(error) === true;
1916
- const sleep2 = async (delayMs) => {
1917
- if (delayMs <= 0 || closed) {
1918
- return;
1919
- }
1920
- await new Promise((resolve) => {
1921
- wakeSleep = resolve;
1922
- sleepTimer = setTimeout(resolve, jitter(delayMs));
1923
- });
1924
- sleepTimer = void 0;
1925
- wakeSleep = void 0;
1926
- };
1927
- const cancelSleep = () => {
1928
- if (sleepTimer) {
1929
- clearTimeout(sleepTimer);
1930
- sleepTimer = void 0;
1931
- }
1932
- wakeSleep?.();
1933
- wakeSleep = void 0;
1934
- };
1935
- const nextRetryDelay = () => {
1936
- const delay = retryDelayMs;
1937
- retryDelayMs = Math.min(retryDelayMs * 2, maxRetryDelayMs);
1938
- return delay;
1939
- };
1940
- const handleFailure = (error) => {
1941
- failedAttempts += 1;
1942
- const delayMs = nextRetryDelay();
1943
- if (error instanceof CursorRejectedError) {
1944
- lastCursor = void 0;
1945
- deliveredSinceCursor.clear();
1946
- log.warn(
1947
- "resume cursor rejected; accepting event gap and resuming live",
1948
- {
1949
- attempt: failedAttempts,
1950
- delayMs,
1951
- error: errorMessage(error.cause),
1952
- label
1953
- }
1954
- );
1955
- return delayMs;
1956
- }
1957
- const attrs = {
1958
- attempt: failedAttempts,
1959
- delayMs,
1960
- error: errorMessage(error),
1961
- hasCursor: lastCursor !== void 0,
1962
- label
1963
- };
1964
- if (failedAttempts >= PERSISTENT_FAILURE_ERROR_THRESHOLD) {
1965
- log.error("stream persistently failing; still retrying", attrs, error);
1966
- return delayMs;
1967
- }
1968
- log.warn("stream interrupted; reconnecting", attrs);
1969
- return delayMs;
1970
- };
1971
- const consumeLive = async () => {
1972
- const live = options.subscribeLive(lastCursor);
1973
- activeLive = live;
1974
- try {
1975
- for await (const event of live) {
1976
- await deliverItem(await options.processLive(event), true, true);
1977
- }
1978
- throw new RetryableStreamError("Live stream ended");
1979
- } finally {
1980
- if (activeLive === live) {
1981
- activeLive = void 0;
1982
- }
1983
- await closeIterable(live);
1984
- }
1985
- };
1986
- const throwLiveError = (liveError) => {
1987
- if (liveError) {
1988
- throw liveError;
1989
- }
1990
- };
1991
- const bufferLiveEvent = (buffer, event) => {
1992
- if (buffer.length >= bufferLimit) {
1993
- throw new LiveBufferOverflowError(bufferLimit);
1994
- }
1995
- buffer.push(event);
1996
- };
1997
- const startLivePump = (live, isBuffering, liveBuffer) => {
1998
- let liveError;
1999
- const pump2 = (async () => {
2000
- try {
2001
- for await (const event of live) {
2002
- if (isBuffering()) {
2003
- bufferLiveEvent(liveBuffer, event);
2004
- continue;
2005
- }
2006
- await deliverItem(await options.processLive(event), true, true);
2007
- }
2008
- throw new RetryableStreamError("Live stream ended");
2009
- } catch (error) {
2010
- liveError = error;
2011
- }
2012
- })();
2013
- return {
2014
- getError: () => liveError,
2015
- pump: pump2
2016
- };
2017
- };
2018
- const replayMissed = async (cursor, getLiveError) => {
2019
- const missed = throwOnCursorRejection(
2020
- options.fetchMissed(cursor, { limit: catchUpPageSize }),
2021
- isCursorRejected
2022
- );
2023
- for await (const event of missed) {
2024
- throwLiveError(getLiveError());
2025
- await deliverItem(await options.processMissed(event), false, false);
2026
- }
2027
- throwLiveError(getLiveError());
2028
- };
2029
- const flushLiveBuffer = async (liveBuffer, getLiveError, stopBuffering) => {
2030
- let index = 0;
2031
- let lastFlushedId;
2032
- while (index < liveBuffer.length) {
2033
- throwLiveError(getLiveError());
2034
- const event = liveBuffer[index];
2035
- if (event === void 0) {
2036
- throw new RetryableStreamError("Live stream buffer index missing");
2037
- }
2038
- const item = await options.processLive(event);
2039
- await deliverItem(item, true, false);
2040
- lastFlushedId = item.id;
2041
- index += 1;
2042
- }
2043
- liveBuffer.length = 0;
2044
- throwLiveError(getLiveError());
2045
- compactDeliveredIds(lastFlushedId);
2046
- stopBuffering();
2047
- };
2048
- const compactDeliveredIds = (lastId) => {
2049
- if (!lastId) {
2050
- return;
2051
- }
2052
- deliveredSinceCursor.clear();
2053
- deliveredSinceCursor.add(lastId);
2054
- };
2055
- const catchUpThenConsumeLive = async (cursor) => {
2056
- const live = options.subscribeLive(cursor);
2057
- activeLive = live;
2058
- let buffering = true;
2059
- const liveBuffer = [];
2060
- const livePump = startLivePump(live, () => buffering, liveBuffer);
2061
- try {
2062
- await replayMissed(cursor, livePump.getError);
2063
- await flushLiveBuffer(liveBuffer, livePump.getError, () => {
2064
- buffering = false;
2065
- });
2066
- noteRecovery();
2067
- await livePump.pump;
2068
- throwLiveError(livePump.getError());
2069
- } finally {
2070
- buffering = false;
2071
- if (activeLive === live) {
2072
- activeLive = void 0;
2073
- }
2074
- await closeIterable(live);
2075
- await livePump.pump.catch(ignoreCleanupError);
2076
- }
2077
- };
2078
- const run = async () => {
2079
- while (!closed) {
2080
- try {
2081
- if (lastCursor) {
2082
- await catchUpThenConsumeLive(lastCursor);
2083
- } else {
2084
- await consumeLive();
2085
- }
2086
- } catch (error) {
2087
- await closeIterable(activeLive).catch(ignoreCleanupError);
2088
- activeLive = void 0;
2089
- if (closed) {
2090
- break;
2091
- }
2092
- await sleep2(handleFailure(error));
2093
- }
2094
- }
2095
- end();
2096
- };
2097
- const pump = run().catch((error) => {
2098
- log.error("resumable stream loop crashed", { label }, error);
2099
- if (!closed) {
2100
- end(error);
2101
- }
2102
- });
2103
- return async () => {
2104
- closed = true;
2105
- cancelSleep();
2106
- await closeIterable(activeLive);
2107
- await pump.catch(ignoreCleanupError);
2108
- };
2109
- });
2110
-
2111
- // src/providers/imessage/remote/contact-share.ts
2112
- import { sanitizeErrorMessage } from "@photon-ai/otel";
2113
- import { LRUCache } from "lru-cache";
2114
- var SHARE_TTL_MS = 24 * 60 * 60 * 1e3;
2115
- var MAX_TRACKED_CHATS = 1e4;
2116
- var ContactShareTracker = class {
2117
- cache = new LRUCache({
2118
- max: MAX_TRACKED_CHATS,
2119
- ttl: SHARE_TTL_MS,
2120
- ttlAutopurge: false
2121
- });
2122
- /**
2123
- * Best-effort share. The cache is set eagerly so that a burst of inbound
2124
- * messages for the same chat coalesces to a single API call. On failure the
2125
- * entry is evicted so the next inbound retries — transient errors don't
2126
- * permanently mute the feature for a chat. Never awaits and never throws:
2127
- * the receive stream must not crash on share failures.
2128
- */
2129
- maybeShare(client, chatGuid) {
2130
- if (this.cache.has(chatGuid)) {
2131
- return;
2132
- }
2133
- this.cache.set(chatGuid, true);
2134
- const safeChatGuid = sanitizeErrorMessage(chatGuid);
2135
- client.chats.shareContactInfo(chatGuid).then(() => {
2136
- console.info(
2137
- `[spectrum-ts][imessage][contact-share] shared contact info to ${safeChatGuid}`
2138
- );
2139
- }).catch((error) => {
2140
- this.cache.delete(chatGuid);
2141
- console.warn(
2142
- `[spectrum-ts][imessage][contact-share] failed to share contact info to ${safeChatGuid}`,
2143
- error
2144
- );
2145
- });
2146
- }
2147
- };
2148
- var trackers = /* @__PURE__ */ new WeakMap();
2149
- var getContactShareTracker = (owner) => {
2150
- let tracker = trackers.get(owner);
2151
- if (!tracker) {
2152
- tracker = new ContactShareTracker();
2153
- trackers.set(owner, tracker);
2154
- }
2155
- return tracker;
2156
- };
2157
-
2158
- // src/providers/imessage/remote/polls.ts
2159
- var isVotedPollEvent = (event) => event.delta.type === "voted";
2160
- var isUnvotedPollEvent = (event) => event.delta.type === "unvoted";
2161
- var toCachedPoll = (input) => {
2162
- const poll = asPoll({
2163
- title: input.title,
2164
- options: input.options.map((optionInfo) => ({
2165
- title: optionInfo.text
2166
- }))
2167
- });
2168
- const optionsByIdentifier = /* @__PURE__ */ new Map();
2169
- for (const [index, optionInfo] of input.options.entries()) {
2170
- const option = poll.options[index];
2171
- if (option && optionInfo.optionIdentifier) {
2172
- optionsByIdentifier.set(optionInfo.optionIdentifier, option);
2173
- }
2174
- }
2175
- return { poll, optionsByIdentifier };
2176
- };
2177
- var cachePollInfo = (cache, info) => {
2178
- const cached = toCachedPoll(info);
2179
- cache.set(info.pollMessageGuid, cached);
2180
- return cached;
2181
- };
2182
- var cachePollEvent = (cache, event) => {
2183
- if (event.delta.type === "created" || event.delta.type === "optionAdded") {
2184
- try {
2185
- const cached = toCachedPoll({
2186
- title: event.delta.title,
2187
- options: event.delta.options
2188
- });
2189
- cache.set(event.pollMessageGuid, cached);
2190
- return cached;
2191
- } catch (e) {
2192
- console.error("[spectrum-ts][imessage][poll] failed to cache poll", e);
2193
- }
2194
- }
2195
- };
2196
- var fetchPollInfo = async (client, cache, event) => {
2197
- try {
2198
- const info = await client.polls.get(event.pollMessageGuid);
2199
- cachePollInfo(cache, info);
2200
- return info;
2201
- } catch (e) {
2202
- console.error("[spectrum-ts][imessage][poll] failed to fetch poll", e);
2203
- return;
2204
- }
2205
- };
2206
- var resolvePoll = async (client, cache, event) => {
2207
- const cached = cache.get(event.pollMessageGuid);
2208
- if (cached) {
2209
- return cached;
2210
- }
2211
- try {
2212
- const info = await client.polls.get(event.pollMessageGuid);
2213
- return cachePollInfo(cache, info);
2214
- } catch (e) {
2215
- console.error("[spectrum-ts][imessage][poll] failed to resolve poll", e);
2216
- return;
2217
- }
2218
- };
2219
- var buildPollOptionMessage = (input) => {
2220
- const option = input.cached.optionsByIdentifier.get(input.optionId);
2221
- if (!option) {
2222
- return;
2223
- }
2224
- const action = input.selected ? "selected" : "deselected";
2225
- const eventTime = input.event.occurredAt.getTime();
2226
- return {
2227
- id: `${input.event.pollMessageGuid}:${input.senderAddress}:${input.optionId}:${action}:${eventTime}`,
2228
- sender: { id: input.senderAddress },
2229
- space: {
2230
- id: input.chatGuid,
2231
- type: chatTypeFromGuid(input.chatGuid),
2232
- phone: input.phone
2233
- },
2234
- timestamp: input.event.occurredAt,
2235
- content: asPollOption({
2236
- option,
2237
- poll: input.cached.poll,
2238
- selected: input.selected
2239
- })
2240
- };
2241
- };
2242
- var refreshPollMetadata = async (client, pollCache, event) => {
2243
- const info = await fetchPollInfo(client, pollCache, event);
2244
- if (!info) {
2245
- return;
2246
- }
2247
- return pollCache.get(info.pollMessageGuid);
2248
- };
2249
- var toPollOptionMessage = async (client, pollCache, event, phone) => {
2250
- const senderAddress = event.actor?.address;
2251
- const optionId = event.delta.optionIdentifier;
2252
- if (!(senderAddress && optionId)) {
2253
- return [];
2254
- }
2255
- let cached = await resolvePoll(client, pollCache, event);
2256
- if (!cached) {
2257
- return [];
2258
- }
2259
- if (!cached.optionsByIdentifier.has(optionId)) {
2260
- const refreshed = await refreshPollMetadata(client, pollCache, event);
2261
- if (refreshed) {
2262
- cached = refreshed;
2263
- }
2264
- }
2265
- const message = buildPollOptionMessage({
2266
- cached,
2267
- chatGuid: event.chatGuid,
2268
- event,
2269
- optionId,
2270
- phone,
2271
- selected: event.delta.type === "voted",
2272
- senderAddress
2273
- });
2274
- return message ? [message] : [];
2275
- };
2276
- var toPollDeltaMessages = async (client, pollCache, event, phone) => {
2277
- if (isVotedPollEvent(event)) {
2278
- return toPollOptionMessage(client, pollCache, event, phone);
2279
- }
2280
- if (isUnvotedPollEvent(event)) {
2281
- return toPollOptionMessage(client, pollCache, event, phone);
2282
- }
2283
- return [];
2284
- };
2285
-
2286
- // src/providers/imessage/remote/stream.ts
2287
- var isCursorRejectedIMessageError = (error) => error instanceof ValidationError;
2288
- var streamLabel = (kind, phone) => `imessage.${kind}:${phone === SHARED_PHONE ? phone : sanitizePhone(phone)}`;
2289
- var isEventFromCurrentAccount = (event, phone) => event.isFromMe || phone !== SHARED_PHONE && event.actor?.address !== void 0 && event.actor.address === phone;
2290
- var toMessageItem = async (client, event, phone, cursor, onInbound) => {
2291
- if (event.type === "message.received") {
2292
- if (event.message.isFromMe) {
2293
- return { cursor, id: event.message.guid, values: [] };
2294
- }
2295
- const inboundChatGuid = event.message.chatGuids?.[0];
2296
- if (inboundChatGuid) {
2297
- onInbound?.(inboundChatGuid);
2298
- }
2299
- const cache = getMessageCache(client);
2300
- return {
2301
- cursor,
2302
- id: event.message.guid,
2303
- values: await toInboundMessages(client, cache, event, phone)
2304
- };
2305
- }
2306
- if (event.type === "message.reactionAdded") {
2307
- if (isEventFromCurrentAccount(event, phone)) {
2308
- return {
2309
- cursor,
2310
- id: `${event.messageGuid}:reaction:${event.sequence}`,
2311
- values: []
2312
- };
2313
- }
2314
- const cache = getMessageCache(client);
2315
- return {
2316
- cursor,
2317
- id: `${event.messageGuid}:reaction:${event.sequence}`,
2318
- values: await toReactionMessages(client, cache, event, phone)
2319
- };
2320
- }
2321
- return {
2322
- cursor,
2323
- id: `${event.type}:${"messageGuid" in event ? event.messageGuid : "unknown"}:${event.sequence}`,
2324
- values: []
2325
- };
2326
- };
2327
- var toPollItem = async (client, pollCache, event, phone, cursor) => {
2328
- cachePollEvent(pollCache, event);
2329
- if (isEventFromCurrentAccount(event, phone)) {
2330
- return {
2331
- cursor,
2332
- id: `${event.pollMessageGuid}:poll:${event.sequence}`,
2333
- values: []
2334
- };
2335
- }
2336
- return {
2337
- cursor,
2338
- id: `${event.pollMessageGuid}:poll:${event.sequence}`,
2339
- values: await toPollDeltaMessages(client, pollCache, event, phone)
2340
- };
2341
- };
2342
- var toCatchUpCompleteItem = (event) => ({
2343
- cursor: String(event.headSequence),
2344
- id: `${event.type}:${event.headSequence}`,
2345
- values: []
2346
- });
2347
- var isMessageEvent = (event) => event.type.startsWith("message.");
2348
- var isPollEvent = (event) => event.type === "poll.changed";
2349
- async function* catchUpEvents(client, cursor, isWanted) {
2350
- const since = toResumeAfter(cursor);
2351
- if (since === void 0) {
2352
- return;
2353
- }
2354
- for await (const event of client.events.catchUp(since)) {
2355
- if (event.type === "catchup.complete") {
2356
- yield event;
2357
- return;
2358
- }
2359
- if (isWanted(event)) {
2360
- yield event;
2361
- }
2362
- }
2363
- }
2364
- var toResumeAfter = (cursor) => {
2365
- if (!cursor) {
2366
- return;
2367
- }
2368
- const sequence = Number(cursor);
2369
- return Number.isSafeInteger(sequence) && sequence >= 0 ? sequence : void 0;
2370
- };
2371
- async function* afterCursor(stream2, cursor) {
2372
- const resumeAfter = toResumeAfter(cursor);
2373
- try {
2374
- for await (const event of stream2) {
2375
- if (resumeAfter !== void 0 && event.sequence <= resumeAfter) {
2376
- continue;
2377
- }
2378
- yield event;
2379
- }
2380
- } finally {
2381
- await stream2.close?.();
2382
- }
2383
- }
2384
- var withClose = (source, cursor) => Object.assign(afterCursor(source, cursor), {
2385
- close: async () => {
2386
- await source.close?.();
2387
- }
2388
- });
2389
- var messageStream = (client, phone, onInbound) => resumableOrderedStream({
2390
- fetchMissed: (cursor) => catchUpEvents(client, cursor, isMessageEvent),
2391
- isCursorRejectedError: isCursorRejectedIMessageError,
2392
- label: streamLabel("messages", phone),
2393
- processLive: (event) => toMessageItem(client, event, phone, String(event.sequence), onInbound),
2394
- processMissed: (event) => event.type === "catchup.complete" ? Promise.resolve(toCatchUpCompleteItem(event)) : toMessageItem(
2395
- client,
2396
- event,
2397
- phone,
2398
- String(event.sequence),
2399
- onInbound
2400
- ),
2401
- subscribeLive: (cursor) => withClose(client.messages.subscribeEvents(), cursor)
2402
- });
2403
- var pollStream = (client, pollCache, phone) => resumableOrderedStream({
2404
- fetchMissed: (cursor) => catchUpEvents(client, cursor, isPollEvent),
2405
- isCursorRejectedError: isCursorRejectedIMessageError,
2406
- label: streamLabel("polls", phone),
2407
- processLive: (event) => toPollItem(client, pollCache, event, phone, String(event.sequence)),
2408
- processMissed: (event) => event.type === "catchup.complete" ? Promise.resolve(toCatchUpCompleteItem(event)) : toPollItem(client, pollCache, event, phone, String(event.sequence)),
2409
- subscribeLive: (cursor) => withClose(client.polls.subscribeEvents(), cursor)
2410
- });
2411
- var clientStream = (client, pollCache, phone, onInbound) => mergeStreams([
2412
- messageStream(client, phone, onInbound),
2413
- pollStream(client, pollCache, phone)
2414
- ]);
2415
- var messages3 = (clients, projectConfig) => {
2416
- const pollCache = getPollCache(clients);
2417
- const shareEnabled = projectConfig?.profile?.imessageSynced === true;
2418
- const tracker = shareEnabled ? getContactShareTracker(clients) : void 0;
2419
- return mergeStreams(
2420
- clients.map(
2421
- (entry) => clientStream(
2422
- entry.client,
2423
- pollCache,
2424
- entry.phone,
2425
- tracker ? (chatGuid) => tracker.maybeShare(entry.client, chatGuid) : void 0
2426
- )
2427
- )
2428
- );
2429
- };
2430
-
2431
- // src/providers/imessage/remote/stream-text.ts
2432
- var INITIAL_THROTTLE_MS = 1e3;
2433
- var BACKOFF_FACTOR = 2;
2434
- var MAX_EDITS = 5;
2435
- var sendStreamText = async (remote, spaceId, content) => {
2436
- if (content.format === "markdown") {
2437
- throw unsupportedRemoteContent(
2438
- "streamText",
2439
- "markdown-formatted streams have no native iMessage delivery"
2440
- );
2441
- }
2442
- const chat = toChatGuid(spaceId);
2443
- let sent;
2444
- let full = "";
2445
- let lastSentText = "";
2446
- let lastEditAt = 0;
2447
- let editCount = 0;
2448
- const flushEdit = async (text2) => {
2449
- if (!sent || text2 === lastSentText) {
2450
- return;
2451
- }
2452
- await remote.messages.edit(chat, toMessageGuid(sent.guid), text2);
2453
- lastSentText = text2;
2454
- lastEditAt = Date.now();
2455
- editCount += 1;
2456
- };
2457
- for await (const delta of content.stream()) {
2458
- full += delta;
2459
- if (!sent) {
2460
- sent = await remote.messages.sendText(chat, full);
2461
- lastSentText = full;
2462
- lastEditAt = Date.now();
2463
- continue;
2464
- }
2465
- const hasBudgetForInterimEdit = editCount < MAX_EDITS - 1;
2466
- const requiredGap = INITIAL_THROTTLE_MS * BACKOFF_FACTOR ** editCount;
2467
- if (hasBudgetForInterimEdit && Date.now() - lastEditAt >= requiredGap) {
2468
- await flushEdit(full);
2469
- }
2470
- }
2471
- if (!sent) {
2472
- throw unsupportedRemoteContent(
2473
- "streamText",
2474
- "stream produced no text \u2014 nothing to send"
2475
- );
2476
- }
2477
- await flushEdit(full);
2478
- return {
2479
- id: sent.guid,
2480
- content: asText(full),
2481
- direction: "outbound",
2482
- space: { id: spaceId },
2483
- timestamp: sent.dateCreated
2484
- };
2485
- };
2486
-
2487
- // src/providers/imessage/remote/typing.ts
2488
- var startTyping = async (remote, spaceId) => {
2489
- await remote.chats.setTyping(toChatGuid(spaceId), true);
2490
- };
2491
- var stopTyping = async (remote, spaceId) => {
2492
- await remote.chats.setTyping(toChatGuid(spaceId), false);
2493
- };
2494
-
2495
- // src/providers/imessage/remote/api.ts
2496
- var messages4 = (clients, projectConfig) => messages3(clients, projectConfig);
2497
- var setBackground2 = async (remote, spaceId, content) => setBackground(remote, spaceId, content);
2498
- var sendCustomizedMiniApp2 = async (remote, spaceId, content) => sendCustomizedMiniApp(remote, spaceId, content);
2499
- var setDisplayName2 = async (remote, spaceId, content) => setDisplayName(remote, spaceId, content);
2500
- var setIcon2 = async (remote, spaceId, content) => setIcon(remote, spaceId, content);
2501
- var markRead2 = async (remote, spaceId) => {
2502
- await markRead(remote, spaceId);
2503
- };
2504
- var startTyping2 = async (remote, spaceId) => {
2505
- await startTyping(remote, spaceId);
2506
- };
2507
- var stopTyping2 = async (remote, spaceId) => {
2508
- await stopTyping(remote, spaceId);
2509
- };
2510
- var send4 = async (remote, spaceId, content) => send3(remote, spaceId, content);
2511
- var sendStreamText2 = async (remote, spaceId, content) => sendStreamText(remote, spaceId, content);
2512
- var replyToMessage2 = async (remote, spaceId, msgId, content) => replyToMessage(remote, spaceId, msgId, content);
2513
- var editMessage2 = async (remote, spaceId, msgId, content) => editMessage(remote, spaceId, msgId, content);
2514
- var reactToMessage2 = async (remote, spaceId, target, reaction) => reactToMessage(remote, spaceId, target, reaction);
2515
- var unsendMessage2 = async (remote, spaceId, msgId) => unsendMessage(remote, spaceId, msgId);
2516
- var unsendReaction2 = async (remote, spaceId, target, reaction) => unsendReaction(remote, spaceId, target, reaction);
2517
- var getMessage4 = async (remote, spaceId, msgId, phone) => getMessage3(remote, spaceId, msgId, phone);
2518
-
2519
- // src/providers/imessage/remote/client.ts
2520
- var isSharedMode = (clients) => clients.length === 1 && clients[0]?.phone === SHARED_PHONE;
2521
- var availablePhones = (clients) => clients.map((c) => c.phone);
2522
- var clientForPhone = (clients, phone) => {
2523
- if (isSharedMode(clients)) {
2524
- const entry2 = clients[0];
2525
- if (!entry2) {
2526
- throw new Error("No iMessage clients configured");
2527
- }
2528
- return entry2.client;
2529
- }
2530
- const entry = clients.find((c) => c.phone === phone);
2531
- if (!entry) {
2532
- const list = availablePhones(clients).join(", ") || "<none>";
2533
- throw new Error(
2534
- `No iMessage client serves phone ${phone}. Available: ${list}`
2535
- );
2536
- }
2537
- return entry.client;
2538
- };
2539
- var randomPhone = (clients) => {
2540
- if (clients.length === 0) {
2541
- throw new Error("No iMessage phones configured for this account");
2542
- }
2543
- if (isSharedMode(clients)) {
2544
- return SHARED_PHONE;
2545
- }
2546
- const entry = clients[Math.floor(Math.random() * clients.length)];
2547
- if (!entry) {
2548
- throw new Error("No iMessage phones configured for this account");
2549
- }
2550
- return entry.phone;
2551
- };
2552
-
2553
- // src/providers/imessage/index.ts
2554
- var isPollContent = (content) => content.type === "poll" || content.type === "poll_option";
2555
- var cacheRemoteOutbound = (remote, space, record) => {
2556
- if (!record) {
2557
- return record;
2558
- }
2559
- cacheMessage(getMessageCache(remote), {
2560
- ...record,
2561
- direction: record.direction ?? "outbound",
2562
- space: {
2563
- ...record.space,
2564
- id: record.space.id,
2565
- phone: space.phone,
2566
- type: space.type
2567
- }
2568
- });
2569
- return record;
2570
- };
2571
- var handleEdit = async (client, space, content) => {
2572
- if (isLocal(client)) {
2573
- throw UnsupportedError.action("edit", "iMessage (local mode)");
2574
- }
2575
- if (content.content.type !== "text") {
2576
- throw UnsupportedError.content(
2577
- "edit",
2578
- "iMessage",
2579
- `only text content can be edited (got "${content.content.type}")`
2580
- );
2581
- }
2582
- const remote = clientForPhone(client, space.phone);
2583
- await editMessage2(remote, space.id, content.target.id, content.content);
2584
- };
2585
- var handleUnsend = async (client, space, content) => {
2586
- if (isLocal(client)) {
2587
- throw UnsupportedError.action("unsend", "iMessage (local mode)");
2588
- }
2589
- if (isPollContent(content.target.content)) {
2590
- throw UnsupportedError.action(
2591
- "unsend",
2592
- "iMessage",
2593
- "iMessage polls cannot be unsent"
2594
- );
2595
- }
2596
- const remote = clientForPhone(client, space.phone);
2597
- const targetContent = content.target.content;
2598
- if (targetContent.type === "reaction") {
2599
- await unsendReaction2(
2600
- remote,
2601
- space.id,
2602
- targetContent.target,
2603
- targetContent.emoji
2604
- );
2605
- return;
2606
- }
2607
- await unsendMessage2(remote, space.id, content.target.id);
2608
- };
2609
- var handleStreamText = async (client, space, content) => {
2610
- if (isLocal(client)) {
2611
- throw UnsupportedError.action(
2612
- "streamText",
2613
- "iMessage (local mode)",
2614
- "streaming text responses require remote iMessage"
2615
- );
2616
- }
2617
- const remote = clientForPhone(client, space.phone);
2618
- return cacheRemoteOutbound(
2619
- remote,
2620
- space,
2621
- await sendStreamText2(remote, space.id, content)
2622
- );
2623
- };
2624
- var handleBackground = async (client, space, content) => {
2625
- if (isLocal(client)) {
2626
- throw UnsupportedError.action(
2627
- "background",
2628
- "iMessage (local mode)",
2629
- "chat backgrounds require remote iMessage"
2630
- );
2631
- }
2632
- const remote = clientForPhone(client, space.phone);
2633
- await setBackground2(remote, space.id, content);
2634
- };
2635
- var handleCustomizedMiniApp = async (client, space, content) => {
2636
- if (isLocal(client)) {
2637
- throw UnsupportedError.action(
2638
- "customized-mini-app",
2639
- "iMessage (local mode)",
2640
- "mini app cards require remote iMessage"
2641
- );
2642
- }
2643
- const remote = clientForPhone(client, space.phone);
2644
- return cacheRemoteOutbound(
2645
- remote,
2646
- space,
2647
- await sendCustomizedMiniApp2(remote, space.id, content)
2648
- );
2649
- };
2650
- var handleRead = async (client, space) => {
2651
- if (isLocal(client)) {
2652
- throw UnsupportedError.action(
2653
- "read",
2654
- "iMessage (local mode)",
2655
- "marking chats as read requires remote iMessage"
2656
- );
2657
- }
2658
- const remote = clientForPhone(client, space.phone);
2659
- await markRead2(remote, space.id);
2660
- };
2661
- var handleTyping = async (client, space, state) => {
2662
- if (isLocal(client)) {
2663
- return;
2664
- }
2665
- const remote = clientForPhone(client, space.phone);
2666
- if (state === "start") {
2667
- await startTyping2(remote, space.id);
2668
- } else {
2669
- await stopTyping2(remote, space.id);
2670
- }
2671
- };
2672
- var handleRename = async (client, space, content) => {
2673
- if (isLocal(client)) {
2674
- throw UnsupportedError.action(
2675
- "rename",
2676
- "iMessage (local mode)",
2677
- "renaming chats requires remote iMessage"
2678
- );
2679
- }
2680
- if (space.type !== "group") {
2681
- throw UnsupportedError.action(
2682
- "rename",
2683
- "iMessage",
2684
- "only group chats can be renamed (this space is a DM)"
2685
- );
2686
- }
2687
- const remote = clientForPhone(client, space.phone);
2688
- await setDisplayName2(remote, space.id, content);
2689
- };
2690
- var handleAvatar = async (client, space, content) => {
2691
- if (isLocal(client)) {
2692
- throw UnsupportedError.action(
2693
- "avatar",
2694
- "iMessage (local mode)",
2695
- "setting group avatars requires remote iMessage"
2696
- );
2697
- }
2698
- if (space.type !== "group") {
2699
- throw UnsupportedError.action(
2700
- "avatar",
2701
- "iMessage",
2702
- "only group chats have avatars (this space is a DM)"
2703
- );
2704
- }
2705
- const remote = clientForPhone(client, space.phone);
2706
- await setIcon2(remote, space.id, content);
2707
- };
2708
- var imessage = definePlatform("iMessage", {
2709
- config: configSchema,
2710
- static: {
2711
- effect: {
2712
- message: MessageEffect2
2713
- }
2714
- },
2715
- lifecycle: {
2716
- createClient: async ({
2717
- config,
2718
- projectId,
2719
- projectSecret
2720
- }) => {
2721
- if (config.local) {
2722
- return new IMessageSDK2();
2723
- }
2724
- if (config.clients) {
2725
- const entries = Array.isArray(config.clients) ? config.clients : [config.clients];
2726
- return entries.map((e) => ({
2727
- phone: e.phone,
2728
- client: createClient2({
2729
- address: e.address,
2730
- tls: true,
2731
- token: e.token
2732
- })
2733
- }));
2734
- }
2735
- if (!(projectId && projectSecret)) {
2736
- throw new Error(
2737
- "iMessage requires projectId and projectSecret. Either pass credentials to Spectrum(), use local mode: imessage.config({ local: true }), or provide explicit client config: imessage.config({ clients: [...] })"
2738
- );
2739
- }
2740
- return await createCloudClients(projectId, projectSecret);
2741
- },
2742
- destroyClient: async ({ client }) => {
2743
- if (isLocal(client)) {
2744
- await client.close();
2745
- return;
2746
- }
2747
- await disposeCloudAuth(client);
2748
- await Promise.all(client.map((entry) => entry.client.close()));
2749
- }
2750
- },
2751
- user: {
2752
- resolve: async ({ input }) => ({ id: input.userID })
2753
- },
2754
- space: {
2755
- schema: spaceSchema,
2756
- params: spaceParamsSchema,
2757
- create: async ({ input, client }) => {
2758
- if (isLocal(client)) {
2759
- throw UnsupportedError.action(
2760
- "space.create",
2761
- "iMessage (local mode)",
2762
- "local mode only supports replying to existing messages"
2763
- );
2764
- }
2765
- if (input.users.length === 0) {
2766
- throw new Error("iMessage space creation requires at least one user");
2767
- }
2768
- if (client.length === 0) {
2769
- throw new Error("No iMessage clients configured");
2770
- }
2771
- const addresses = input.users.map((u) => u.id);
2772
- if (isSharedMode(client)) {
2773
- if (addresses.length > 1) {
2774
- throw UnsupportedError.action(
2775
- "space.create",
2776
- "iMessage (shared mode)",
2777
- "shared mode cannot create group chats \u2014 use a dedicated number, or space.get(chatGuid) for an existing group"
2778
- );
2779
- }
2780
- return {
2781
- id: dmChatGuid(addresses[0] ?? ""),
2782
- type: "dm",
2783
- phone: SHARED_PHONE
2784
- };
2785
- }
2786
- const phone = input.params?.phone ?? randomPhone(client);
2787
- const remote = clientForPhone(client, phone);
2788
- const { chat } = await remote.chats.create(addresses);
2789
- return {
2790
- id: chat.guid,
2791
- type: chat.isGroup ? "group" : "dm",
2792
- phone
2793
- };
2794
- },
2795
- get: async ({ input, client }) => {
2796
- if (isLocal(client)) {
2797
- throw UnsupportedError.action(
2798
- "space.get",
2799
- "iMessage (local mode)",
2800
- "local mode only supports replying to existing messages"
2801
- );
2802
- }
2803
- if (client.length === 0) {
2804
- throw new Error("No iMessage clients configured");
2805
- }
2806
- const phone = isSharedMode(client) ? SHARED_PHONE : input.params?.phone ?? (client.length === 1 ? client[0]?.phone : void 0);
2807
- if (!phone) {
2808
- throw new Error(
2809
- `iMessage space.get requires params.phone when multiple clients are configured. Available: ${availablePhones(client).join(", ")}`
2810
- );
2811
- }
2812
- return {
2813
- id: input.id,
2814
- type: chatTypeFromGuid(input.id),
2815
- phone
2816
- };
2817
- },
2818
- actions: {
2819
- // Sugar: `space.background(input, opts?)` →
2820
- // `space.send(background(input, opts?))`. Routed through the universal
2821
- // send pipeline so the unsupported-content + warn-and-skip path on
2822
- // local-mode iMessage is identical to the canonical form.
2823
- background: async (space, input, opts) => {
2824
- await space.send(background(input, opts));
2825
- }
2826
- }
2827
- },
2828
- message: {
2829
- schema: messageSchema
2830
- },
2831
- messages: ({ client, projectConfig }) => isLocal(client) ? messages2(client) : messages4(client, projectConfig),
2832
- send: async ({ space, content, client }) => {
2833
- if (content.type === "reply") {
2834
- if (isLocal(client)) {
2835
- throw UnsupportedError.action("reply", "iMessage (local mode)");
2836
- }
2837
- if (isPollContent(content.target.content)) {
2838
- throw UnsupportedError.action(
2839
- "reply",
2840
- "iMessage",
2841
- "iMessage polls do not support replies"
2842
- );
2843
- }
2844
- const remote2 = clientForPhone(client, space.phone);
2845
- return cacheRemoteOutbound(
2846
- remote2,
2847
- space,
2848
- await replyToMessage2(
2849
- remote2,
2850
- space.id,
2851
- content.target.id,
2852
- content.content
2853
- )
2854
- );
2855
- }
2856
- if (content.type === "reaction") {
2857
- if (isLocal(client)) {
2858
- throw UnsupportedError.action("react", "iMessage (local mode)");
2859
- }
2860
- if (isPollContent(content.target.content)) {
2861
- throw UnsupportedError.action(
2862
- "react",
2863
- "iMessage",
2864
- "iMessage polls do not support reactions"
2865
- );
2866
- }
2867
- const remote2 = clientForPhone(client, space.phone);
2868
- return cacheRemoteOutbound(
2869
- remote2,
2870
- space,
2871
- await reactToMessage2(
2872
- remote2,
2873
- space.id,
2874
- content.target,
2875
- content.emoji
2876
- )
2877
- );
2878
- }
2879
- if (content.type === "typing") {
2880
- await handleTyping(client, space, content.state);
2881
- return;
2882
- }
2883
- if (content.type === "edit") {
2884
- await handleEdit(client, space, content);
2885
- return;
2886
- }
2887
- if (content.type === "unsend") {
2888
- await handleUnsend(client, space, content);
2889
- return;
2890
- }
2891
- if (content.type === "streamText") {
2892
- return await handleStreamText(client, space, content);
2893
- }
2894
- if (content.type === "rename") {
2895
- await handleRename(client, space, content);
2896
- return;
2897
- }
2898
- if (content.type === "avatar") {
2899
- await handleAvatar(client, space, content);
2900
- return;
2901
- }
2902
- if (content.type === "read") {
2903
- await handleRead(client, space);
2904
- return;
2905
- }
2906
- if (isBackground(content)) {
2907
- await handleBackground(client, space, content);
2908
- return;
2909
- }
2910
- if (isCustomizedMiniApp(content)) {
2911
- return await handleCustomizedMiniApp(client, space, content);
2912
- }
2913
- if (isLocal(client)) {
2914
- return await send2(client, space.id, content);
2915
- }
2916
- const remote = clientForPhone(client, space.phone);
2917
- return cacheRemoteOutbound(
2918
- remote,
2919
- space,
2920
- await send4(remote, space.id, content)
2921
- );
2922
- },
2923
- actions: {
2924
- getMessage: async ({ client }, space, messageId) => {
2925
- if (isLocal(client)) {
2926
- return getMessage2(client, messageId);
2927
- }
2928
- const remote = clientForPhone(client, space.phone);
2929
- return getMessage4(remote, space.id, messageId, space.phone);
2930
- },
2931
- // Fetch an attachment by GUID. Returns a spectrum `Attachment` whose
2932
- // `.read()` / `.stream()` lazily download the bytes — calling both
2933
- // issues two independent gRPC downloads, so cache `.read()` if you
2934
- // need the bytes more than once. Returns `undefined` for unknown
2935
- // GUIDs. Local-mode iMessage is not supported.
2936
- getAttachment: async ({ client }, guid, phone) => {
2937
- if (isLocal(client)) {
2938
- throw UnsupportedError.action(
2939
- "getAttachment",
2940
- "iMessage (local mode)",
2941
- "fetching attachments by GUID requires remote iMessage"
2942
- );
2943
- }
2944
- if (client.length === 0) {
2945
- throw new Error("No iMessage clients configured");
2946
- }
2947
- const routedPhone = (() => {
2948
- if (isSharedMode(client)) {
2949
- return SHARED_PHONE;
2950
- }
2951
- if (phone) {
2952
- return phone;
2953
- }
2954
- if (client.length === 1) {
2955
- return client[0].phone;
2956
- }
2957
- throw new Error(
2958
- `imessage.getAttachment requires a phone in multi-phone mode. Available: ${availablePhones(client).join(", ")}`
2959
- );
2960
- })();
2961
- const remote = clientForPhone(client, routedPhone);
2962
- return withSpan(
2963
- "spectrum.imessage.getAttachment",
2964
- {
2965
- "spectrum.provider": "iMessage",
2966
- "spectrum.imessage.attachment.guid": guid,
2967
- "spectrum.imessage.phone": routedPhone
2968
- },
2969
- () => getRemoteAttachment(remote, guid)
2970
- );
2971
- }
2972
- }
2973
- });
2974
-
2975
- export {
2976
- background,
2977
- customizedMiniApp,
2978
- effect,
2979
- imessage
2980
- };