spectrum-ts 1.1.0 → 1.1.2

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.
@@ -1,25 +1,26 @@
1
1
  import {
2
2
  asGroup,
3
- asRichlink
4
- } from "../../chunk-LAGNM6I7.js";
3
+ asRichlink,
4
+ groupSchema
5
+ } from "../../chunk-UWUPCIB4.js";
5
6
  import {
6
7
  asPoll,
7
8
  asPollOption,
8
9
  cloud,
9
10
  mergeStreams,
10
11
  stream
11
- } from "../../chunk-PXX7ISZ6.js";
12
+ } from "../../chunk-FF2R4EP3.js";
12
13
  import {
13
14
  UnsupportedError,
14
15
  asAttachment,
15
16
  asContact,
16
17
  asCustom,
17
- asReaction,
18
18
  asText,
19
19
  definePlatform,
20
20
  fromVCard,
21
+ reactionSchema,
21
22
  toVCard
22
- } from "../../chunk-XMAI2AAN.js";
23
+ } from "../../chunk-5GQ2OMFY.js";
23
24
 
24
25
  // src/providers/imessage/index.ts
25
26
  import { createClient as createClient2, directChat } from "@photon-ai/advanced-imessage";
@@ -111,17 +112,12 @@ async function disposeCloudAuth(clients) {
111
112
  }
112
113
  }
113
114
 
114
- // src/providers/imessage/local.ts
115
+ // src/providers/imessage/local/attachments.ts
115
116
  import { createReadStream } from "fs";
116
- import { mkdtemp, readFile, rm, writeFile } from "fs/promises";
117
- import { tmpdir } from "os";
118
- import { basename, join } from "path";
117
+ import { readFile } from "fs/promises";
119
118
  import { Readable } from "stream";
120
- var synthSendResult = () => ({
121
- id: crypto.randomUUID(),
122
- timestamp: /* @__PURE__ */ new Date()
123
- });
124
- var DEFAULT_ATTACHMENT_NAME = "attachment";
119
+
120
+ // src/providers/imessage/shared/vcard.ts
125
121
  var VCARD_MIME_TYPES = /* @__PURE__ */ new Set([
126
122
  "text/vcard",
127
123
  "text/x-vcard",
@@ -136,6 +132,13 @@ var isVCardAttachment = (mimeType, fileName) => {
136
132
  }
137
133
  return Boolean(fileName?.toLowerCase().endsWith(".vcf"));
138
134
  };
135
+ var vcardFileName = (contact) => {
136
+ const base = contact.name?.formatted ?? contact.user?.id ?? "contact";
137
+ return `${base.replace(/[^a-zA-Z0-9_\-.]/g, "_")}.vcf`;
138
+ };
139
+
140
+ // src/providers/imessage/local/attachments.ts
141
+ var DEFAULT_ATTACHMENT_NAME = "attachment";
139
142
  var readLocalAttachment = async (att) => {
140
143
  if (!att.localPath) {
141
144
  throw new Error(
@@ -164,6 +167,9 @@ var toVCardContent = async (att) => {
164
167
  return toAttachmentContent(att);
165
168
  }
166
169
  };
170
+ var localAttachmentContent = async (att) => isVCardAttachment(att.mimeType, att.fileName) ? await toVCardContent(att) : toAttachmentContent(att);
171
+
172
+ // src/providers/imessage/local/inbound.ts
167
173
  var toMessages = async (message) => {
168
174
  const { chatId, chatKind } = message;
169
175
  if (!chatId || chatKind === "unknown") {
@@ -182,7 +188,7 @@ var toMessages = async (message) => {
182
188
  message.attachments.map(async (att) => ({
183
189
  ...base,
184
190
  id: `${message.id}:${att.id}`,
185
- content: isVCardAttachment(att.mimeType, att.fileName) ? await toVCardContent(att) : toAttachmentContent(att)
191
+ content: await localAttachmentContent(att)
186
192
  }))
187
193
  );
188
194
  }
@@ -214,10 +220,25 @@ var messages = (client) => stream((emit, end) => {
214
220
  });
215
221
  };
216
222
  });
217
- var vcardFileName = (content) => {
218
- const base = content.name?.formatted ?? content.user?.id ?? "contact";
219
- return `${base.replace(/[^a-zA-Z0-9_\-.]/g, "_")}.vcf`;
220
- };
223
+
224
+ // src/providers/imessage/local/send.ts
225
+ import { mkdtemp, rm, writeFile } from "fs/promises";
226
+ import { tmpdir } from "os";
227
+ import { basename, join } from "path";
228
+
229
+ // src/providers/imessage/shared/errors.ts
230
+ var IMESSAGE_PLATFORM = "iMessage";
231
+ var LOCAL_IMESSAGE_PLATFORM = "iMessage (local mode)";
232
+ var unsupportedRemoteContent = (type, detail) => UnsupportedError.content(type, IMESSAGE_PLATFORM, detail);
233
+ var unsupportedLocalContent = (type) => UnsupportedError.content(type, LOCAL_IMESSAGE_PLATFORM);
234
+
235
+ // src/providers/imessage/local/send.ts
236
+ var synthRecord = (spaceId, content) => ({
237
+ id: crypto.randomUUID(),
238
+ content,
239
+ space: { id: spaceId },
240
+ timestamp: /* @__PURE__ */ new Date()
241
+ });
221
242
  var sendTempFile = async (client, spaceId, name, data) => {
222
243
  const safeName = basename(name) || DEFAULT_ATTACHMENT_NAME;
223
244
  const dir = await mkdtemp(join(tmpdir(), "spectrum-"));
@@ -234,10 +255,10 @@ var send = async (client, spaceId, content) => {
234
255
  switch (content.type) {
235
256
  case "text":
236
257
  await client.send({ to: spaceId, text: content.text });
237
- return synthSendResult();
258
+ return synthRecord(spaceId, content);
238
259
  case "attachment":
239
260
  await sendTempFile(client, spaceId, content.name, await content.read());
240
- return synthSendResult();
261
+ return synthRecord(spaceId, content);
241
262
  case "contact": {
242
263
  const vcf = await toVCard(content);
243
264
  await sendTempFile(
@@ -246,146 +267,38 @@ var send = async (client, spaceId, content) => {
246
267
  vcardFileName(content),
247
268
  Buffer.from(vcf, "utf8")
248
269
  );
249
- return synthSendResult();
270
+ return synthRecord(spaceId, content);
250
271
  }
251
272
  case "poll":
252
- throw UnsupportedError.content("poll", "iMessage (local mode)");
273
+ throw unsupportedLocalContent("poll");
253
274
  default:
254
- throw UnsupportedError.content(content.type, "iMessage (local mode)");
275
+ throw unsupportedLocalContent(content.type);
255
276
  }
256
277
  };
257
278
  var getMessage = async (_client, _id) => void 0;
258
279
 
259
- // src/providers/imessage/remote.ts
260
- import {
261
- chatGuid,
262
- messageGuid,
263
- Reaction
264
- } from "@photon-ai/advanced-imessage";
280
+ // src/providers/imessage/local/api.ts
281
+ var messages2 = (client) => messages(client);
282
+ var send2 = async (client, spaceId, content) => send(client, spaceId, content);
283
+ var getMessage2 = async (client, id) => getMessage(client, id);
265
284
 
266
- // src/utils/audio.ts
267
- import { spawn } from "child_process";
268
- import { mkdtemp as mkdtemp2, readFile as readFile2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
269
- import { tmpdir as tmpdir2 } from "os";
270
- import { join as join2 } from "path";
271
- var M4A_BRANDS = /* @__PURE__ */ new Set([
272
- "M4A ",
273
- "M4B ",
274
- "M4P ",
275
- "mp42",
276
- "mp41",
277
- "isom",
278
- "iso2"
279
- ]);
280
- var M4A_MIME_TYPES = /* @__PURE__ */ new Set([
281
- "audio/mp4",
282
- "audio/mp4a-latm",
283
- "audio/x-m4a",
284
- "audio/aac",
285
- "audio/aacp"
286
- ]);
287
- var FFMPEG_MISSING_MESSAGE = "voice content: input is not m4a/aac and ffmpeg is unavailable. Install `ffmpeg-static` or ensure `ffmpeg` is on PATH.";
288
- var isM4a = (buffer) => {
289
- if (buffer.length < 12) {
290
- return false;
291
- }
292
- if (buffer.toString("ascii", 4, 8) !== "ftyp") {
293
- return false;
294
- }
295
- return M4A_BRANDS.has(buffer.toString("ascii", 8, 12));
296
- };
297
- var isM4aMimeType = (mimeType) => M4A_MIME_TYPES.has(mimeType.toLowerCase());
298
- var cachedFfmpegPath;
299
- var tryStaticBinary = async () => {
300
- try {
301
- const mod = await import("ffmpeg-static");
302
- return mod.default ?? void 0;
303
- } catch {
304
- return void 0;
305
- }
306
- };
307
- var resolveFfmpegPath = async () => {
308
- if (cachedFfmpegPath) {
309
- return cachedFfmpegPath;
310
- }
311
- cachedFfmpegPath = await tryStaticBinary() ?? "ffmpeg";
312
- return cachedFfmpegPath;
313
- };
314
- var collectStream = (stream2) => {
315
- if (!stream2) {
316
- return Promise.resolve("");
317
- }
318
- return new Promise((resolve, reject) => {
319
- const chunks = [];
320
- stream2.on("data", (chunk) => {
321
- chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
322
- });
323
- stream2.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
324
- stream2.on("error", reject);
325
- });
326
- };
327
- var isMissingBinaryError = (err) => err?.code === "ENOENT";
328
- var runFfmpeg = (ffmpegPath, args) => {
329
- const proc = spawn(ffmpegPath, args, { stdio: ["ignore", "ignore", "pipe"] });
330
- const stderr = collectStream(proc.stderr);
331
- const exit = new Promise((resolve, reject) => {
332
- proc.on(
333
- "error",
334
- (err) => reject(
335
- isMissingBinaryError(err) ? new Error(FFMPEG_MISSING_MESSAGE) : err
336
- )
337
- );
338
- proc.on("exit", (code) => resolve(code ?? -1));
339
- });
340
- return Promise.all([exit, stderr]).then(([code, text]) => ({
341
- code,
342
- stderr: text
343
- }));
344
- };
345
- var DURATION_PATTERN = /Duration:\s*(\d+):(\d{2}):(\d{2})(?:\.(\d{1,3}))?/;
346
- var parseDuration = (stderr) => {
347
- const match = stderr.match(DURATION_PATTERN);
348
- if (!match) {
349
- return void 0;
350
- }
351
- const [, hh, mm, ss, frac] = match;
352
- const seconds = Number(hh) * 3600 + Number(mm) * 60 + Number(ss) + Number(`0.${frac ?? 0}`);
353
- return Number.isFinite(seconds) ? seconds : void 0;
354
- };
355
- var transcodeToM4a = async (buffer) => {
356
- const ffmpeg = await resolveFfmpegPath();
357
- const dir = await mkdtemp2(join2(tmpdir2(), "spectrum-voice-"));
358
- const inPath = join2(dir, "in");
359
- const outPath = join2(dir, "out.m4a");
360
- try {
361
- await writeFile2(inPath, buffer);
362
- const { code, stderr } = await runFfmpeg(ffmpeg, [
363
- "-y",
364
- "-i",
365
- inPath,
366
- "-f",
367
- "ipod",
368
- "-c:a",
369
- "aac",
370
- outPath
371
- ]);
372
- if (code !== 0) {
373
- throw new Error(`ffmpeg conversion failed (exit ${code}): ${stderr}`);
374
- }
375
- const out = await readFile2(outPath);
376
- return { buffer: out, duration: parseDuration(stderr) };
377
- } finally {
378
- await rm2(dir, { recursive: true, force: true }).catch(() => {
379
- });
380
- }
381
- };
382
- var ensureM4a = async (buffer, mimeType) => {
383
- if (isM4aMimeType(mimeType) || isM4a(buffer)) {
384
- return { buffer };
285
+ // src/providers/imessage/remote/client.ts
286
+ var REMOTE_CLIENT_MISSING = "No remote iMessage client available";
287
+ var firstRemoteClient = (clients) => clients[0];
288
+ var primaryRemoteClient = (clients) => {
289
+ const remote = firstRemoteClient(clients);
290
+ if (!remote) {
291
+ throw new Error(REMOTE_CLIENT_MISSING);
385
292
  }
386
- return transcodeToM4a(buffer);
293
+ return remote;
387
294
  };
388
295
 
296
+ // src/providers/imessage/remote/inbound.ts
297
+ import {
298
+ messageGuid,
299
+ NotFoundError
300
+ } from "@photon-ai/advanced-imessage";
301
+
389
302
  // src/providers/imessage/cache.ts
390
303
  var DEFAULT_MAX = 1e3;
391
304
  var MessageCache = class {
@@ -509,73 +422,28 @@ var getPollCache = (owner) => {
509
422
  return cache;
510
423
  };
511
424
 
512
- // src/providers/imessage/remote.ts
513
- var PLATFORM = "iMessage";
514
- var URL_BALLOON_BUNDLE_ID = "com.apple.messages.URLBalloonProvider";
515
- var GROUP_ITEM_ALLOWED = /* @__PURE__ */ new Set([
516
- "attachment",
517
- "contact",
518
- "voice"
519
- ]);
520
- var unsupportedContent = (type, detail) => UnsupportedError.content(type, PLATFORM, detail);
521
- var toSendResult = (receipt) => ({
522
- id: receipt.guid,
523
- timestamp: /* @__PURE__ */ new Date()
524
- });
525
- var VCARD_MIME_TYPES2 = /* @__PURE__ */ new Set([
526
- "text/vcard",
527
- "text/x-vcard",
528
- "text/directory",
529
- "application/vcard",
530
- "application/x-vcard"
531
- ]);
532
- var isVCardAttachment2 = (mimeType, fileName) => {
533
- if (mimeType && VCARD_MIME_TYPES2.has(mimeType.toLowerCase())) {
534
- return true;
535
- }
536
- return Boolean(fileName?.toLowerCase().endsWith(".vcf"));
537
- };
538
- var EMOJI_TO_TAPBACK = {
539
- "\u2764\uFE0F": Reaction.love,
540
- "\u{1F44D}": Reaction.like,
541
- "\u{1F44E}": Reaction.dislike,
542
- "\u{1F602}": Reaction.laugh,
543
- "\u203C\uFE0F": Reaction.emphasize,
544
- "\u2753": Reaction.question
545
- };
546
- var TAPBACK_TO_EMOJI = Object.fromEntries(
547
- Object.entries(EMOJI_TO_TAPBACK).map(([emoji, kind]) => [kind, emoji])
548
- );
549
- var TAPBACK_CODE_TO_KIND = {
550
- "2000": Reaction.love,
551
- "2001": Reaction.like,
552
- "2002": Reaction.dislike,
553
- "2003": Reaction.laugh,
554
- "2004": Reaction.emphasize,
555
- "2005": Reaction.question,
556
- "2006": Reaction.emoji,
557
- "2007": Reaction.sticker
425
+ // src/providers/imessage/remote/ids.ts
426
+ var PART_PREFIX = /^p:(\d+)\//;
427
+ var formatChildId = (partIndex, parentGuid) => `p:${partIndex}/${parentGuid}`;
428
+ var parseTapbackTarget = (target) => {
429
+ const match = target.match(PART_PREFIX);
430
+ const guid = target.replace(PART_PREFIX, "");
431
+ const partIndex = match ? Number(match[1]) : 0;
432
+ return { guid, partIndex };
558
433
  };
559
- var isTapbackRemoval = (code) => code.startsWith("3");
560
- var resolveReactionEmoji = (type, emoji) => {
561
- if (emoji) {
562
- return emoji;
563
- }
564
- if (!type) {
434
+ var parseChildId = (id) => {
435
+ const match = id.match(PART_PREFIX);
436
+ if (!match) {
565
437
  return null;
566
438
  }
567
- const kind = TAPBACK_CODE_TO_KIND[type] ?? type;
568
- return TAPBACK_TO_EMOJI[kind] ?? null;
569
- };
570
- var getAssociatedMessageType = (message) => {
571
- const direct = message.associatedMessageType;
572
- if (typeof direct === "string") {
573
- return direct;
574
- }
575
- const raw = message._raw;
576
- const fromRaw = raw?.associatedMessageType;
577
- return typeof fromRaw === "string" ? fromRaw : void 0;
439
+ return {
440
+ parentGuid: id.replace(PART_PREFIX, ""),
441
+ partIndex: Number(match[1])
442
+ };
578
443
  };
444
+
445
+ // src/providers/imessage/remote/inbound.ts
446
+ var URL_BALLOON_BUNDLE_ID = "com.apple.messages.URLBalloonProvider";
579
447
  var getBalloonBundleId = (message) => {
580
448
  const raw = message._raw;
581
449
  const id = raw?.balloonBundleId;
@@ -589,6 +457,31 @@ var resolveChatGuid = (message, hint) => {
589
457
  return first ?? "";
590
458
  };
591
459
  var resolveSenderId = (message) => message.sender?.address ?? "";
460
+ var isIMessageMessage = (value) => {
461
+ if (typeof value !== "object" || value === null) {
462
+ return false;
463
+ }
464
+ const record = value;
465
+ return typeof record.id === "string" && record.id.length > 0 && typeof record.content === "object" && record.content !== null && typeof record.sender === "object" && record.sender !== null && typeof record.space === "object" && record.space !== null;
466
+ };
467
+ var asProviderGroup = (items) => groupSchema.parse({ type: "group", items });
468
+ var buildMessageBase = (message, chatGuidHint, timestamp) => {
469
+ const chat = resolveChatGuid(message, chatGuidHint);
470
+ return {
471
+ sender: { id: resolveSenderId(message) },
472
+ space: {
473
+ id: chat,
474
+ type: chat.includes(";+;") ? "group" : "dm"
475
+ },
476
+ timestamp
477
+ };
478
+ };
479
+ var receivedEventFromMessage = (message) => ({
480
+ chatGuid: resolveChatGuid(message, void 0),
481
+ message,
482
+ timestamp: message.dateCreated ?? /* @__PURE__ */ new Date(),
483
+ type: "message.received"
484
+ });
592
485
  var toAttachmentContent2 = (client, info) => asAttachment({
593
486
  name: info.fileName,
594
487
  mimeType: info.mimeType,
@@ -600,22 +493,15 @@ var toVCardContent2 = async (client, info) => {
600
493
  try {
601
494
  const buf = Buffer.from(await client.attachments.downloadBuffer(info.guid));
602
495
  return asContact(fromVCard(buf.toString("utf8")));
603
- } catch {
496
+ } catch (err) {
497
+ console.warn(
498
+ "[spectrum-ts][imessage] failed to parse vCard attachment; falling back to attachment content",
499
+ { error: err, guid: info.guid }
500
+ );
604
501
  return toAttachmentContent2(client, info);
605
502
  }
606
503
  };
607
- var attachmentContent = async (client, info) => isVCardAttachment2(info.mimeType, info.fileName) ? await toVCardContent2(client, info) : toAttachmentContent2(client, info);
608
- var baseShape = (message, chatGuidHint, timestamp) => {
609
- const chat = resolveChatGuid(message, chatGuidHint);
610
- return {
611
- sender: { id: resolveSenderId(message) },
612
- space: {
613
- id: chat,
614
- type: chat.includes(";+;") ? "group" : "dm"
615
- },
616
- timestamp
617
- };
618
- };
504
+ var attachmentContent = async (client, info) => isVCardAttachment(info.mimeType, info.fileName) ? await toVCardContent2(client, info) : toAttachmentContent2(client, info);
619
505
  var buildAttachmentMessage = async (client, base, info, id, partIndex, parentId) => {
620
506
  const content = await attachmentContent(client, info);
621
507
  const msg = { ...base, id, content, partIndex };
@@ -624,13 +510,29 @@ var buildAttachmentMessage = async (client, base, info, id, partIndex, parentId)
624
510
  }
625
511
  return msg;
626
512
  };
627
- var rebuildFromAppleMessage = async (client, message, chatGuidHint) => {
628
- const messageGuidStr = message.guid;
629
- const timestamp = message.dateCreated ?? /* @__PURE__ */ new Date();
630
- const base = baseShape(message, chatGuidHint, timestamp);
631
- if (message.attachments.length === 1) {
632
- const info = message.attachments[0];
633
- if (!info) {
513
+ var toRichlinkMessage = (message, base, id) => {
514
+ const url = message.text ?? "";
515
+ try {
516
+ return { ...base, id, content: asRichlink({ url }) };
517
+ } catch (err) {
518
+ console.warn(
519
+ "[spectrum-ts][imessage] failed to convert message to rich link; falling back to text/custom content",
520
+ { error: err, message, url }
521
+ );
522
+ return {
523
+ ...base,
524
+ id,
525
+ content: url ? asText(url) : asCustom(message)
526
+ };
527
+ }
528
+ };
529
+ var rebuildFromAppleMessage = async (client, message, chatGuidHint) => {
530
+ const messageGuidStr = message.guid;
531
+ const timestamp = message.dateCreated ?? /* @__PURE__ */ new Date();
532
+ const base = buildMessageBase(message, chatGuidHint, timestamp);
533
+ if (message.attachments.length === 1) {
534
+ const info = message.attachments[0];
535
+ if (!info) {
634
536
  throw new Error("Unreachable: attachments.length === 1 but no element");
635
537
  }
636
538
  return buildAttachmentMessage(client, base, info, messageGuidStr, 0);
@@ -656,22 +558,13 @@ var rebuildFromAppleMessage = async (client, message, chatGuidHint) => {
656
558
  return {
657
559
  ...base,
658
560
  id: messageGuidStr,
659
- content: asGroup({ items })
561
+ content: asProviderGroup(items)
660
562
  };
661
563
  }
662
- const text = message.text;
663
564
  if (getBalloonBundleId(message) === URL_BALLOON_BUNDLE_ID) {
664
- const url = text ?? "";
665
- try {
666
- return { ...base, id: messageGuidStr, content: asRichlink({ url }) };
667
- } catch {
668
- return {
669
- ...base,
670
- id: messageGuidStr,
671
- content: url ? asText(url) : asCustom(message)
672
- };
673
- }
565
+ return toRichlinkMessage(message, base, messageGuidStr);
674
566
  }
567
+ const text = message.text;
675
568
  return {
676
569
  ...base,
677
570
  id: messageGuidStr,
@@ -682,45 +575,166 @@ var cacheMessage = (cache, message) => {
682
575
  cache.set(message.id, message);
683
576
  if (message.content.type === "group") {
684
577
  for (const item of message.content.items) {
685
- cache.set(item.id, item);
578
+ if (isIMessageMessage(item)) {
579
+ cache.set(item.id, item);
580
+ }
686
581
  }
687
582
  }
688
583
  };
689
- var toRichlinkMessage = (event, base, id) => {
690
- const url = event.message.text ?? "";
691
- try {
692
- return { ...base, id, content: asRichlink({ url }) };
693
- } catch {
694
- return {
584
+ var toInboundMessages = async (client, cache, event) => {
585
+ const base = buildMessageBase(event.message, event.chatGuid, event.timestamp);
586
+ const messageGuidStr = event.message.guid;
587
+ if (getBalloonBundleId(event.message) === URL_BALLOON_BUNDLE_ID) {
588
+ const msg2 = toRichlinkMessage(event.message, base, messageGuidStr);
589
+ cacheMessage(cache, msg2);
590
+ return [msg2];
591
+ }
592
+ if (event.message.attachments.length === 1) {
593
+ const info = event.message.attachments[0];
594
+ if (!info) {
595
+ throw new Error("Unreachable: attachments.length === 1 but no element");
596
+ }
597
+ const msg2 = await buildAttachmentMessage(
598
+ client,
599
+ base,
600
+ info,
601
+ messageGuidStr,
602
+ 0
603
+ );
604
+ cacheMessage(cache, msg2);
605
+ return [msg2];
606
+ }
607
+ if (event.message.attachments.length > 1) {
608
+ const items = [];
609
+ for (let i = 0; i < event.message.attachments.length; i++) {
610
+ const info = event.message.attachments[i];
611
+ if (!info) {
612
+ continue;
613
+ }
614
+ items.push(
615
+ await buildAttachmentMessage(
616
+ client,
617
+ base,
618
+ info,
619
+ formatChildId(i, messageGuidStr),
620
+ i,
621
+ messageGuidStr
622
+ )
623
+ );
624
+ }
625
+ const parent = {
695
626
  ...base,
696
- id,
697
- content: url ? asText(url) : asCustom(event.message)
627
+ id: messageGuidStr,
628
+ content: asProviderGroup(items)
698
629
  };
630
+ cacheMessage(cache, parent);
631
+ return [parent];
699
632
  }
633
+ const text = event.message.text;
634
+ const msg = {
635
+ ...base,
636
+ id: messageGuidStr,
637
+ content: text ? asText(text) : asCustom(event.message)
638
+ };
639
+ cacheMessage(cache, msg);
640
+ return [msg];
700
641
  };
701
- var PART_PREFIX = /^p:(\d+)\//;
702
- var formatChildId = (partIndex, parentGuid) => `p:${partIndex}/${parentGuid}`;
703
- var parseTapbackTarget = (target) => {
704
- const match = target.match(PART_PREFIX);
705
- const guid = target.replace(PART_PREFIX, "");
706
- const partIndex = match ? Number(match[1]) : 0;
707
- return { guid, partIndex };
642
+ var getMessage3 = async (remote, spaceId, msgId) => {
643
+ const cache = getMessageCache(remote);
644
+ const cached = cache.get(msgId);
645
+ if (cached) {
646
+ return cached;
647
+ }
648
+ const childRef = parseChildId(msgId);
649
+ if (childRef) {
650
+ try {
651
+ const fetched = await remote.messages.get(
652
+ messageGuid(childRef.parentGuid)
653
+ );
654
+ const parent = await rebuildFromAppleMessage(remote, fetched, spaceId);
655
+ cacheMessage(cache, parent);
656
+ if (parent.content.type !== "group") {
657
+ return;
658
+ }
659
+ const item = parent.content.items[childRef.partIndex];
660
+ return isIMessageMessage(item) ? item : void 0;
661
+ } catch (err) {
662
+ if (err instanceof NotFoundError) {
663
+ return;
664
+ }
665
+ throw err;
666
+ }
667
+ }
668
+ try {
669
+ const fetched = await remote.messages.get(messageGuid(msgId));
670
+ const rebuilt = await rebuildFromAppleMessage(remote, fetched, spaceId);
671
+ cacheMessage(cache, rebuilt);
672
+ return rebuilt;
673
+ } catch (err) {
674
+ if (err instanceof NotFoundError) {
675
+ return;
676
+ }
677
+ throw err;
678
+ }
708
679
  };
709
- var parseChildId = (id) => {
710
- const match = id.match(PART_PREFIX);
711
- if (!match) {
680
+
681
+ // src/providers/imessage/remote/reactions.ts
682
+ import {
683
+ chatGuid,
684
+ messageGuid as messageGuid2,
685
+ Reaction
686
+ } from "@photon-ai/advanced-imessage";
687
+ var EMOJI_TO_TAPBACK = {
688
+ "\u2764\uFE0F": Reaction.love,
689
+ "\u{1F44D}": Reaction.like,
690
+ "\u{1F44E}": Reaction.dislike,
691
+ "\u{1F602}": Reaction.laugh,
692
+ "\u203C\uFE0F": Reaction.emphasize,
693
+ "\u2753": Reaction.question
694
+ };
695
+ var TAPBACK_TO_EMOJI = Object.fromEntries(
696
+ Object.entries(EMOJI_TO_TAPBACK).map(([emoji, kind]) => [kind, emoji])
697
+ );
698
+ var TAPBACK_CODE_TO_KIND = {
699
+ "2000": Reaction.love,
700
+ "2001": Reaction.like,
701
+ "2002": Reaction.dislike,
702
+ "2003": Reaction.laugh,
703
+ "2004": Reaction.emphasize,
704
+ "2005": Reaction.question,
705
+ "2006": Reaction.emoji,
706
+ "2007": Reaction.sticker
707
+ };
708
+ var isTapbackRemoval = (code) => code.startsWith("3");
709
+ var resolveReactionEmoji = (type, emoji) => {
710
+ if (emoji) {
711
+ return emoji;
712
+ }
713
+ if (!type) {
712
714
  return null;
713
715
  }
714
- return {
715
- parentGuid: id.replace(PART_PREFIX, ""),
716
- partIndex: Number(match[1])
717
- };
716
+ const kind = TAPBACK_CODE_TO_KIND[type] ?? type;
717
+ return TAPBACK_TO_EMOJI[kind] ?? null;
718
+ };
719
+ var getAssociatedMessageType = (message) => {
720
+ const direct = message.associatedMessageType;
721
+ if (typeof direct === "string") {
722
+ return direct;
723
+ }
724
+ const raw = message._raw;
725
+ const fromRaw = raw?.associatedMessageType;
726
+ return typeof fromRaw === "string" ? fromRaw : void 0;
718
727
  };
728
+ var asProviderReaction = (emoji, target) => reactionSchema.parse({
729
+ emoji,
730
+ target,
731
+ type: "reaction"
732
+ });
719
733
  var resolveReactionTarget = async (client, cache, strippedGuid, partIndex) => {
720
734
  let candidate = cache.get(strippedGuid);
721
735
  if (!candidate) {
722
736
  try {
723
- const fetched = await client.messages.get(messageGuid(strippedGuid));
737
+ const fetched = await client.messages.get(messageGuid2(strippedGuid));
724
738
  candidate = await rebuildFromAppleMessage(client, fetched);
725
739
  cacheMessage(cache, candidate);
726
740
  } catch {
@@ -728,12 +742,16 @@ var resolveReactionTarget = async (client, cache, strippedGuid, partIndex) => {
728
742
  }
729
743
  }
730
744
  if (candidate.content.type === "group") {
731
- const item = candidate.content.items[partIndex];
732
- return item ?? candidate;
745
+ const items = candidate.content.items;
746
+ if (!Array.isArray(items)) {
747
+ return candidate;
748
+ }
749
+ const item = items[partIndex];
750
+ return isIMessageMessage(item) ? item : candidate;
733
751
  }
734
752
  return candidate;
735
753
  };
736
- var toReactionMessage = async (client, cache, event, base, id, target) => {
754
+ var toReactionMessages = async (client, cache, event, target) => {
737
755
  const type = getAssociatedMessageType(event.message);
738
756
  if (type && isTapbackRemoval(type)) {
739
757
  return [];
@@ -755,76 +773,640 @@ var toReactionMessage = async (client, cache, event, base, id, target) => {
755
773
  if (!resolved) {
756
774
  return [];
757
775
  }
776
+ const messageId = event.message.guid;
777
+ if (typeof messageId !== "string" || messageId.length === 0) {
778
+ return [];
779
+ }
780
+ const base = buildMessageBase(event.message, event.chatGuid, event.timestamp);
758
781
  return [
759
782
  {
760
783
  ...base,
761
- id,
762
- content: asReaction({ emoji, target: resolved })
784
+ id: messageId,
785
+ content: asProviderReaction(emoji, resolved)
763
786
  }
764
787
  ];
765
788
  };
766
- var toMessages2 = async (client, cache, event) => {
767
- const base = baseShape(event.message, event.chatGuid, event.timestamp);
768
- const messageGuidStr = event.message.guid;
769
- const assoc = event.message.associatedMessageGuid;
770
- if (assoc) {
771
- return toReactionMessage(client, cache, event, base, messageGuidStr, assoc);
789
+ var reactToMessage = async (remote, spaceId, target, reaction) => {
790
+ const chat = chatGuid(spaceId);
791
+ const parentGuid = target.parentId ?? target.id;
792
+ const guid = messageGuid2(parentGuid);
793
+ const opts = typeof target.partIndex === "number" ? { partIndex: target.partIndex } : void 0;
794
+ const native = EMOJI_TO_TAPBACK[reaction];
795
+ if (native) {
796
+ await remote.messages.react(chat, guid, native, opts);
797
+ } else {
798
+ await remote.messages.reactEmoji(chat, guid, reaction, opts);
772
799
  }
773
- if (getBalloonBundleId(event.message) === URL_BALLOON_BUNDLE_ID) {
774
- const msg2 = toRichlinkMessage(event, base, messageGuidStr);
775
- cacheMessage(cache, msg2);
776
- return [msg2];
800
+ };
801
+
802
+ // src/providers/imessage/remote/send.ts
803
+ import {
804
+ chatGuid as chatGuid2,
805
+ messageGuid as messageGuid3
806
+ } from "@photon-ai/advanced-imessage";
807
+
808
+ // src/utils/audio.ts
809
+ import { spawn } from "child_process";
810
+ import { mkdtemp as mkdtemp2, readFile as readFile2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
811
+ import { tmpdir as tmpdir2 } from "os";
812
+ import { join as join2 } from "path";
813
+ var M4A_BRANDS = /* @__PURE__ */ new Set([
814
+ "M4A ",
815
+ "M4B ",
816
+ "M4P ",
817
+ "mp42",
818
+ "mp41",
819
+ "isom",
820
+ "iso2"
821
+ ]);
822
+ var M4A_MIME_TYPES = /* @__PURE__ */ new Set([
823
+ "audio/mp4",
824
+ "audio/mp4a-latm",
825
+ "audio/x-m4a",
826
+ "audio/aac",
827
+ "audio/aacp"
828
+ ]);
829
+ var FFMPEG_MISSING_MESSAGE = "voice content: input is not m4a/aac and ffmpeg is unavailable. Install `ffmpeg-static` or ensure `ffmpeg` is on PATH.";
830
+ var isM4a = (buffer) => {
831
+ if (buffer.length < 12) {
832
+ return false;
777
833
  }
778
- if (event.message.attachments.length === 1) {
779
- const info = event.message.attachments[0];
780
- if (!info) {
781
- throw new Error("Unreachable: attachments.length === 1 but no element");
834
+ if (buffer.toString("ascii", 4, 8) !== "ftyp") {
835
+ return false;
836
+ }
837
+ return M4A_BRANDS.has(buffer.toString("ascii", 8, 12));
838
+ };
839
+ var isM4aMimeType = (mimeType) => M4A_MIME_TYPES.has(mimeType.toLowerCase());
840
+ var cachedFfmpegPath;
841
+ var tryStaticBinary = async () => {
842
+ try {
843
+ const mod = await import("ffmpeg-static");
844
+ return mod.default ?? void 0;
845
+ } catch {
846
+ return void 0;
847
+ }
848
+ };
849
+ var resolveFfmpegPath = async () => {
850
+ if (cachedFfmpegPath) {
851
+ return cachedFfmpegPath;
852
+ }
853
+ cachedFfmpegPath = await tryStaticBinary() ?? "ffmpeg";
854
+ return cachedFfmpegPath;
855
+ };
856
+ var collectStream = (stream2) => {
857
+ if (!stream2) {
858
+ return Promise.resolve("");
859
+ }
860
+ return new Promise((resolve, reject) => {
861
+ const chunks = [];
862
+ stream2.on("data", (chunk) => {
863
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
864
+ });
865
+ stream2.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
866
+ stream2.on("error", reject);
867
+ });
868
+ };
869
+ var isMissingBinaryError = (err) => err?.code === "ENOENT";
870
+ var runFfmpeg = (ffmpegPath, args) => {
871
+ const proc = spawn(ffmpegPath, args, { stdio: ["ignore", "ignore", "pipe"] });
872
+ const stderr = collectStream(proc.stderr);
873
+ const exit = new Promise((resolve, reject) => {
874
+ proc.on(
875
+ "error",
876
+ (err) => reject(
877
+ isMissingBinaryError(err) ? new Error(FFMPEG_MISSING_MESSAGE) : err
878
+ )
879
+ );
880
+ proc.on("exit", (code) => resolve(code ?? -1));
881
+ });
882
+ return Promise.all([exit, stderr]).then(([code, text]) => ({
883
+ code,
884
+ stderr: text
885
+ }));
886
+ };
887
+ var DURATION_PATTERN = /Duration:\s*(\d+):(\d{2}):(\d{2})(?:\.(\d{1,3}))?/;
888
+ var parseDuration = (stderr) => {
889
+ const match = stderr.match(DURATION_PATTERN);
890
+ if (!match) {
891
+ return void 0;
892
+ }
893
+ const [, hh, mm, ss, frac] = match;
894
+ const seconds = Number(hh) * 3600 + Number(mm) * 60 + Number(ss) + Number(`0.${frac ?? 0}`);
895
+ return Number.isFinite(seconds) ? seconds : void 0;
896
+ };
897
+ var transcodeToM4a = async (buffer) => {
898
+ const ffmpeg = await resolveFfmpegPath();
899
+ const dir = await mkdtemp2(join2(tmpdir2(), "spectrum-voice-"));
900
+ const inPath = join2(dir, "in");
901
+ const outPath = join2(dir, "out.m4a");
902
+ try {
903
+ await writeFile2(inPath, buffer);
904
+ const { code, stderr } = await runFfmpeg(ffmpeg, [
905
+ "-y",
906
+ "-i",
907
+ inPath,
908
+ "-f",
909
+ "ipod",
910
+ "-c:a",
911
+ "aac",
912
+ outPath
913
+ ]);
914
+ if (code !== 0) {
915
+ throw new Error(`ffmpeg conversion failed (exit ${code}): ${stderr}`);
916
+ }
917
+ const out = await readFile2(outPath);
918
+ return { buffer: out, duration: parseDuration(stderr) };
919
+ } finally {
920
+ await rm2(dir, { recursive: true, force: true }).catch(() => {
921
+ });
922
+ }
923
+ };
924
+ var ensureM4a = async (buffer, mimeType) => {
925
+ if (isM4aMimeType(mimeType) || isM4a(buffer)) {
926
+ return { buffer };
927
+ }
928
+ return transcodeToM4a(buffer);
929
+ };
930
+
931
+ // src/providers/imessage/remote/send.ts
932
+ var GROUP_ITEM_ALLOWED = /* @__PURE__ */ new Set([
933
+ "text",
934
+ "attachment",
935
+ "contact",
936
+ "voice"
937
+ ]);
938
+ var MAX_GROUP_TEXT_ITEMS = 1;
939
+ var toDate = (value) => {
940
+ if (value instanceof Date) {
941
+ return value;
942
+ }
943
+ if (typeof value === "number" || typeof value === "string") {
944
+ const date = new Date(value);
945
+ return Number.isNaN(date.getTime()) ? void 0 : date;
946
+ }
947
+ };
948
+ var receiptTimestamp = (receipt) => toDate(receipt.timestamp) ?? toDate(receipt.date) ?? toDate(receipt.dateCreated) ?? /* @__PURE__ */ new Date();
949
+ var receiptGuid = (receipt) => {
950
+ if (typeof receipt.guid !== "string" || receipt.guid.length === 0) {
951
+ throw new Error("iMessage send receipt is missing a message guid");
952
+ }
953
+ return receipt.guid;
954
+ };
955
+ var outboundRecord = (spaceId, id, content, timestamp, extras) => ({
956
+ id,
957
+ content,
958
+ space: { id: spaceId },
959
+ timestamp,
960
+ ...extras
961
+ });
962
+ var withReply = (options, replyTo) => replyTo ? { ...options, replyTo } : options;
963
+ var replyOptions = (replyTo) => replyTo ? { replyTo } : void 0;
964
+ var sendVCardAttachment = (remote, name, vcf) => remote.attachments.upload({
965
+ data: Buffer.from(vcf, "utf8"),
966
+ fileName: name,
967
+ mimeType: "text/vcard"
968
+ });
969
+ var sendContactAttachment = async (remote, content) => {
970
+ const vcf = await toVCard(content);
971
+ const name = vcardFileName(content);
972
+ const upload = await sendVCardAttachment(remote, name, vcf);
973
+ return { guid: upload.guid, name };
974
+ };
975
+ var uploadAttachment = async (remote, content) => {
976
+ const attachment = await remote.attachments.upload({
977
+ data: await content.read(),
978
+ fileName: content.name,
979
+ mimeType: content.mimeType
980
+ });
981
+ return { guid: attachment.guid, name: content.name };
982
+ };
983
+ var uploadVoice = async (remote, content) => {
984
+ const { buffer } = await ensureM4a(await content.read(), content.mimeType);
985
+ const name = content.name ?? "voice.m4a";
986
+ const attachment = await remote.attachments.upload({
987
+ data: buffer,
988
+ fileName: name,
989
+ mimeType: "audio/x-m4a"
990
+ });
991
+ return { guid: attachment.guid, name };
992
+ };
993
+ var sendContent = async (remote, spaceId, chat, content, replyTo) => {
994
+ switch (content.type) {
995
+ case "text": {
996
+ const receipt = await remote.messages.send(
997
+ chat,
998
+ content.text,
999
+ withReply({}, replyTo)
1000
+ );
1001
+ return outboundRecord(
1002
+ spaceId,
1003
+ receiptGuid(receipt),
1004
+ content,
1005
+ receiptTimestamp(receipt)
1006
+ );
1007
+ }
1008
+ case "richlink": {
1009
+ const receipt = await remote.messages.send(
1010
+ chat,
1011
+ content.url,
1012
+ withReply({ richLink: true }, replyTo)
1013
+ );
1014
+ return outboundRecord(
1015
+ spaceId,
1016
+ receiptGuid(receipt),
1017
+ content,
1018
+ receiptTimestamp(receipt)
1019
+ );
1020
+ }
1021
+ case "attachment": {
1022
+ const { guid } = await uploadAttachment(remote, content);
1023
+ const receipt = await remote.messages.send(chat, "", {
1024
+ attachment: guid,
1025
+ ...replyOptions(replyTo)
1026
+ });
1027
+ return outboundRecord(
1028
+ spaceId,
1029
+ receiptGuid(receipt),
1030
+ content,
1031
+ receiptTimestamp(receipt)
1032
+ );
1033
+ }
1034
+ case "contact": {
1035
+ const { guid } = await sendContactAttachment(remote, content);
1036
+ const receipt = await remote.messages.send(chat, "", {
1037
+ attachment: guid,
1038
+ ...replyOptions(replyTo)
1039
+ });
1040
+ return outboundRecord(
1041
+ spaceId,
1042
+ receiptGuid(receipt),
1043
+ content,
1044
+ receiptTimestamp(receipt)
1045
+ );
1046
+ }
1047
+ case "voice": {
1048
+ const { guid } = await uploadVoice(remote, content);
1049
+ const receipt = await remote.messages.send(chat, "", {
1050
+ attachment: guid,
1051
+ audioMessage: true,
1052
+ ...replyOptions(replyTo)
1053
+ });
1054
+ return outboundRecord(
1055
+ spaceId,
1056
+ receiptGuid(receipt),
1057
+ content,
1058
+ receiptTimestamp(receipt)
1059
+ );
1060
+ }
1061
+ case "poll":
1062
+ if (replyTo) {
1063
+ throw unsupportedRemoteContent(
1064
+ "poll",
1065
+ "polls cannot be sent as replies"
1066
+ );
1067
+ }
1068
+ return outboundRecord(
1069
+ spaceId,
1070
+ receiptGuid(
1071
+ await remote.polls.create(
1072
+ chat,
1073
+ content.title,
1074
+ content.options.map((option) => option.title)
1075
+ )
1076
+ ),
1077
+ content,
1078
+ /* @__PURE__ */ new Date()
1079
+ );
1080
+ default:
1081
+ throw unsupportedRemoteContent(content.type);
1082
+ }
1083
+ };
1084
+ var validateGroupContent = (content) => {
1085
+ let textCount = 0;
1086
+ for (const sub of content.items) {
1087
+ const itemType = sub.content.type;
1088
+ if (!GROUP_ITEM_ALLOWED.has(itemType)) {
1089
+ throw unsupportedRemoteContent(
1090
+ "group",
1091
+ `"${itemType}" items are not supported inside a group`
1092
+ );
1093
+ }
1094
+ if (itemType === "text" && ++textCount > MAX_GROUP_TEXT_ITEMS) {
1095
+ throw unsupportedRemoteContent(
1096
+ "group",
1097
+ `groups can contain at most ${MAX_GROUP_TEXT_ITEMS} text item`
1098
+ );
1099
+ }
1100
+ }
1101
+ };
1102
+ var resolvePart = async (remote, content) => {
1103
+ switch (content.type) {
1104
+ case "text":
1105
+ return { text: content.text };
1106
+ case "attachment": {
1107
+ const { guid, name } = await uploadAttachment(remote, content);
1108
+ return { attachmentGuid: guid, attachmentName: name };
1109
+ }
1110
+ case "contact": {
1111
+ const { guid, name } = await sendContactAttachment(remote, content);
1112
+ return { attachmentGuid: guid, attachmentName: name };
1113
+ }
1114
+ case "voice": {
1115
+ const { guid, name } = await uploadVoice(remote, content);
1116
+ return { attachmentGuid: guid, attachmentName: name };
1117
+ }
1118
+ default:
1119
+ throw unsupportedRemoteContent(content.type);
1120
+ }
1121
+ };
1122
+ var send3 = async (remote, spaceId, content) => {
1123
+ const chat = chatGuid2(spaceId);
1124
+ if (content.type === "group") {
1125
+ validateGroupContent(content);
1126
+ const resolved = await Promise.all(
1127
+ content.items.map((sub) => resolvePart(remote, sub.content))
1128
+ );
1129
+ const receipt = await remote.messages.sendMultipart(
1130
+ chat,
1131
+ resolved.map((part, idx) => ({ ...part, partIndex: idx }))
1132
+ );
1133
+ const parentGuid = receiptGuid(receipt);
1134
+ const timestamp = receiptTimestamp(receipt);
1135
+ const items = content.items.map(
1136
+ (sub, idx) => outboundRecord(
1137
+ spaceId,
1138
+ formatChildId(idx, parentGuid),
1139
+ sub.content,
1140
+ timestamp,
1141
+ { partIndex: idx, parentId: parentGuid }
1142
+ )
1143
+ );
1144
+ return outboundRecord(
1145
+ spaceId,
1146
+ parentGuid,
1147
+ asGroup({ items }),
1148
+ timestamp
1149
+ );
1150
+ }
1151
+ return sendContent(remote, spaceId, chat, content);
1152
+ };
1153
+ var replyToMessage = async (remote, spaceId, msgId, content) => {
1154
+ const chat = chatGuid2(spaceId);
1155
+ const replyTo = messageGuid3(msgId);
1156
+ return sendContent(remote, spaceId, chat, content, replyTo);
1157
+ };
1158
+ var editMessage = async (remote, spaceId, msgId, content) => {
1159
+ if (content.type !== "text") {
1160
+ throw unsupportedRemoteContent(
1161
+ content.type,
1162
+ "only text content can be edited"
1163
+ );
1164
+ }
1165
+ await remote.messages.edit(
1166
+ chatGuid2(spaceId),
1167
+ messageGuid3(msgId),
1168
+ content.text
1169
+ );
1170
+ };
1171
+
1172
+ // src/providers/imessage/remote/stream.ts
1173
+ import {
1174
+ AuthenticationError,
1175
+ IMessageError,
1176
+ NotFoundError as NotFoundError2,
1177
+ ValidationError
1178
+ } from "@photon-ai/advanced-imessage";
1179
+
1180
+ // src/utils/resumable-stream.ts
1181
+ var CATCH_UP_PAGE_SIZE = 100;
1182
+ var MAX_BUFFERED_LIVE_EVENTS = 1e3;
1183
+ var RECONNECT_INITIAL_DELAY_MS = 500;
1184
+ var RECONNECT_MAX_DELAY_MS = 3e4;
1185
+ var RetryableStreamError = class extends Error {
1186
+ constructor(message) {
1187
+ super(message);
1188
+ this.name = "RetryableStreamError";
1189
+ }
1190
+ };
1191
+ var LiveBufferOverflowError = class extends RetryableStreamError {
1192
+ constructor(limit) {
1193
+ super(`Live stream buffer exceeded ${limit} events during catch-up`);
1194
+ this.name = "LiveBufferOverflowError";
1195
+ }
1196
+ };
1197
+ var closeIterable = async (iterable) => {
1198
+ if (!iterable) {
1199
+ return;
1200
+ }
1201
+ await iterable.close?.();
1202
+ };
1203
+ var jitterDelay = (delayMs) => Math.random() * delayMs;
1204
+ var resumableOrderedStream = (options) => stream((emit, end) => {
1205
+ const catchUpPageSize = options.catchUpPageSize ?? CATCH_UP_PAGE_SIZE;
1206
+ const bufferLimit = options.bufferLimit ?? MAX_BUFFERED_LIVE_EVENTS;
1207
+ const initialRetryDelayMs = options.initialRetryDelayMs ?? RECONNECT_INITIAL_DELAY_MS;
1208
+ const maxRetryDelayMs = options.maxRetryDelayMs ?? RECONNECT_MAX_DELAY_MS;
1209
+ let activeLive;
1210
+ let closed = false;
1211
+ let lastCursor;
1212
+ let retryDelayMs = initialRetryDelayMs;
1213
+ let sleepTimer;
1214
+ let wakeSleep;
1215
+ const deliveredSinceCursor = /* @__PURE__ */ new Set();
1216
+ const resetRetryDelay = () => {
1217
+ retryDelayMs = initialRetryDelayMs;
1218
+ };
1219
+ const advanceCursor = (cursor, clearDelivered) => {
1220
+ if (!cursor || cursor === lastCursor) {
1221
+ return;
1222
+ }
1223
+ lastCursor = cursor;
1224
+ if (clearDelivered) {
1225
+ deliveredSinceCursor.clear();
1226
+ }
1227
+ };
1228
+ const deliverItem = async (item, resetRetry, clearOnCursorAdvance) => {
1229
+ const alreadyDelivered = deliveredSinceCursor.has(item.id);
1230
+ if (!alreadyDelivered) {
1231
+ for (const value of item.values) {
1232
+ await emit(value);
1233
+ }
1234
+ }
1235
+ advanceCursor(item.cursor, clearOnCursorAdvance);
1236
+ deliveredSinceCursor.add(item.id);
1237
+ if (resetRetry) {
1238
+ resetRetryDelay();
1239
+ }
1240
+ };
1241
+ const retryable = (error) => error instanceof RetryableStreamError || options.isRetryableError(error);
1242
+ const sleep = async (delayMs) => {
1243
+ if (delayMs <= 0 || closed) {
1244
+ return;
1245
+ }
1246
+ await new Promise((resolve) => {
1247
+ wakeSleep = resolve;
1248
+ sleepTimer = setTimeout(resolve, jitterDelay(delayMs));
1249
+ });
1250
+ sleepTimer = void 0;
1251
+ wakeSleep = void 0;
1252
+ };
1253
+ const cancelSleep = () => {
1254
+ if (sleepTimer) {
1255
+ clearTimeout(sleepTimer);
1256
+ sleepTimer = void 0;
1257
+ }
1258
+ wakeSleep?.();
1259
+ wakeSleep = void 0;
1260
+ };
1261
+ const nextRetryDelay = () => {
1262
+ const delay = retryDelayMs;
1263
+ retryDelayMs = Math.min(retryDelayMs * 2, maxRetryDelayMs);
1264
+ return delay;
1265
+ };
1266
+ const consumeLive = async () => {
1267
+ const live = options.subscribeLive();
1268
+ activeLive = live;
1269
+ try {
1270
+ for await (const event of live) {
1271
+ await deliverItem(await options.processLive(event), true, true);
1272
+ }
1273
+ throw new RetryableStreamError("Live stream ended");
1274
+ } finally {
1275
+ if (activeLive === live) {
1276
+ activeLive = void 0;
1277
+ }
1278
+ await closeIterable(live);
782
1279
  }
783
- const msg2 = await buildAttachmentMessage(
784
- client,
785
- base,
786
- info,
787
- messageGuidStr,
788
- 0
789
- );
790
- cacheMessage(cache, msg2);
791
- return [msg2];
792
- }
793
- if (event.message.attachments.length > 1) {
794
- const items = [];
795
- for (let i = 0; i < event.message.attachments.length; i++) {
796
- const info = event.message.attachments[i];
797
- if (!info) {
798
- continue;
1280
+ };
1281
+ const throwLiveError = (liveError) => {
1282
+ if (liveError) {
1283
+ throw liveError;
1284
+ }
1285
+ };
1286
+ const bufferLiveEvent = (buffer, event) => {
1287
+ if (buffer.length >= bufferLimit) {
1288
+ throw new LiveBufferOverflowError(bufferLimit);
1289
+ }
1290
+ buffer.push(event);
1291
+ };
1292
+ const startLivePump = (live, isBuffering, liveBuffer) => {
1293
+ let liveError;
1294
+ const pump2 = (async () => {
1295
+ try {
1296
+ for await (const event of live) {
1297
+ if (isBuffering()) {
1298
+ bufferLiveEvent(liveBuffer, event);
1299
+ continue;
1300
+ }
1301
+ await deliverItem(await options.processLive(event), true, true);
1302
+ }
1303
+ throw new RetryableStreamError("Live stream ended");
1304
+ } catch (error) {
1305
+ liveError = error;
799
1306
  }
800
- items.push(
801
- await buildAttachmentMessage(
802
- client,
803
- base,
804
- info,
805
- formatChildId(i, messageGuidStr),
806
- i,
807
- messageGuidStr
808
- )
1307
+ })();
1308
+ return {
1309
+ getError: () => liveError,
1310
+ pump: pump2
1311
+ };
1312
+ };
1313
+ const replayMissed = async (cursor, getLiveError) => {
1314
+ for await (const event of options.fetchMissed(cursor, {
1315
+ limit: catchUpPageSize
1316
+ })) {
1317
+ throwLiveError(getLiveError());
1318
+ await deliverItem(await options.processMissed(event), false, false);
1319
+ }
1320
+ throwLiveError(getLiveError());
1321
+ };
1322
+ const flushLiveBuffer = async (liveBuffer, getLiveError) => {
1323
+ let index = 0;
1324
+ let lastFlushedId;
1325
+ while (index < liveBuffer.length) {
1326
+ throwLiveError(getLiveError());
1327
+ const event = liveBuffer[index];
1328
+ if (event === void 0) {
1329
+ throw new RetryableStreamError("Live stream buffer index missing");
1330
+ }
1331
+ const item = await options.processLive(event);
1332
+ await deliverItem(item, true, false);
1333
+ lastFlushedId = item.id;
1334
+ index += 1;
1335
+ }
1336
+ liveBuffer.length = 0;
1337
+ throwLiveError(getLiveError());
1338
+ return lastFlushedId;
1339
+ };
1340
+ const compactDeliveredIds = (lastId) => {
1341
+ if (!lastId) {
1342
+ return;
1343
+ }
1344
+ deliveredSinceCursor.clear();
1345
+ deliveredSinceCursor.add(lastId);
1346
+ };
1347
+ const catchUpThenConsumeLive = async (cursor) => {
1348
+ const live = options.subscribeLive();
1349
+ activeLive = live;
1350
+ let buffering = true;
1351
+ const liveBuffer = [];
1352
+ const livePump = startLivePump(live, () => buffering, liveBuffer);
1353
+ try {
1354
+ await replayMissed(cursor, livePump.getError);
1355
+ const lastFlushedId = await flushLiveBuffer(
1356
+ liveBuffer,
1357
+ livePump.getError
809
1358
  );
1359
+ compactDeliveredIds(lastFlushedId);
1360
+ buffering = false;
1361
+ resetRetryDelay();
1362
+ await livePump.pump;
1363
+ throwLiveError(livePump.getError());
1364
+ } finally {
1365
+ buffering = false;
1366
+ if (activeLive === live) {
1367
+ activeLive = void 0;
1368
+ }
1369
+ await closeIterable(live);
1370
+ await livePump.pump.catch(() => void 0);
810
1371
  }
811
- const parent = {
812
- ...base,
813
- id: messageGuidStr,
814
- content: asGroup({ items })
815
- };
816
- cacheMessage(cache, parent);
817
- return [parent];
818
- }
819
- const text = event.message.text;
820
- const msg = {
821
- ...base,
822
- id: messageGuidStr,
823
- content: text ? asText(text) : asCustom(event.message)
824
1372
  };
825
- cacheMessage(cache, msg);
826
- return [msg];
827
- };
1373
+ const run = async () => {
1374
+ while (!closed) {
1375
+ try {
1376
+ if (lastCursor) {
1377
+ await catchUpThenConsumeLive(lastCursor);
1378
+ } else {
1379
+ await consumeLive();
1380
+ }
1381
+ } catch (error) {
1382
+ await closeIterable(activeLive);
1383
+ activeLive = void 0;
1384
+ if (closed) {
1385
+ break;
1386
+ }
1387
+ if (!retryable(error)) {
1388
+ end(error);
1389
+ return;
1390
+ }
1391
+ await sleep(nextRetryDelay());
1392
+ }
1393
+ }
1394
+ end();
1395
+ };
1396
+ const pump = run().catch((error) => {
1397
+ if (!closed) {
1398
+ end(error);
1399
+ }
1400
+ });
1401
+ return async () => {
1402
+ closed = true;
1403
+ cancelSleep();
1404
+ await closeIterable(activeLive);
1405
+ await pump;
1406
+ };
1407
+ });
1408
+
1409
+ // src/providers/imessage/remote/polls.ts
828
1410
  var isVotedPollEvent = (event) => event.delta.type === "voted";
829
1411
  var isUnvotedPollEvent = (event) => event.delta.type === "unvoted";
830
1412
  var toCachedPoll = (input) => {
@@ -908,7 +1490,7 @@ var buildPollOptionMessage = (input) => {
908
1490
  };
909
1491
  };
910
1492
  var buildPollOptionMessages = (input) => {
911
- const messages3 = [];
1493
+ const messages5 = [];
912
1494
  for (const delta of input.deltas) {
913
1495
  const message = buildPollOptionMessage({
914
1496
  cached: input.cached,
@@ -919,10 +1501,10 @@ var buildPollOptionMessages = (input) => {
919
1501
  senderAddress: input.senderAddress
920
1502
  });
921
1503
  if (message) {
922
- messages3.push(message);
1504
+ messages5.push(message);
923
1505
  }
924
1506
  }
925
- return messages3;
1507
+ return messages5;
926
1508
  };
927
1509
  var allOptionIdsKnown = (cached, optionIds) => optionIds.every((optionId) => cached.optionsByIdentifier.has(optionId));
928
1510
  var refreshPollMetadata = async (client, pollCache, event, fallbackOptionIds) => {
@@ -977,7 +1559,7 @@ var toPollVoteMessages = async (client, pollCache, event) => {
977
1559
  senderAddress,
978
1560
  currentOptionIds
979
1561
  );
980
- const messages3 = buildPollOptionMessages({
1562
+ const messages5 = buildPollOptionMessages({
981
1563
  cached: resolvedPoll,
982
1564
  chatGuid: chatGuidStr,
983
1565
  deltas,
@@ -990,7 +1572,7 @@ var toPollVoteMessages = async (client, pollCache, event) => {
990
1572
  currentOptionIds,
991
1573
  event.at
992
1574
  );
993
- return messages3;
1575
+ return messages5;
994
1576
  };
995
1577
  var toPollUnvoteMessages = async (client, pollCache, event) => {
996
1578
  const senderAddress = event.actor.address;
@@ -1006,23 +1588,16 @@ var toPollUnvoteMessages = async (client, pollCache, event) => {
1006
1588
  return [];
1007
1589
  }
1008
1590
  const chatGuidStr = event.chatGuid;
1009
- const messages3 = [];
1010
1591
  const deltas = pollCache.clearedActorSelectionDeltas(pollId, senderAddress);
1011
- for (const delta of deltas) {
1012
- const message = buildPollOptionMessage({
1013
- cached,
1014
- chatGuid: chatGuidStr,
1015
- event,
1016
- optionId: delta.optionId,
1017
- selected: delta.selected,
1018
- senderAddress
1019
- });
1020
- if (message) {
1021
- messages3.push(message);
1022
- }
1023
- }
1592
+ const messages5 = buildPollOptionMessages({
1593
+ cached,
1594
+ chatGuid: chatGuidStr,
1595
+ deltas,
1596
+ event,
1597
+ senderAddress
1598
+ });
1024
1599
  pollCache.commitActorSelection(pollId, senderAddress, [], event.at);
1025
- return messages3;
1600
+ return messages5;
1026
1601
  };
1027
1602
  var toPollDeltaMessages = async (client, pollCache, event) => {
1028
1603
  if (isVotedPollEvent(event)) {
@@ -1033,294 +1608,156 @@ var toPollDeltaMessages = async (client, pollCache, event) => {
1033
1608
  }
1034
1609
  return [];
1035
1610
  };
1036
- var clientStream = (client, pollCache) => {
1037
- const messageSub = client.messages.subscribe("message.received");
1038
- const pollSub = client.polls.subscribe();
1611
+
1612
+ // src/providers/imessage/remote/stream.ts
1613
+ var pollRetryDelay = (delayMs) => Math.random() * delayMs;
1614
+ var isRetryableIMessageStreamError = (error) => {
1615
+ if (error instanceof AuthenticationError || error instanceof NotFoundError2 || error instanceof ValidationError) {
1616
+ return false;
1617
+ }
1618
+ if (error instanceof IMessageError) {
1619
+ return true;
1620
+ }
1621
+ return false;
1622
+ };
1623
+ var toMessageItem = async (client, event, cursor) => {
1624
+ const id = event.message.guid;
1625
+ if (event.message.isFromMe) {
1626
+ return { cursor, id, values: [] };
1627
+ }
1039
1628
  const cache = getMessageCache(client);
1040
- return stream((emit, end) => {
1041
- const messagePump = (async () => {
1042
- try {
1043
- for await (const event of messageSub) {
1044
- if (event.message.isFromMe) {
1045
- continue;
1046
- }
1047
- for (const message of await toMessages2(client, cache, event)) {
1048
- await emit(message);
1049
- }
1050
- }
1051
- } catch (e) {
1052
- end(e);
1053
- }
1054
- })();
1055
- const pollPump = (async () => {
1056
- try {
1057
- for await (const event of pollSub) {
1058
- cachePollEvent(pollCache, event);
1059
- if (event.actor.isFromMe) {
1060
- continue;
1061
- }
1062
- const messages3 = await toPollDeltaMessages(client, pollCache, event);
1063
- for (const vote of messages3) {
1064
- await emit(vote);
1065
- }
1066
- }
1067
- } catch (e) {
1068
- console.error("[spectrum-ts][imessage][poll] stream failed", e);
1069
- }
1070
- })();
1071
- return async () => {
1072
- messageSub.close();
1073
- pollSub.close();
1074
- await Promise.all([messagePump, pollPump]);
1075
- };
1076
- });
1629
+ const target = event.message.associatedMessageGuid;
1630
+ const values = target ? await toReactionMessages(client, cache, event, target) : await toInboundMessages(client, cache, event);
1631
+ return { cursor, id, values };
1077
1632
  };
1078
- var sendVCardAttachment = (remote, name, vcf) => remote.attachments.upload({
1079
- data: Buffer.from(vcf, "utf8"),
1080
- fileName: name,
1081
- mimeType: "text/vcard"
1633
+ var messageStream = (client) => resumableOrderedStream({
1634
+ fetchMissed: (cursor, { limit }) => client.messages.fetchMissed(cursor, { limit }),
1635
+ isRetryableError: isRetryableIMessageStreamError,
1636
+ processLive: (event) => toMessageItem(client, event, event.cursor),
1637
+ processMissed: (message) => toMessageItem(client, receivedEventFromMessage(message)),
1638
+ subscribeLive: () => client.messages.subscribe("message.received")
1082
1639
  });
1083
- var vcardFileName2 = (contact) => {
1084
- const base = contact.name?.formatted ?? contact.user?.id ?? "contact";
1085
- return `${base.replace(/[^a-zA-Z0-9_\-.]/g, "_")}.vcf`;
1086
- };
1087
- var sendContactAttachment = async (remote, content) => {
1088
- const vcf = await toVCard(content);
1089
- const upload = await sendVCardAttachment(remote, vcardFileName2(content), vcf);
1090
- return upload.guid;
1091
- };
1092
- var messages2 = (clients) => {
1093
- const pollCache = getPollCache(clients);
1094
- return mergeStreams(clients.map((client) => clientStream(client, pollCache)));
1640
+ var logPollStreamError = (error) => {
1641
+ console.error("[spectrum-ts][imessage][poll] stream failed", error);
1095
1642
  };
1096
- var startTyping = async (clients, spaceId) => {
1097
- const remote = clients[0];
1098
- if (!remote) {
1643
+ var emitPollMessages = async (client, pollCache, event, emit) => {
1644
+ cachePollEvent(pollCache, event);
1645
+ if (event.actor.isFromMe) {
1099
1646
  return;
1100
1647
  }
1101
- await remote.chats.startTyping(chatGuid(spaceId));
1102
- };
1103
- var stopTyping = async (clients, spaceId) => {
1104
- const remote = clients[0];
1105
- if (!remote) {
1106
- return;
1648
+ const messages5 = await toPollDeltaMessages(client, pollCache, event);
1649
+ for (const vote of messages5) {
1650
+ await emit(vote);
1107
1651
  }
1108
- await remote.chats.stopTyping(chatGuid(spaceId));
1109
1652
  };
1110
- var sendSingle = async (remote, chat, content) => {
1111
- switch (content.type) {
1112
- case "text":
1113
- return toSendResult(await remote.messages.send(chat, content.text));
1114
- case "richlink":
1115
- return toSendResult(
1116
- await remote.messages.send(chat, content.url, { richLink: true })
1117
- );
1118
- case "attachment": {
1119
- const attachment = await remote.attachments.upload({
1120
- data: await content.read(),
1121
- fileName: content.name,
1122
- mimeType: content.mimeType
1123
- });
1124
- return toSendResult(
1125
- await remote.messages.send(chat, "", { attachment: attachment.guid })
1126
- );
1127
- }
1128
- case "contact": {
1129
- const attachment = await sendContactAttachment(remote, content);
1130
- return toSendResult(await remote.messages.send(chat, "", { attachment }));
1131
- }
1132
- case "voice": {
1133
- const { buffer } = await ensureM4a(
1134
- await content.read(),
1135
- content.mimeType
1136
- );
1137
- const attachment = await remote.attachments.upload({
1138
- data: buffer,
1139
- fileName: content.name ?? "voice.m4a",
1140
- mimeType: "audio/x-m4a"
1141
- });
1142
- return toSendResult(
1143
- await remote.messages.send(chat, "", {
1144
- attachment: attachment.guid,
1145
- audioMessage: true
1146
- })
1147
- );
1148
- }
1149
- case "poll":
1150
- return toSendResult(
1151
- await remote.polls.create(
1152
- chat,
1153
- content.title,
1154
- content.options.map((o) => o.title)
1155
- )
1156
- );
1157
- default:
1158
- throw unsupportedContent(content.type);
1653
+ var runPollSubscription = async (client, pollCache, subscription, emit, onEvent) => {
1654
+ for await (const event of subscription) {
1655
+ onEvent();
1656
+ await emitPollMessages(client, pollCache, event, emit);
1159
1657
  }
1160
1658
  };
1161
- var send2 = async (clients, spaceId, content) => {
1162
- const remote = clients[0];
1163
- if (!remote) {
1164
- throw new Error("No remote iMessage client available");
1165
- }
1166
- const chat = chatGuid(spaceId);
1167
- if (content.type === "group") {
1168
- for (const sub of content.items) {
1169
- const itemType = sub.content.type;
1170
- if (!GROUP_ITEM_ALLOWED.has(itemType)) {
1171
- throw unsupportedContent(
1172
- "group",
1173
- `"${itemType}" items are not supported inside a group`
1174
- );
1175
- }
1659
+ var pollStream = (client, pollCache) => stream((emit, end) => {
1660
+ let active = client.polls.subscribe();
1661
+ let closed = false;
1662
+ let retryDelayMs = RECONNECT_INITIAL_DELAY_MS;
1663
+ let sleepTimer;
1664
+ let wakeSleep;
1665
+ const sleep = async (delayMs) => {
1666
+ if (closed) {
1667
+ return;
1176
1668
  }
1177
- const groupMembers = [];
1178
- for (const sub of content.items) {
1179
- groupMembers.push(await sendSingle(remote, chat, sub.content));
1669
+ await new Promise((resolve) => {
1670
+ wakeSleep = resolve;
1671
+ sleepTimer = setTimeout(resolve, pollRetryDelay(delayMs));
1672
+ });
1673
+ sleepTimer = void 0;
1674
+ wakeSleep = void 0;
1675
+ };
1676
+ const cancelSleep = () => {
1677
+ if (sleepTimer) {
1678
+ clearTimeout(sleepTimer);
1679
+ sleepTimer = void 0;
1180
1680
  }
1181
- const first = groupMembers[0];
1182
- if (!first) {
1183
- throw new Error("Empty group");
1681
+ wakeSleep?.();
1682
+ wakeSleep = void 0;
1683
+ };
1684
+ const pump = (async () => {
1685
+ while (!closed) {
1686
+ try {
1687
+ await runPollSubscription(client, pollCache, active, emit, () => {
1688
+ retryDelayMs = RECONNECT_INITIAL_DELAY_MS;
1689
+ });
1690
+ } catch (e) {
1691
+ if (!closed) {
1692
+ logPollStreamError(e);
1693
+ }
1694
+ } finally {
1695
+ await active.close();
1696
+ }
1697
+ if (!closed) {
1698
+ await sleep(retryDelayMs);
1699
+ retryDelayMs = Math.min(retryDelayMs * 2, RECONNECT_MAX_DELAY_MS);
1700
+ active = client.polls.subscribe();
1701
+ }
1184
1702
  }
1185
- return { ...first, groupMembers };
1186
- }
1187
- return sendSingle(remote, chat, content);
1703
+ end();
1704
+ })();
1705
+ return async () => {
1706
+ closed = true;
1707
+ cancelSleep();
1708
+ await active.close();
1709
+ await pump;
1710
+ };
1711
+ });
1712
+ var clientStream = (client, pollCache) => {
1713
+ return mergeStreams([messageStream(client), pollStream(client, pollCache)]);
1188
1714
  };
1189
- var replyToMessage = async (clients, spaceId, msgId, content) => {
1190
- const remote = clients[0];
1191
- if (!remote) {
1192
- throw new Error("No remote iMessage client available");
1193
- }
1194
- const chat = chatGuid(spaceId);
1195
- const replyTo = messageGuid(msgId);
1196
- switch (content.type) {
1197
- case "text":
1198
- return toSendResult(
1199
- await remote.messages.send(chat, content.text, { replyTo })
1200
- );
1201
- case "richlink":
1202
- return toSendResult(
1203
- await remote.messages.send(chat, content.url, {
1204
- richLink: true,
1205
- replyTo
1206
- })
1207
- );
1208
- case "attachment": {
1209
- const attachment = await remote.attachments.upload({
1210
- data: await content.read(),
1211
- fileName: content.name,
1212
- mimeType: content.mimeType
1213
- });
1214
- return toSendResult(
1215
- await remote.messages.send(chat, "", {
1216
- attachment: attachment.guid,
1217
- replyTo
1218
- })
1219
- );
1220
- }
1221
- case "contact": {
1222
- const attachment = await sendContactAttachment(remote, content);
1223
- return toSendResult(
1224
- await remote.messages.send(chat, "", { attachment, replyTo })
1225
- );
1226
- }
1227
- case "voice": {
1228
- const { buffer } = await ensureM4a(
1229
- await content.read(),
1230
- content.mimeType
1231
- );
1232
- const attachment = await remote.attachments.upload({
1233
- data: buffer,
1234
- fileName: content.name ?? "voice.m4a",
1235
- mimeType: "audio/x-m4a"
1236
- });
1237
- return toSendResult(
1238
- await remote.messages.send(chat, "", {
1239
- attachment: attachment.guid,
1240
- audioMessage: true,
1241
- replyTo
1242
- })
1243
- );
1244
- }
1245
- case "poll":
1246
- throw UnsupportedError.content(
1247
- "poll",
1248
- PLATFORM,
1249
- "polls cannot be sent as replies"
1250
- );
1251
- default:
1252
- throw unsupportedContent(content.type);
1253
- }
1715
+ var messages3 = (clients) => {
1716
+ const pollCache = getPollCache(clients);
1717
+ return mergeStreams(clients.map((client) => clientStream(client, pollCache)));
1254
1718
  };
1255
- var editMessage = async (clients, spaceId, msgId, content) => {
1256
- if (content.type !== "text") {
1257
- throw UnsupportedError.content(
1258
- content.type,
1259
- PLATFORM,
1260
- "only text content can be edited"
1261
- );
1262
- }
1263
- const remote = clients[0];
1719
+
1720
+ // src/providers/imessage/remote/typing.ts
1721
+ import { chatGuid as chatGuid3 } from "@photon-ai/advanced-imessage";
1722
+ var startTyping = async (remote, spaceId) => {
1723
+ await remote.chats.startTyping(chatGuid3(spaceId));
1724
+ };
1725
+ var stopTyping = async (remote, spaceId) => {
1726
+ await remote.chats.stopTyping(chatGuid3(spaceId));
1727
+ };
1728
+
1729
+ // src/providers/imessage/remote/api.ts
1730
+ var messages4 = (clients) => messages3(clients);
1731
+ var startTyping2 = async (clients, spaceId) => {
1732
+ const remote = firstRemoteClient(clients);
1264
1733
  if (!remote) {
1265
- throw new Error("No remote iMessage client available");
1734
+ return;
1266
1735
  }
1267
- await remote.messages.edit(
1268
- chatGuid(spaceId),
1269
- messageGuid(msgId),
1270
- content.text
1271
- );
1736
+ await startTyping(remote, spaceId);
1272
1737
  };
1273
- var reactToMessage = async (clients, spaceId, target, reaction) => {
1274
- const remote = clients[0];
1738
+ var stopTyping2 = async (clients, spaceId) => {
1739
+ const remote = firstRemoteClient(clients);
1275
1740
  if (!remote) {
1276
1741
  return;
1277
1742
  }
1278
- const chat = chatGuid(spaceId);
1279
- const parentGuid = target.parentId ?? target.id;
1280
- const guid = messageGuid(parentGuid);
1281
- const opts = typeof target.partIndex === "number" ? { partIndex: target.partIndex } : void 0;
1282
- const native = EMOJI_TO_TAPBACK[reaction];
1283
- if (native) {
1284
- await remote.messages.react(chat, guid, native, opts);
1285
- } else {
1286
- await remote.messages.reactEmoji(chat, guid, reaction, opts);
1287
- }
1743
+ await stopTyping(remote, spaceId);
1288
1744
  };
1289
- var getMessage2 = async (clients, spaceId, msgId) => {
1290
- const remote = clients[0];
1745
+ var send4 = async (clients, spaceId, content) => send3(primaryRemoteClient(clients), spaceId, content);
1746
+ var replyToMessage2 = async (clients, spaceId, msgId, content) => replyToMessage(primaryRemoteClient(clients), spaceId, msgId, content);
1747
+ var editMessage2 = async (clients, spaceId, msgId, content) => editMessage(primaryRemoteClient(clients), spaceId, msgId, content);
1748
+ var reactToMessage2 = async (clients, spaceId, target, reaction) => {
1749
+ const remote = firstRemoteClient(clients);
1291
1750
  if (!remote) {
1292
1751
  return;
1293
1752
  }
1294
- const cache = getMessageCache(remote);
1295
- const cached = cache.get(msgId);
1296
- if (cached) {
1297
- return cached;
1298
- }
1299
- const childRef = parseChildId(msgId);
1300
- if (childRef) {
1301
- try {
1302
- const fetched = await remote.messages.get(
1303
- messageGuid(childRef.parentGuid)
1304
- );
1305
- const parent = await rebuildFromAppleMessage(remote, fetched, spaceId);
1306
- cacheMessage(cache, parent);
1307
- if (parent.content.type !== "group") {
1308
- return;
1309
- }
1310
- const items = parent.content.items;
1311
- return items[childRef.partIndex];
1312
- } catch {
1313
- return;
1314
- }
1315
- }
1316
- try {
1317
- const fetched = await remote.messages.get(messageGuid(msgId));
1318
- const rebuilt = await rebuildFromAppleMessage(remote, fetched, spaceId);
1319
- cacheMessage(cache, rebuilt);
1320
- return rebuilt;
1321
- } catch {
1753
+ await reactToMessage(remote, spaceId, target, reaction);
1754
+ };
1755
+ var getMessage4 = async (clients, spaceId, msgId) => {
1756
+ const remote = firstRemoteClient(clients);
1757
+ if (!remote) {
1322
1758
  return;
1323
1759
  }
1760
+ return getMessage3(remote, spaceId, msgId);
1324
1761
  };
1325
1762
 
1326
1763
  // src/providers/imessage/types.ts
@@ -1346,6 +1783,7 @@ var messageSchema = z.object({
1346
1783
  });
1347
1784
 
1348
1785
  // src/providers/imessage/index.ts
1786
+ var isPollContent = (content) => content.type === "poll" || content.type === "poll_option";
1349
1787
  var imessage = definePlatform("iMessage", {
1350
1788
  config: configSchema,
1351
1789
  user: {
@@ -1414,55 +1852,69 @@ var imessage = definePlatform("iMessage", {
1414
1852
  }
1415
1853
  },
1416
1854
  events: {
1417
- messages: ({ client }) => isLocal(client) ? messages(client) : messages2(client)
1855
+ messages: ({ client }) => isLocal(client) ? messages2(client) : messages4(client)
1418
1856
  },
1419
1857
  actions: {
1420
1858
  send: async ({ space, content, client }) => {
1421
1859
  if (isLocal(client)) {
1422
- return await send(client, space.id, content);
1860
+ return await send2(client, space.id, content);
1423
1861
  }
1424
- return await send2(client, space.id, content);
1862
+ return await send4(client, space.id, content);
1425
1863
  },
1426
1864
  startTyping: async ({ space, client }) => {
1427
1865
  if (isLocal(client)) {
1428
1866
  return;
1429
1867
  }
1430
- await startTyping(client, space.id);
1868
+ await startTyping2(client, space.id);
1431
1869
  },
1432
1870
  stopTyping: async ({ space, client }) => {
1433
1871
  if (isLocal(client)) {
1434
1872
  return;
1435
1873
  }
1436
- await stopTyping(client, space.id);
1874
+ await stopTyping2(client, space.id);
1437
1875
  },
1438
1876
  reactToMessage: async ({ space, target, reaction, client }) => {
1439
1877
  if (isLocal(client)) {
1440
1878
  throw UnsupportedError.action("react", "iMessage (local mode)");
1441
1879
  }
1442
- await reactToMessage(
1880
+ if (isPollContent(target.content)) {
1881
+ throw UnsupportedError.action(
1882
+ "react",
1883
+ "iMessage",
1884
+ "iMessage polls do not support reactions"
1885
+ );
1886
+ }
1887
+ await reactToMessage2(
1443
1888
  client,
1444
1889
  space.id,
1445
1890
  target,
1446
1891
  reaction
1447
1892
  );
1448
1893
  },
1449
- replyToMessage: async ({ space, messageId, content, client }) => {
1894
+ replyToMessage: async ({ space, messageId, target, content, client }) => {
1450
1895
  if (isLocal(client)) {
1451
1896
  throw UnsupportedError.action("reply", "iMessage (local mode)");
1452
1897
  }
1453
- return await replyToMessage(client, space.id, messageId, content);
1898
+ if (isPollContent(target.content)) {
1899
+ throw UnsupportedError.action(
1900
+ "reply",
1901
+ "iMessage",
1902
+ "iMessage polls do not support replies"
1903
+ );
1904
+ }
1905
+ return await replyToMessage2(client, space.id, messageId, content);
1454
1906
  },
1455
1907
  editMessage: async ({ space, messageId, content, client }) => {
1456
1908
  if (isLocal(client)) {
1457
1909
  throw UnsupportedError.action("edit", "iMessage (local mode)");
1458
1910
  }
1459
- await editMessage(client, space.id, messageId, content);
1911
+ await editMessage2(client, space.id, messageId, content);
1460
1912
  },
1461
1913
  getMessage: async ({ space, messageId, client }) => {
1462
1914
  if (isLocal(client)) {
1463
- return getMessage(client, messageId);
1915
+ return getMessage2(client, messageId);
1464
1916
  }
1465
- return getMessage2(client, space.id, messageId);
1917
+ return getMessage4(client, space.id, messageId);
1466
1918
  }
1467
1919
  }
1468
1920
  });