spectrum-ts 1.1.0 → 1.1.1

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,7 +1,7 @@
1
1
  import {
2
- asGroup,
3
- asRichlink
4
- } from "../../chunk-LAGNM6I7.js";
2
+ asRichlink,
3
+ groupSchema
4
+ } from "../../chunk-TY3RT4OB.js";
5
5
  import {
6
6
  asPoll,
7
7
  asPollOption,
@@ -14,12 +14,12 @@ import {
14
14
  asAttachment,
15
15
  asContact,
16
16
  asCustom,
17
- asReaction,
18
17
  asText,
19
18
  definePlatform,
20
19
  fromVCard,
20
+ reactionSchema,
21
21
  toVCard
22
- } from "../../chunk-XMAI2AAN.js";
22
+ } from "../../chunk-7D6FHYKT.js";
23
23
 
24
24
  // src/providers/imessage/index.ts
25
25
  import { createClient as createClient2, directChat } from "@photon-ai/advanced-imessage";
@@ -111,17 +111,12 @@ async function disposeCloudAuth(clients) {
111
111
  }
112
112
  }
113
113
 
114
- // src/providers/imessage/local.ts
114
+ // src/providers/imessage/local/attachments.ts
115
115
  import { createReadStream } from "fs";
116
- import { mkdtemp, readFile, rm, writeFile } from "fs/promises";
117
- import { tmpdir } from "os";
118
- import { basename, join } from "path";
116
+ import { readFile } from "fs/promises";
119
117
  import { Readable } from "stream";
120
- var synthSendResult = () => ({
121
- id: crypto.randomUUID(),
122
- timestamp: /* @__PURE__ */ new Date()
123
- });
124
- var DEFAULT_ATTACHMENT_NAME = "attachment";
118
+
119
+ // src/providers/imessage/shared/vcard.ts
125
120
  var VCARD_MIME_TYPES = /* @__PURE__ */ new Set([
126
121
  "text/vcard",
127
122
  "text/x-vcard",
@@ -136,6 +131,13 @@ var isVCardAttachment = (mimeType, fileName) => {
136
131
  }
137
132
  return Boolean(fileName?.toLowerCase().endsWith(".vcf"));
138
133
  };
134
+ var vcardFileName = (contact) => {
135
+ const base = contact.name?.formatted ?? contact.user?.id ?? "contact";
136
+ return `${base.replace(/[^a-zA-Z0-9_\-.]/g, "_")}.vcf`;
137
+ };
138
+
139
+ // src/providers/imessage/local/attachments.ts
140
+ var DEFAULT_ATTACHMENT_NAME = "attachment";
139
141
  var readLocalAttachment = async (att) => {
140
142
  if (!att.localPath) {
141
143
  throw new Error(
@@ -164,6 +166,9 @@ var toVCardContent = async (att) => {
164
166
  return toAttachmentContent(att);
165
167
  }
166
168
  };
169
+ var localAttachmentContent = async (att) => isVCardAttachment(att.mimeType, att.fileName) ? await toVCardContent(att) : toAttachmentContent(att);
170
+
171
+ // src/providers/imessage/local/inbound.ts
167
172
  var toMessages = async (message) => {
168
173
  const { chatId, chatKind } = message;
169
174
  if (!chatId || chatKind === "unknown") {
@@ -182,7 +187,7 @@ var toMessages = async (message) => {
182
187
  message.attachments.map(async (att) => ({
183
188
  ...base,
184
189
  id: `${message.id}:${att.id}`,
185
- content: isVCardAttachment(att.mimeType, att.fileName) ? await toVCardContent(att) : toAttachmentContent(att)
190
+ content: await localAttachmentContent(att)
186
191
  }))
187
192
  );
188
193
  }
@@ -214,10 +219,23 @@ var messages = (client) => stream((emit, end) => {
214
219
  });
215
220
  };
216
221
  });
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
- };
222
+
223
+ // src/providers/imessage/local/send.ts
224
+ import { mkdtemp, rm, writeFile } from "fs/promises";
225
+ import { tmpdir } from "os";
226
+ import { basename, join } from "path";
227
+
228
+ // src/providers/imessage/shared/errors.ts
229
+ var IMESSAGE_PLATFORM = "iMessage";
230
+ var LOCAL_IMESSAGE_PLATFORM = "iMessage (local mode)";
231
+ var unsupportedRemoteContent = (type, detail) => UnsupportedError.content(type, IMESSAGE_PLATFORM, detail);
232
+ var unsupportedLocalContent = (type) => UnsupportedError.content(type, LOCAL_IMESSAGE_PLATFORM);
233
+
234
+ // src/providers/imessage/local/send.ts
235
+ var synthSendResult = () => ({
236
+ id: crypto.randomUUID(),
237
+ timestamp: /* @__PURE__ */ new Date()
238
+ });
221
239
  var sendTempFile = async (client, spaceId, name, data) => {
222
240
  const safeName = basename(name) || DEFAULT_ATTACHMENT_NAME;
223
241
  const dir = await mkdtemp(join(tmpdir(), "spectrum-"));
@@ -249,143 +267,35 @@ var send = async (client, spaceId, content) => {
249
267
  return synthSendResult();
250
268
  }
251
269
  case "poll":
252
- throw UnsupportedError.content("poll", "iMessage (local mode)");
270
+ throw unsupportedLocalContent("poll");
253
271
  default:
254
- throw UnsupportedError.content(content.type, "iMessage (local mode)");
272
+ throw unsupportedLocalContent(content.type);
255
273
  }
256
274
  };
257
275
  var getMessage = async (_client, _id) => void 0;
258
276
 
259
- // src/providers/imessage/remote.ts
260
- import {
261
- chatGuid,
262
- messageGuid,
263
- Reaction
264
- } from "@photon-ai/advanced-imessage";
277
+ // src/providers/imessage/local/api.ts
278
+ var messages2 = (client) => messages(client);
279
+ var send2 = async (client, spaceId, content) => send(client, spaceId, content);
280
+ var getMessage2 = async (client, id) => getMessage(client, id);
265
281
 
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 };
282
+ // src/providers/imessage/remote/client.ts
283
+ var REMOTE_CLIENT_MISSING = "No remote iMessage client available";
284
+ var firstRemoteClient = (clients) => clients[0];
285
+ var primaryRemoteClient = (clients) => {
286
+ const remote = firstRemoteClient(clients);
287
+ if (!remote) {
288
+ throw new Error(REMOTE_CLIENT_MISSING);
385
289
  }
386
- return transcodeToM4a(buffer);
290
+ return remote;
387
291
  };
388
292
 
293
+ // src/providers/imessage/remote/inbound.ts
294
+ import {
295
+ messageGuid,
296
+ NotFoundError
297
+ } from "@photon-ai/advanced-imessage";
298
+
389
299
  // src/providers/imessage/cache.ts
390
300
  var DEFAULT_MAX = 1e3;
391
301
  var MessageCache = class {
@@ -509,73 +419,28 @@ var getPollCache = (owner) => {
509
419
  return cache;
510
420
  };
511
421
 
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
422
+ // src/providers/imessage/remote/ids.ts
423
+ var PART_PREFIX = /^p:(\d+)\//;
424
+ var formatChildId = (partIndex, parentGuid) => `p:${partIndex}/${parentGuid}`;
425
+ var parseTapbackTarget = (target) => {
426
+ const match = target.match(PART_PREFIX);
427
+ const guid = target.replace(PART_PREFIX, "");
428
+ const partIndex = match ? Number(match[1]) : 0;
429
+ return { guid, partIndex };
558
430
  };
559
- var isTapbackRemoval = (code) => code.startsWith("3");
560
- var resolveReactionEmoji = (type, emoji) => {
561
- if (emoji) {
562
- return emoji;
563
- }
564
- if (!type) {
431
+ var parseChildId = (id) => {
432
+ const match = id.match(PART_PREFIX);
433
+ if (!match) {
565
434
  return null;
566
435
  }
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;
436
+ return {
437
+ parentGuid: id.replace(PART_PREFIX, ""),
438
+ partIndex: Number(match[1])
439
+ };
578
440
  };
441
+
442
+ // src/providers/imessage/remote/inbound.ts
443
+ var URL_BALLOON_BUNDLE_ID = "com.apple.messages.URLBalloonProvider";
579
444
  var getBalloonBundleId = (message) => {
580
445
  const raw = message._raw;
581
446
  const id = raw?.balloonBundleId;
@@ -589,6 +454,31 @@ var resolveChatGuid = (message, hint) => {
589
454
  return first ?? "";
590
455
  };
591
456
  var resolveSenderId = (message) => message.sender?.address ?? "";
457
+ var isIMessageMessage = (value) => {
458
+ if (typeof value !== "object" || value === null) {
459
+ return false;
460
+ }
461
+ const record = value;
462
+ 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;
463
+ };
464
+ var asProviderGroup = (items) => groupSchema.parse({ type: "group", items });
465
+ var buildMessageBase = (message, chatGuidHint, timestamp) => {
466
+ const chat = resolveChatGuid(message, chatGuidHint);
467
+ return {
468
+ sender: { id: resolveSenderId(message) },
469
+ space: {
470
+ id: chat,
471
+ type: chat.includes(";+;") ? "group" : "dm"
472
+ },
473
+ timestamp
474
+ };
475
+ };
476
+ var receivedEventFromMessage = (message) => ({
477
+ chatGuid: resolveChatGuid(message, void 0),
478
+ message,
479
+ timestamp: message.dateCreated ?? /* @__PURE__ */ new Date(),
480
+ type: "message.received"
481
+ });
592
482
  var toAttachmentContent2 = (client, info) => asAttachment({
593
483
  name: info.fileName,
594
484
  mimeType: info.mimeType,
@@ -600,22 +490,15 @@ var toVCardContent2 = async (client, info) => {
600
490
  try {
601
491
  const buf = Buffer.from(await client.attachments.downloadBuffer(info.guid));
602
492
  return asContact(fromVCard(buf.toString("utf8")));
603
- } catch {
493
+ } catch (err) {
494
+ console.warn(
495
+ "[spectrum-ts][imessage] failed to parse vCard attachment; falling back to attachment content",
496
+ { error: err, guid: info.guid }
497
+ );
604
498
  return toAttachmentContent2(client, info);
605
499
  }
606
500
  };
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
- };
501
+ var attachmentContent = async (client, info) => isVCardAttachment(info.mimeType, info.fileName) ? await toVCardContent2(client, info) : toAttachmentContent2(client, info);
619
502
  var buildAttachmentMessage = async (client, base, info, id, partIndex, parentId) => {
620
503
  const content = await attachmentContent(client, info);
621
504
  const msg = { ...base, id, content, partIndex };
@@ -624,11 +507,27 @@ var buildAttachmentMessage = async (client, base, info, id, partIndex, parentId)
624
507
  }
625
508
  return msg;
626
509
  };
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) {
510
+ var toRichlinkMessage = (message, base, id) => {
511
+ const url = message.text ?? "";
512
+ try {
513
+ return { ...base, id, content: asRichlink({ url }) };
514
+ } catch (err) {
515
+ console.warn(
516
+ "[spectrum-ts][imessage] failed to convert message to rich link; falling back to text/custom content",
517
+ { error: err, message, url }
518
+ );
519
+ return {
520
+ ...base,
521
+ id,
522
+ content: url ? asText(url) : asCustom(message)
523
+ };
524
+ }
525
+ };
526
+ var rebuildFromAppleMessage = async (client, message, chatGuidHint) => {
527
+ const messageGuidStr = message.guid;
528
+ const timestamp = message.dateCreated ?? /* @__PURE__ */ new Date();
529
+ const base = buildMessageBase(message, chatGuidHint, timestamp);
530
+ if (message.attachments.length === 1) {
632
531
  const info = message.attachments[0];
633
532
  if (!info) {
634
533
  throw new Error("Unreachable: attachments.length === 1 but no element");
@@ -656,22 +555,13 @@ var rebuildFromAppleMessage = async (client, message, chatGuidHint) => {
656
555
  return {
657
556
  ...base,
658
557
  id: messageGuidStr,
659
- content: asGroup({ items })
558
+ content: asProviderGroup(items)
660
559
  };
661
560
  }
662
- const text = message.text;
663
561
  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
- }
562
+ return toRichlinkMessage(message, base, messageGuidStr);
674
563
  }
564
+ const text = message.text;
675
565
  return {
676
566
  ...base,
677
567
  id: messageGuidStr,
@@ -682,45 +572,166 @@ var cacheMessage = (cache, message) => {
682
572
  cache.set(message.id, message);
683
573
  if (message.content.type === "group") {
684
574
  for (const item of message.content.items) {
685
- cache.set(item.id, item);
575
+ if (isIMessageMessage(item)) {
576
+ cache.set(item.id, item);
577
+ }
686
578
  }
687
579
  }
688
580
  };
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 {
581
+ var toInboundMessages = async (client, cache, event) => {
582
+ const base = buildMessageBase(event.message, event.chatGuid, event.timestamp);
583
+ const messageGuidStr = event.message.guid;
584
+ if (getBalloonBundleId(event.message) === URL_BALLOON_BUNDLE_ID) {
585
+ const msg2 = toRichlinkMessage(event.message, base, messageGuidStr);
586
+ cacheMessage(cache, msg2);
587
+ return [msg2];
588
+ }
589
+ if (event.message.attachments.length === 1) {
590
+ const info = event.message.attachments[0];
591
+ if (!info) {
592
+ throw new Error("Unreachable: attachments.length === 1 but no element");
593
+ }
594
+ const msg2 = await buildAttachmentMessage(
595
+ client,
596
+ base,
597
+ info,
598
+ messageGuidStr,
599
+ 0
600
+ );
601
+ cacheMessage(cache, msg2);
602
+ return [msg2];
603
+ }
604
+ if (event.message.attachments.length > 1) {
605
+ const items = [];
606
+ for (let i = 0; i < event.message.attachments.length; i++) {
607
+ const info = event.message.attachments[i];
608
+ if (!info) {
609
+ continue;
610
+ }
611
+ items.push(
612
+ await buildAttachmentMessage(
613
+ client,
614
+ base,
615
+ info,
616
+ formatChildId(i, messageGuidStr),
617
+ i,
618
+ messageGuidStr
619
+ )
620
+ );
621
+ }
622
+ const parent = {
695
623
  ...base,
696
- id,
697
- content: url ? asText(url) : asCustom(event.message)
624
+ id: messageGuidStr,
625
+ content: asProviderGroup(items)
698
626
  };
627
+ cacheMessage(cache, parent);
628
+ return [parent];
699
629
  }
630
+ const text = event.message.text;
631
+ const msg = {
632
+ ...base,
633
+ id: messageGuidStr,
634
+ content: text ? asText(text) : asCustom(event.message)
635
+ };
636
+ cacheMessage(cache, msg);
637
+ return [msg];
700
638
  };
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 };
639
+ var getMessage3 = async (remote, spaceId, msgId) => {
640
+ const cache = getMessageCache(remote);
641
+ const cached = cache.get(msgId);
642
+ if (cached) {
643
+ return cached;
644
+ }
645
+ const childRef = parseChildId(msgId);
646
+ if (childRef) {
647
+ try {
648
+ const fetched = await remote.messages.get(
649
+ messageGuid(childRef.parentGuid)
650
+ );
651
+ const parent = await rebuildFromAppleMessage(remote, fetched, spaceId);
652
+ cacheMessage(cache, parent);
653
+ if (parent.content.type !== "group") {
654
+ return;
655
+ }
656
+ const item = parent.content.items[childRef.partIndex];
657
+ return isIMessageMessage(item) ? item : void 0;
658
+ } catch (err) {
659
+ if (err instanceof NotFoundError) {
660
+ return;
661
+ }
662
+ throw err;
663
+ }
664
+ }
665
+ try {
666
+ const fetched = await remote.messages.get(messageGuid(msgId));
667
+ const rebuilt = await rebuildFromAppleMessage(remote, fetched, spaceId);
668
+ cacheMessage(cache, rebuilt);
669
+ return rebuilt;
670
+ } catch (err) {
671
+ if (err instanceof NotFoundError) {
672
+ return;
673
+ }
674
+ throw err;
675
+ }
708
676
  };
709
- var parseChildId = (id) => {
710
- const match = id.match(PART_PREFIX);
711
- if (!match) {
677
+
678
+ // src/providers/imessage/remote/reactions.ts
679
+ import {
680
+ chatGuid,
681
+ messageGuid as messageGuid2,
682
+ Reaction
683
+ } from "@photon-ai/advanced-imessage";
684
+ var EMOJI_TO_TAPBACK = {
685
+ "\u2764\uFE0F": Reaction.love,
686
+ "\u{1F44D}": Reaction.like,
687
+ "\u{1F44E}": Reaction.dislike,
688
+ "\u{1F602}": Reaction.laugh,
689
+ "\u203C\uFE0F": Reaction.emphasize,
690
+ "\u2753": Reaction.question
691
+ };
692
+ var TAPBACK_TO_EMOJI = Object.fromEntries(
693
+ Object.entries(EMOJI_TO_TAPBACK).map(([emoji, kind]) => [kind, emoji])
694
+ );
695
+ var TAPBACK_CODE_TO_KIND = {
696
+ "2000": Reaction.love,
697
+ "2001": Reaction.like,
698
+ "2002": Reaction.dislike,
699
+ "2003": Reaction.laugh,
700
+ "2004": Reaction.emphasize,
701
+ "2005": Reaction.question,
702
+ "2006": Reaction.emoji,
703
+ "2007": Reaction.sticker
704
+ };
705
+ var isTapbackRemoval = (code) => code.startsWith("3");
706
+ var resolveReactionEmoji = (type, emoji) => {
707
+ if (emoji) {
708
+ return emoji;
709
+ }
710
+ if (!type) {
712
711
  return null;
713
712
  }
714
- return {
715
- parentGuid: id.replace(PART_PREFIX, ""),
716
- partIndex: Number(match[1])
717
- };
713
+ const kind = TAPBACK_CODE_TO_KIND[type] ?? type;
714
+ return TAPBACK_TO_EMOJI[kind] ?? null;
718
715
  };
716
+ var getAssociatedMessageType = (message) => {
717
+ const direct = message.associatedMessageType;
718
+ if (typeof direct === "string") {
719
+ return direct;
720
+ }
721
+ const raw = message._raw;
722
+ const fromRaw = raw?.associatedMessageType;
723
+ return typeof fromRaw === "string" ? fromRaw : void 0;
724
+ };
725
+ var asProviderReaction = (emoji, target) => reactionSchema.parse({
726
+ emoji,
727
+ target,
728
+ type: "reaction"
729
+ });
719
730
  var resolveReactionTarget = async (client, cache, strippedGuid, partIndex) => {
720
731
  let candidate = cache.get(strippedGuid);
721
732
  if (!candidate) {
722
733
  try {
723
- const fetched = await client.messages.get(messageGuid(strippedGuid));
734
+ const fetched = await client.messages.get(messageGuid2(strippedGuid));
724
735
  candidate = await rebuildFromAppleMessage(client, fetched);
725
736
  cacheMessage(cache, candidate);
726
737
  } catch {
@@ -728,12 +739,16 @@ var resolveReactionTarget = async (client, cache, strippedGuid, partIndex) => {
728
739
  }
729
740
  }
730
741
  if (candidate.content.type === "group") {
731
- const item = candidate.content.items[partIndex];
732
- return item ?? candidate;
742
+ const items = candidate.content.items;
743
+ if (!Array.isArray(items)) {
744
+ return candidate;
745
+ }
746
+ const item = items[partIndex];
747
+ return isIMessageMessage(item) ? item : candidate;
733
748
  }
734
749
  return candidate;
735
750
  };
736
- var toReactionMessage = async (client, cache, event, base, id, target) => {
751
+ var toReactionMessages = async (client, cache, event, target) => {
737
752
  const type = getAssociatedMessageType(event.message);
738
753
  if (type && isTapbackRemoval(type)) {
739
754
  return [];
@@ -755,76 +770,567 @@ var toReactionMessage = async (client, cache, event, base, id, target) => {
755
770
  if (!resolved) {
756
771
  return [];
757
772
  }
773
+ const messageId = event.message.guid;
774
+ if (typeof messageId !== "string" || messageId.length === 0) {
775
+ return [];
776
+ }
777
+ const base = buildMessageBase(event.message, event.chatGuid, event.timestamp);
758
778
  return [
759
779
  {
760
780
  ...base,
761
- id,
762
- content: asReaction({ emoji, target: resolved })
781
+ id: messageId,
782
+ content: asProviderReaction(emoji, resolved)
763
783
  }
764
784
  ];
765
785
  };
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);
786
+ var reactToMessage = async (remote, spaceId, target, reaction) => {
787
+ const chat = chatGuid(spaceId);
788
+ const parentGuid = target.parentId ?? target.id;
789
+ const guid = messageGuid2(parentGuid);
790
+ const opts = typeof target.partIndex === "number" ? { partIndex: target.partIndex } : void 0;
791
+ const native = EMOJI_TO_TAPBACK[reaction];
792
+ if (native) {
793
+ await remote.messages.react(chat, guid, native, opts);
794
+ } else {
795
+ await remote.messages.reactEmoji(chat, guid, reaction, opts);
772
796
  }
773
- if (getBalloonBundleId(event.message) === URL_BALLOON_BUNDLE_ID) {
774
- const msg2 = toRichlinkMessage(event, base, messageGuidStr);
775
- cacheMessage(cache, msg2);
776
- return [msg2];
797
+ };
798
+
799
+ // src/providers/imessage/remote/send.ts
800
+ import {
801
+ chatGuid as chatGuid2,
802
+ messageGuid as messageGuid3
803
+ } from "@photon-ai/advanced-imessage";
804
+
805
+ // src/utils/audio.ts
806
+ import { spawn } from "child_process";
807
+ import { mkdtemp as mkdtemp2, readFile as readFile2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
808
+ import { tmpdir as tmpdir2 } from "os";
809
+ import { join as join2 } from "path";
810
+ var M4A_BRANDS = /* @__PURE__ */ new Set([
811
+ "M4A ",
812
+ "M4B ",
813
+ "M4P ",
814
+ "mp42",
815
+ "mp41",
816
+ "isom",
817
+ "iso2"
818
+ ]);
819
+ var M4A_MIME_TYPES = /* @__PURE__ */ new Set([
820
+ "audio/mp4",
821
+ "audio/mp4a-latm",
822
+ "audio/x-m4a",
823
+ "audio/aac",
824
+ "audio/aacp"
825
+ ]);
826
+ var FFMPEG_MISSING_MESSAGE = "voice content: input is not m4a/aac and ffmpeg is unavailable. Install `ffmpeg-static` or ensure `ffmpeg` is on PATH.";
827
+ var isM4a = (buffer) => {
828
+ if (buffer.length < 12) {
829
+ return false;
777
830
  }
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");
831
+ if (buffer.toString("ascii", 4, 8) !== "ftyp") {
832
+ return false;
833
+ }
834
+ return M4A_BRANDS.has(buffer.toString("ascii", 8, 12));
835
+ };
836
+ var isM4aMimeType = (mimeType) => M4A_MIME_TYPES.has(mimeType.toLowerCase());
837
+ var cachedFfmpegPath;
838
+ var tryStaticBinary = async () => {
839
+ try {
840
+ const mod = await import("ffmpeg-static");
841
+ return mod.default ?? void 0;
842
+ } catch {
843
+ return void 0;
844
+ }
845
+ };
846
+ var resolveFfmpegPath = async () => {
847
+ if (cachedFfmpegPath) {
848
+ return cachedFfmpegPath;
849
+ }
850
+ cachedFfmpegPath = await tryStaticBinary() ?? "ffmpeg";
851
+ return cachedFfmpegPath;
852
+ };
853
+ var collectStream = (stream2) => {
854
+ if (!stream2) {
855
+ return Promise.resolve("");
856
+ }
857
+ return new Promise((resolve, reject) => {
858
+ const chunks = [];
859
+ stream2.on("data", (chunk) => {
860
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
861
+ });
862
+ stream2.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
863
+ stream2.on("error", reject);
864
+ });
865
+ };
866
+ var isMissingBinaryError = (err) => err?.code === "ENOENT";
867
+ var runFfmpeg = (ffmpegPath, args) => {
868
+ const proc = spawn(ffmpegPath, args, { stdio: ["ignore", "ignore", "pipe"] });
869
+ const stderr = collectStream(proc.stderr);
870
+ const exit = new Promise((resolve, reject) => {
871
+ proc.on(
872
+ "error",
873
+ (err) => reject(
874
+ isMissingBinaryError(err) ? new Error(FFMPEG_MISSING_MESSAGE) : err
875
+ )
876
+ );
877
+ proc.on("exit", (code) => resolve(code ?? -1));
878
+ });
879
+ return Promise.all([exit, stderr]).then(([code, text]) => ({
880
+ code,
881
+ stderr: text
882
+ }));
883
+ };
884
+ var DURATION_PATTERN = /Duration:\s*(\d+):(\d{2}):(\d{2})(?:\.(\d{1,3}))?/;
885
+ var parseDuration = (stderr) => {
886
+ const match = stderr.match(DURATION_PATTERN);
887
+ if (!match) {
888
+ return void 0;
889
+ }
890
+ const [, hh, mm, ss, frac] = match;
891
+ const seconds = Number(hh) * 3600 + Number(mm) * 60 + Number(ss) + Number(`0.${frac ?? 0}`);
892
+ return Number.isFinite(seconds) ? seconds : void 0;
893
+ };
894
+ var transcodeToM4a = async (buffer) => {
895
+ const ffmpeg = await resolveFfmpegPath();
896
+ const dir = await mkdtemp2(join2(tmpdir2(), "spectrum-voice-"));
897
+ const inPath = join2(dir, "in");
898
+ const outPath = join2(dir, "out.m4a");
899
+ try {
900
+ await writeFile2(inPath, buffer);
901
+ const { code, stderr } = await runFfmpeg(ffmpeg, [
902
+ "-y",
903
+ "-i",
904
+ inPath,
905
+ "-f",
906
+ "ipod",
907
+ "-c:a",
908
+ "aac",
909
+ outPath
910
+ ]);
911
+ if (code !== 0) {
912
+ throw new Error(`ffmpeg conversion failed (exit ${code}): ${stderr}`);
782
913
  }
783
- const msg2 = await buildAttachmentMessage(
784
- client,
785
- base,
786
- info,
787
- messageGuidStr,
788
- 0
914
+ const out = await readFile2(outPath);
915
+ return { buffer: out, duration: parseDuration(stderr) };
916
+ } finally {
917
+ await rm2(dir, { recursive: true, force: true }).catch(() => {
918
+ });
919
+ }
920
+ };
921
+ var ensureM4a = async (buffer, mimeType) => {
922
+ if (isM4aMimeType(mimeType) || isM4a(buffer)) {
923
+ return { buffer };
924
+ }
925
+ return transcodeToM4a(buffer);
926
+ };
927
+
928
+ // src/providers/imessage/remote/send.ts
929
+ var GROUP_ITEM_ALLOWED = /* @__PURE__ */ new Set([
930
+ "attachment",
931
+ "contact",
932
+ "voice"
933
+ ]);
934
+ var PartialGroupSendError = class extends Error {
935
+ cause;
936
+ groupMembers;
937
+ constructor(groupMembers, cause) {
938
+ super("iMessage group send failed after one or more items were sent");
939
+ this.name = "PartialGroupSendError";
940
+ this.cause = cause;
941
+ this.groupMembers = groupMembers;
942
+ }
943
+ };
944
+ var toDate = (value) => {
945
+ if (value instanceof Date) {
946
+ return value;
947
+ }
948
+ if (typeof value === "number" || typeof value === "string") {
949
+ const date = new Date(value);
950
+ return Number.isNaN(date.getTime()) ? void 0 : date;
951
+ }
952
+ };
953
+ var receiptTimestamp = (receipt) => toDate(receipt.timestamp) ?? toDate(receipt.date) ?? toDate(receipt.dateCreated) ?? /* @__PURE__ */ new Date();
954
+ var toSendResult = (receipt) => {
955
+ if (typeof receipt.guid !== "string" || receipt.guid.length === 0) {
956
+ throw new Error("iMessage send receipt is missing a message guid");
957
+ }
958
+ return {
959
+ id: receipt.guid,
960
+ timestamp: receiptTimestamp(receipt)
961
+ };
962
+ };
963
+ var withReply = (options, replyTo) => replyTo ? { ...options, replyTo } : options;
964
+ var replyOptions = (replyTo) => replyTo ? { replyTo } : void 0;
965
+ var sendVCardAttachment = (remote, name, vcf) => remote.attachments.upload({
966
+ data: Buffer.from(vcf, "utf8"),
967
+ fileName: name,
968
+ mimeType: "text/vcard"
969
+ });
970
+ var sendContactAttachment = async (remote, content) => {
971
+ const vcf = await toVCard(content);
972
+ const upload = await sendVCardAttachment(remote, vcardFileName(content), vcf);
973
+ return upload.guid;
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 attachment.guid;
982
+ };
983
+ var uploadVoice = async (remote, content) => {
984
+ const { buffer } = await ensureM4a(await content.read(), content.mimeType);
985
+ const attachment = await remote.attachments.upload({
986
+ data: buffer,
987
+ fileName: content.name ?? "voice.m4a",
988
+ mimeType: "audio/x-m4a"
989
+ });
990
+ return attachment.guid;
991
+ };
992
+ var sendContent = async (remote, chat, content, replyTo) => {
993
+ switch (content.type) {
994
+ case "text":
995
+ return toSendResult(
996
+ await remote.messages.send(chat, content.text, withReply({}, replyTo))
997
+ );
998
+ case "richlink":
999
+ return toSendResult(
1000
+ await remote.messages.send(
1001
+ chat,
1002
+ content.url,
1003
+ withReply({ richLink: true }, replyTo)
1004
+ )
1005
+ );
1006
+ case "attachment":
1007
+ return toSendResult(
1008
+ await remote.messages.send(chat, "", {
1009
+ attachment: await uploadAttachment(remote, content),
1010
+ ...replyOptions(replyTo)
1011
+ })
1012
+ );
1013
+ case "contact":
1014
+ return toSendResult(
1015
+ await remote.messages.send(chat, "", {
1016
+ attachment: await sendContactAttachment(remote, content),
1017
+ ...replyOptions(replyTo)
1018
+ })
1019
+ );
1020
+ case "voice":
1021
+ return toSendResult(
1022
+ await remote.messages.send(chat, "", {
1023
+ attachment: await uploadVoice(remote, content),
1024
+ audioMessage: true,
1025
+ ...replyOptions(replyTo)
1026
+ })
1027
+ );
1028
+ case "poll":
1029
+ if (replyTo) {
1030
+ throw unsupportedRemoteContent(
1031
+ "poll",
1032
+ "polls cannot be sent as replies"
1033
+ );
1034
+ }
1035
+ return toSendResult(
1036
+ await remote.polls.create(
1037
+ chat,
1038
+ content.title,
1039
+ content.options.map((option) => option.title)
1040
+ )
1041
+ );
1042
+ default:
1043
+ throw unsupportedRemoteContent(content.type);
1044
+ }
1045
+ };
1046
+ var validateGroupContent = (content) => {
1047
+ for (const sub of content.items) {
1048
+ const itemType = sub.content.type;
1049
+ if (!GROUP_ITEM_ALLOWED.has(itemType)) {
1050
+ throw unsupportedRemoteContent(
1051
+ "group",
1052
+ `"${itemType}" items are not supported inside a group`
1053
+ );
1054
+ }
1055
+ }
1056
+ };
1057
+ var send3 = async (remote, spaceId, content) => {
1058
+ const chat = chatGuid2(spaceId);
1059
+ if (content.type === "group") {
1060
+ validateGroupContent(content);
1061
+ const groupMembers = [];
1062
+ try {
1063
+ for (const sub of content.items) {
1064
+ groupMembers.push(await sendContent(remote, chat, sub.content));
1065
+ }
1066
+ } catch (err) {
1067
+ throw new PartialGroupSendError(groupMembers, err);
1068
+ }
1069
+ const first = groupMembers[0];
1070
+ if (!first) {
1071
+ throw new Error("Empty group");
1072
+ }
1073
+ return { ...first, groupMembers };
1074
+ }
1075
+ return sendContent(remote, chat, content);
1076
+ };
1077
+ var replyToMessage = async (remote, spaceId, msgId, content) => {
1078
+ const chat = chatGuid2(spaceId);
1079
+ const replyTo = messageGuid3(msgId);
1080
+ return sendContent(remote, chat, content, replyTo);
1081
+ };
1082
+ var editMessage = async (remote, spaceId, msgId, content) => {
1083
+ if (content.type !== "text") {
1084
+ throw unsupportedRemoteContent(
1085
+ content.type,
1086
+ "only text content can be edited"
789
1087
  );
790
- cacheMessage(cache, msg2);
791
- return [msg2];
792
1088
  }
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;
1089
+ await remote.messages.edit(
1090
+ chatGuid2(spaceId),
1091
+ messageGuid3(msgId),
1092
+ content.text
1093
+ );
1094
+ };
1095
+
1096
+ // src/providers/imessage/remote/stream.ts
1097
+ import {
1098
+ AuthenticationError,
1099
+ IMessageError,
1100
+ NotFoundError as NotFoundError2,
1101
+ ValidationError
1102
+ } from "@photon-ai/advanced-imessage";
1103
+
1104
+ // src/utils/resumable-stream.ts
1105
+ var CATCH_UP_PAGE_SIZE = 100;
1106
+ var MAX_BUFFERED_LIVE_EVENTS = 1e3;
1107
+ var RECONNECT_INITIAL_DELAY_MS = 500;
1108
+ var RECONNECT_MAX_DELAY_MS = 3e4;
1109
+ var RetryableStreamError = class extends Error {
1110
+ constructor(message) {
1111
+ super(message);
1112
+ this.name = "RetryableStreamError";
1113
+ }
1114
+ };
1115
+ var LiveBufferOverflowError = class extends RetryableStreamError {
1116
+ constructor(limit) {
1117
+ super(`Live stream buffer exceeded ${limit} events during catch-up`);
1118
+ this.name = "LiveBufferOverflowError";
1119
+ }
1120
+ };
1121
+ var closeIterable = async (iterable) => {
1122
+ if (!iterable) {
1123
+ return;
1124
+ }
1125
+ await iterable.close?.();
1126
+ };
1127
+ var jitterDelay = (delayMs) => Math.random() * delayMs;
1128
+ var resumableOrderedStream = (options) => stream((emit, end) => {
1129
+ const catchUpPageSize = options.catchUpPageSize ?? CATCH_UP_PAGE_SIZE;
1130
+ const bufferLimit = options.bufferLimit ?? MAX_BUFFERED_LIVE_EVENTS;
1131
+ const initialRetryDelayMs = options.initialRetryDelayMs ?? RECONNECT_INITIAL_DELAY_MS;
1132
+ const maxRetryDelayMs = options.maxRetryDelayMs ?? RECONNECT_MAX_DELAY_MS;
1133
+ let activeLive;
1134
+ let closed = false;
1135
+ let lastCursor;
1136
+ let retryDelayMs = initialRetryDelayMs;
1137
+ let sleepTimer;
1138
+ let wakeSleep;
1139
+ const deliveredSinceCursor = /* @__PURE__ */ new Set();
1140
+ const resetRetryDelay = () => {
1141
+ retryDelayMs = initialRetryDelayMs;
1142
+ };
1143
+ const advanceCursor = (cursor, clearDelivered) => {
1144
+ if (!cursor || cursor === lastCursor) {
1145
+ return;
1146
+ }
1147
+ lastCursor = cursor;
1148
+ if (clearDelivered) {
1149
+ deliveredSinceCursor.clear();
1150
+ }
1151
+ };
1152
+ const deliverItem = async (item, resetRetry, clearOnCursorAdvance) => {
1153
+ const alreadyDelivered = deliveredSinceCursor.has(item.id);
1154
+ if (!alreadyDelivered) {
1155
+ for (const value of item.values) {
1156
+ await emit(value);
1157
+ }
1158
+ }
1159
+ advanceCursor(item.cursor, clearOnCursorAdvance);
1160
+ deliveredSinceCursor.add(item.id);
1161
+ if (resetRetry) {
1162
+ resetRetryDelay();
1163
+ }
1164
+ };
1165
+ const retryable = (error) => error instanceof RetryableStreamError || options.isRetryableError(error);
1166
+ const sleep = async (delayMs) => {
1167
+ if (delayMs <= 0 || closed) {
1168
+ return;
1169
+ }
1170
+ await new Promise((resolve) => {
1171
+ wakeSleep = resolve;
1172
+ sleepTimer = setTimeout(resolve, jitterDelay(delayMs));
1173
+ });
1174
+ sleepTimer = void 0;
1175
+ wakeSleep = void 0;
1176
+ };
1177
+ const cancelSleep = () => {
1178
+ if (sleepTimer) {
1179
+ clearTimeout(sleepTimer);
1180
+ sleepTimer = void 0;
1181
+ }
1182
+ wakeSleep?.();
1183
+ wakeSleep = void 0;
1184
+ };
1185
+ const nextRetryDelay = () => {
1186
+ const delay = retryDelayMs;
1187
+ retryDelayMs = Math.min(retryDelayMs * 2, maxRetryDelayMs);
1188
+ return delay;
1189
+ };
1190
+ const consumeLive = async () => {
1191
+ const live = options.subscribeLive();
1192
+ activeLive = live;
1193
+ try {
1194
+ for await (const event of live) {
1195
+ await deliverItem(await options.processLive(event), true, true);
1196
+ }
1197
+ throw new RetryableStreamError("Live stream ended");
1198
+ } finally {
1199
+ if (activeLive === live) {
1200
+ activeLive = void 0;
1201
+ }
1202
+ await closeIterable(live);
1203
+ }
1204
+ };
1205
+ const throwLiveError = (liveError) => {
1206
+ if (liveError) {
1207
+ throw liveError;
1208
+ }
1209
+ };
1210
+ const bufferLiveEvent = (buffer, event) => {
1211
+ if (buffer.length >= bufferLimit) {
1212
+ throw new LiveBufferOverflowError(bufferLimit);
1213
+ }
1214
+ buffer.push(event);
1215
+ };
1216
+ const startLivePump = (live, isBuffering, liveBuffer) => {
1217
+ let liveError;
1218
+ const pump2 = (async () => {
1219
+ try {
1220
+ for await (const event of live) {
1221
+ if (isBuffering()) {
1222
+ bufferLiveEvent(liveBuffer, event);
1223
+ continue;
1224
+ }
1225
+ await deliverItem(await options.processLive(event), true, true);
1226
+ }
1227
+ throw new RetryableStreamError("Live stream ended");
1228
+ } catch (error) {
1229
+ liveError = error;
799
1230
  }
800
- items.push(
801
- await buildAttachmentMessage(
802
- client,
803
- base,
804
- info,
805
- formatChildId(i, messageGuidStr),
806
- i,
807
- messageGuidStr
808
- )
1231
+ })();
1232
+ return {
1233
+ getError: () => liveError,
1234
+ pump: pump2
1235
+ };
1236
+ };
1237
+ const replayMissed = async (cursor, getLiveError) => {
1238
+ for await (const event of options.fetchMissed(cursor, {
1239
+ limit: catchUpPageSize
1240
+ })) {
1241
+ throwLiveError(getLiveError());
1242
+ await deliverItem(await options.processMissed(event), false, false);
1243
+ }
1244
+ throwLiveError(getLiveError());
1245
+ };
1246
+ const flushLiveBuffer = async (liveBuffer, getLiveError) => {
1247
+ let index = 0;
1248
+ let lastFlushedId;
1249
+ while (index < liveBuffer.length) {
1250
+ throwLiveError(getLiveError());
1251
+ const event = liveBuffer[index];
1252
+ if (event === void 0) {
1253
+ throw new RetryableStreamError("Live stream buffer index missing");
1254
+ }
1255
+ const item = await options.processLive(event);
1256
+ await deliverItem(item, true, false);
1257
+ lastFlushedId = item.id;
1258
+ index += 1;
1259
+ }
1260
+ liveBuffer.length = 0;
1261
+ throwLiveError(getLiveError());
1262
+ return lastFlushedId;
1263
+ };
1264
+ const compactDeliveredIds = (lastId) => {
1265
+ if (!lastId) {
1266
+ return;
1267
+ }
1268
+ deliveredSinceCursor.clear();
1269
+ deliveredSinceCursor.add(lastId);
1270
+ };
1271
+ const catchUpThenConsumeLive = async (cursor) => {
1272
+ const live = options.subscribeLive();
1273
+ activeLive = live;
1274
+ let buffering = true;
1275
+ const liveBuffer = [];
1276
+ const livePump = startLivePump(live, () => buffering, liveBuffer);
1277
+ try {
1278
+ await replayMissed(cursor, livePump.getError);
1279
+ const lastFlushedId = await flushLiveBuffer(
1280
+ liveBuffer,
1281
+ livePump.getError
809
1282
  );
1283
+ compactDeliveredIds(lastFlushedId);
1284
+ buffering = false;
1285
+ resetRetryDelay();
1286
+ await livePump.pump;
1287
+ throwLiveError(livePump.getError());
1288
+ } finally {
1289
+ buffering = false;
1290
+ if (activeLive === live) {
1291
+ activeLive = void 0;
1292
+ }
1293
+ await closeIterable(live);
1294
+ await livePump.pump.catch(() => void 0);
810
1295
  }
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
1296
  };
825
- cacheMessage(cache, msg);
826
- return [msg];
827
- };
1297
+ const run = async () => {
1298
+ while (!closed) {
1299
+ try {
1300
+ if (lastCursor) {
1301
+ await catchUpThenConsumeLive(lastCursor);
1302
+ } else {
1303
+ await consumeLive();
1304
+ }
1305
+ } catch (error) {
1306
+ await closeIterable(activeLive);
1307
+ activeLive = void 0;
1308
+ if (closed) {
1309
+ break;
1310
+ }
1311
+ if (!retryable(error)) {
1312
+ end(error);
1313
+ return;
1314
+ }
1315
+ await sleep(nextRetryDelay());
1316
+ }
1317
+ }
1318
+ end();
1319
+ };
1320
+ const pump = run().catch((error) => {
1321
+ if (!closed) {
1322
+ end(error);
1323
+ }
1324
+ });
1325
+ return async () => {
1326
+ closed = true;
1327
+ cancelSleep();
1328
+ await closeIterable(activeLive);
1329
+ await pump;
1330
+ };
1331
+ });
1332
+
1333
+ // src/providers/imessage/remote/polls.ts
828
1334
  var isVotedPollEvent = (event) => event.delta.type === "voted";
829
1335
  var isUnvotedPollEvent = (event) => event.delta.type === "unvoted";
830
1336
  var toCachedPoll = (input) => {
@@ -908,7 +1414,7 @@ var buildPollOptionMessage = (input) => {
908
1414
  };
909
1415
  };
910
1416
  var buildPollOptionMessages = (input) => {
911
- const messages3 = [];
1417
+ const messages5 = [];
912
1418
  for (const delta of input.deltas) {
913
1419
  const message = buildPollOptionMessage({
914
1420
  cached: input.cached,
@@ -919,10 +1425,10 @@ var buildPollOptionMessages = (input) => {
919
1425
  senderAddress: input.senderAddress
920
1426
  });
921
1427
  if (message) {
922
- messages3.push(message);
1428
+ messages5.push(message);
923
1429
  }
924
1430
  }
925
- return messages3;
1431
+ return messages5;
926
1432
  };
927
1433
  var allOptionIdsKnown = (cached, optionIds) => optionIds.every((optionId) => cached.optionsByIdentifier.has(optionId));
928
1434
  var refreshPollMetadata = async (client, pollCache, event, fallbackOptionIds) => {
@@ -977,7 +1483,7 @@ var toPollVoteMessages = async (client, pollCache, event) => {
977
1483
  senderAddress,
978
1484
  currentOptionIds
979
1485
  );
980
- const messages3 = buildPollOptionMessages({
1486
+ const messages5 = buildPollOptionMessages({
981
1487
  cached: resolvedPoll,
982
1488
  chatGuid: chatGuidStr,
983
1489
  deltas,
@@ -990,7 +1496,7 @@ var toPollVoteMessages = async (client, pollCache, event) => {
990
1496
  currentOptionIds,
991
1497
  event.at
992
1498
  );
993
- return messages3;
1499
+ return messages5;
994
1500
  };
995
1501
  var toPollUnvoteMessages = async (client, pollCache, event) => {
996
1502
  const senderAddress = event.actor.address;
@@ -1006,23 +1512,16 @@ var toPollUnvoteMessages = async (client, pollCache, event) => {
1006
1512
  return [];
1007
1513
  }
1008
1514
  const chatGuidStr = event.chatGuid;
1009
- const messages3 = [];
1010
1515
  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
- }
1516
+ const messages5 = buildPollOptionMessages({
1517
+ cached,
1518
+ chatGuid: chatGuidStr,
1519
+ deltas,
1520
+ event,
1521
+ senderAddress
1522
+ });
1024
1523
  pollCache.commitActorSelection(pollId, senderAddress, [], event.at);
1025
- return messages3;
1524
+ return messages5;
1026
1525
  };
1027
1526
  var toPollDeltaMessages = async (client, pollCache, event) => {
1028
1527
  if (isVotedPollEvent(event)) {
@@ -1033,294 +1532,156 @@ var toPollDeltaMessages = async (client, pollCache, event) => {
1033
1532
  }
1034
1533
  return [];
1035
1534
  };
1036
- var clientStream = (client, pollCache) => {
1037
- const messageSub = client.messages.subscribe("message.received");
1038
- const pollSub = client.polls.subscribe();
1535
+
1536
+ // src/providers/imessage/remote/stream.ts
1537
+ var pollRetryDelay = (delayMs) => Math.random() * delayMs;
1538
+ var isRetryableIMessageStreamError = (error) => {
1539
+ if (error instanceof AuthenticationError || error instanceof NotFoundError2 || error instanceof ValidationError) {
1540
+ return false;
1541
+ }
1542
+ if (error instanceof IMessageError) {
1543
+ return true;
1544
+ }
1545
+ return false;
1546
+ };
1547
+ var toMessageItem = async (client, event, cursor) => {
1548
+ const id = event.message.guid;
1549
+ if (event.message.isFromMe) {
1550
+ return { cursor, id, values: [] };
1551
+ }
1039
1552
  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
- });
1553
+ const target = event.message.associatedMessageGuid;
1554
+ const values = target ? await toReactionMessages(client, cache, event, target) : await toInboundMessages(client, cache, event);
1555
+ return { cursor, id, values };
1077
1556
  };
1078
- var sendVCardAttachment = (remote, name, vcf) => remote.attachments.upload({
1079
- data: Buffer.from(vcf, "utf8"),
1080
- fileName: name,
1081
- mimeType: "text/vcard"
1557
+ var messageStream = (client) => resumableOrderedStream({
1558
+ fetchMissed: (cursor, { limit }) => client.messages.fetchMissed(cursor, { limit }),
1559
+ isRetryableError: isRetryableIMessageStreamError,
1560
+ processLive: (event) => toMessageItem(client, event, event.cursor),
1561
+ processMissed: (message) => toMessageItem(client, receivedEventFromMessage(message)),
1562
+ subscribeLive: () => client.messages.subscribe("message.received")
1082
1563
  });
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)));
1564
+ var logPollStreamError = (error) => {
1565
+ console.error("[spectrum-ts][imessage][poll] stream failed", error);
1095
1566
  };
1096
- var startTyping = async (clients, spaceId) => {
1097
- const remote = clients[0];
1098
- if (!remote) {
1567
+ var emitPollMessages = async (client, pollCache, event, emit) => {
1568
+ cachePollEvent(pollCache, event);
1569
+ if (event.actor.isFromMe) {
1099
1570
  return;
1100
1571
  }
1101
- await remote.chats.startTyping(chatGuid(spaceId));
1102
- };
1103
- var stopTyping = async (clients, spaceId) => {
1104
- const remote = clients[0];
1105
- if (!remote) {
1106
- return;
1572
+ const messages5 = await toPollDeltaMessages(client, pollCache, event);
1573
+ for (const vote of messages5) {
1574
+ await emit(vote);
1107
1575
  }
1108
- await remote.chats.stopTyping(chatGuid(spaceId));
1109
1576
  };
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);
1577
+ var runPollSubscription = async (client, pollCache, subscription, emit, onEvent) => {
1578
+ for await (const event of subscription) {
1579
+ onEvent();
1580
+ await emitPollMessages(client, pollCache, event, emit);
1159
1581
  }
1160
1582
  };
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
- }
1583
+ var pollStream = (client, pollCache) => stream((emit, end) => {
1584
+ let active = client.polls.subscribe();
1585
+ let closed = false;
1586
+ let retryDelayMs = RECONNECT_INITIAL_DELAY_MS;
1587
+ let sleepTimer;
1588
+ let wakeSleep;
1589
+ const sleep = async (delayMs) => {
1590
+ if (closed) {
1591
+ return;
1176
1592
  }
1177
- const groupMembers = [];
1178
- for (const sub of content.items) {
1179
- groupMembers.push(await sendSingle(remote, chat, sub.content));
1593
+ await new Promise((resolve) => {
1594
+ wakeSleep = resolve;
1595
+ sleepTimer = setTimeout(resolve, pollRetryDelay(delayMs));
1596
+ });
1597
+ sleepTimer = void 0;
1598
+ wakeSleep = void 0;
1599
+ };
1600
+ const cancelSleep = () => {
1601
+ if (sleepTimer) {
1602
+ clearTimeout(sleepTimer);
1603
+ sleepTimer = void 0;
1180
1604
  }
1181
- const first = groupMembers[0];
1182
- if (!first) {
1183
- throw new Error("Empty group");
1605
+ wakeSleep?.();
1606
+ wakeSleep = void 0;
1607
+ };
1608
+ const pump = (async () => {
1609
+ while (!closed) {
1610
+ try {
1611
+ await runPollSubscription(client, pollCache, active, emit, () => {
1612
+ retryDelayMs = RECONNECT_INITIAL_DELAY_MS;
1613
+ });
1614
+ } catch (e) {
1615
+ if (!closed) {
1616
+ logPollStreamError(e);
1617
+ }
1618
+ } finally {
1619
+ await active.close();
1620
+ }
1621
+ if (!closed) {
1622
+ await sleep(retryDelayMs);
1623
+ retryDelayMs = Math.min(retryDelayMs * 2, RECONNECT_MAX_DELAY_MS);
1624
+ active = client.polls.subscribe();
1625
+ }
1184
1626
  }
1185
- return { ...first, groupMembers };
1186
- }
1187
- return sendSingle(remote, chat, content);
1627
+ end();
1628
+ })();
1629
+ return async () => {
1630
+ closed = true;
1631
+ cancelSleep();
1632
+ await active.close();
1633
+ await pump;
1634
+ };
1635
+ });
1636
+ var clientStream = (client, pollCache) => {
1637
+ return mergeStreams([messageStream(client), pollStream(client, pollCache)]);
1188
1638
  };
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
- }
1639
+ var messages3 = (clients) => {
1640
+ const pollCache = getPollCache(clients);
1641
+ return mergeStreams(clients.map((client) => clientStream(client, pollCache)));
1254
1642
  };
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];
1643
+
1644
+ // src/providers/imessage/remote/typing.ts
1645
+ import { chatGuid as chatGuid3 } from "@photon-ai/advanced-imessage";
1646
+ var startTyping = async (remote, spaceId) => {
1647
+ await remote.chats.startTyping(chatGuid3(spaceId));
1648
+ };
1649
+ var stopTyping = async (remote, spaceId) => {
1650
+ await remote.chats.stopTyping(chatGuid3(spaceId));
1651
+ };
1652
+
1653
+ // src/providers/imessage/remote/api.ts
1654
+ var messages4 = (clients) => messages3(clients);
1655
+ var startTyping2 = async (clients, spaceId) => {
1656
+ const remote = firstRemoteClient(clients);
1264
1657
  if (!remote) {
1265
- throw new Error("No remote iMessage client available");
1658
+ return;
1266
1659
  }
1267
- await remote.messages.edit(
1268
- chatGuid(spaceId),
1269
- messageGuid(msgId),
1270
- content.text
1271
- );
1660
+ await startTyping(remote, spaceId);
1272
1661
  };
1273
- var reactToMessage = async (clients, spaceId, target, reaction) => {
1274
- const remote = clients[0];
1662
+ var stopTyping2 = async (clients, spaceId) => {
1663
+ const remote = firstRemoteClient(clients);
1275
1664
  if (!remote) {
1276
1665
  return;
1277
1666
  }
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
- }
1667
+ await stopTyping(remote, spaceId);
1288
1668
  };
1289
- var getMessage2 = async (clients, spaceId, msgId) => {
1290
- const remote = clients[0];
1669
+ var send4 = async (clients, spaceId, content) => send3(primaryRemoteClient(clients), spaceId, content);
1670
+ var replyToMessage2 = async (clients, spaceId, msgId, content) => replyToMessage(primaryRemoteClient(clients), spaceId, msgId, content);
1671
+ var editMessage2 = async (clients, spaceId, msgId, content) => editMessage(primaryRemoteClient(clients), spaceId, msgId, content);
1672
+ var reactToMessage2 = async (clients, spaceId, target, reaction) => {
1673
+ const remote = firstRemoteClient(clients);
1291
1674
  if (!remote) {
1292
1675
  return;
1293
1676
  }
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 {
1677
+ await reactToMessage(remote, spaceId, target, reaction);
1678
+ };
1679
+ var getMessage4 = async (clients, spaceId, msgId) => {
1680
+ const remote = firstRemoteClient(clients);
1681
+ if (!remote) {
1322
1682
  return;
1323
1683
  }
1684
+ return getMessage3(remote, spaceId, msgId);
1324
1685
  };
1325
1686
 
1326
1687
  // src/providers/imessage/types.ts
@@ -1346,6 +1707,7 @@ var messageSchema = z.object({
1346
1707
  });
1347
1708
 
1348
1709
  // src/providers/imessage/index.ts
1710
+ var isPollContent = (content) => content.type === "poll" || content.type === "poll_option";
1349
1711
  var imessage = definePlatform("iMessage", {
1350
1712
  config: configSchema,
1351
1713
  user: {
@@ -1414,55 +1776,69 @@ var imessage = definePlatform("iMessage", {
1414
1776
  }
1415
1777
  },
1416
1778
  events: {
1417
- messages: ({ client }) => isLocal(client) ? messages(client) : messages2(client)
1779
+ messages: ({ client }) => isLocal(client) ? messages2(client) : messages4(client)
1418
1780
  },
1419
1781
  actions: {
1420
1782
  send: async ({ space, content, client }) => {
1421
1783
  if (isLocal(client)) {
1422
- return await send(client, space.id, content);
1784
+ return await send2(client, space.id, content);
1423
1785
  }
1424
- return await send2(client, space.id, content);
1786
+ return await send4(client, space.id, content);
1425
1787
  },
1426
1788
  startTyping: async ({ space, client }) => {
1427
1789
  if (isLocal(client)) {
1428
1790
  return;
1429
1791
  }
1430
- await startTyping(client, space.id);
1792
+ await startTyping2(client, space.id);
1431
1793
  },
1432
1794
  stopTyping: async ({ space, client }) => {
1433
1795
  if (isLocal(client)) {
1434
1796
  return;
1435
1797
  }
1436
- await stopTyping(client, space.id);
1798
+ await stopTyping2(client, space.id);
1437
1799
  },
1438
1800
  reactToMessage: async ({ space, target, reaction, client }) => {
1439
1801
  if (isLocal(client)) {
1440
1802
  throw UnsupportedError.action("react", "iMessage (local mode)");
1441
1803
  }
1442
- await reactToMessage(
1804
+ if (isPollContent(target.content)) {
1805
+ throw UnsupportedError.action(
1806
+ "react",
1807
+ "iMessage",
1808
+ "iMessage polls do not support reactions"
1809
+ );
1810
+ }
1811
+ await reactToMessage2(
1443
1812
  client,
1444
1813
  space.id,
1445
1814
  target,
1446
1815
  reaction
1447
1816
  );
1448
1817
  },
1449
- replyToMessage: async ({ space, messageId, content, client }) => {
1818
+ replyToMessage: async ({ space, messageId, target, content, client }) => {
1450
1819
  if (isLocal(client)) {
1451
1820
  throw UnsupportedError.action("reply", "iMessage (local mode)");
1452
1821
  }
1453
- return await replyToMessage(client, space.id, messageId, content);
1822
+ if (isPollContent(target.content)) {
1823
+ throw UnsupportedError.action(
1824
+ "reply",
1825
+ "iMessage",
1826
+ "iMessage polls do not support replies"
1827
+ );
1828
+ }
1829
+ return await replyToMessage2(client, space.id, messageId, content);
1454
1830
  },
1455
1831
  editMessage: async ({ space, messageId, content, client }) => {
1456
1832
  if (isLocal(client)) {
1457
1833
  throw UnsupportedError.action("edit", "iMessage (local mode)");
1458
1834
  }
1459
- await editMessage(client, space.id, messageId, content);
1835
+ await editMessage2(client, space.id, messageId, content);
1460
1836
  },
1461
1837
  getMessage: async ({ space, messageId, client }) => {
1462
1838
  if (isLocal(client)) {
1463
- return getMessage(client, messageId);
1839
+ return getMessage2(client, messageId);
1464
1840
  }
1465
- return getMessage2(client, space.id, messageId);
1841
+ return getMessage4(client, space.id, messageId);
1466
1842
  }
1467
1843
  }
1468
1844
  });