spectrum-ts 1.0.1 → 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,23 +1,25 @@
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
+ asPoll,
7
+ asPollOption,
6
8
  cloud,
7
9
  mergeStreams,
8
10
  stream
9
- } from "../../chunk-2Y5GBI6W.js";
11
+ } from "../../chunk-PXX7ISZ6.js";
10
12
  import {
11
13
  UnsupportedError,
12
14
  asAttachment,
13
15
  asContact,
14
16
  asCustom,
15
- asReaction,
16
17
  asText,
17
18
  definePlatform,
18
19
  fromVCard,
20
+ reactionSchema,
19
21
  toVCard
20
- } from "../../chunk-XMAI2AAN.js";
22
+ } from "../../chunk-7D6FHYKT.js";
21
23
 
22
24
  // src/providers/imessage/index.ts
23
25
  import { createClient as createClient2, directChat } from "@photon-ai/advanced-imessage";
@@ -109,17 +111,12 @@ async function disposeCloudAuth(clients) {
109
111
  }
110
112
  }
111
113
 
112
- // src/providers/imessage/local.ts
114
+ // src/providers/imessage/local/attachments.ts
113
115
  import { createReadStream } from "fs";
114
- import { mkdtemp, readFile, rm, writeFile } from "fs/promises";
115
- import { tmpdir } from "os";
116
- import { basename, join } from "path";
116
+ import { readFile } from "fs/promises";
117
117
  import { Readable } from "stream";
118
- var synthSendResult = () => ({
119
- id: crypto.randomUUID(),
120
- timestamp: /* @__PURE__ */ new Date()
121
- });
122
- var DEFAULT_ATTACHMENT_NAME = "attachment";
118
+
119
+ // src/providers/imessage/shared/vcard.ts
123
120
  var VCARD_MIME_TYPES = /* @__PURE__ */ new Set([
124
121
  "text/vcard",
125
122
  "text/x-vcard",
@@ -134,6 +131,13 @@ var isVCardAttachment = (mimeType, fileName) => {
134
131
  }
135
132
  return Boolean(fileName?.toLowerCase().endsWith(".vcf"));
136
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";
137
141
  var readLocalAttachment = async (att) => {
138
142
  if (!att.localPath) {
139
143
  throw new Error(
@@ -162,6 +166,9 @@ var toVCardContent = async (att) => {
162
166
  return toAttachmentContent(att);
163
167
  }
164
168
  };
169
+ var localAttachmentContent = async (att) => isVCardAttachment(att.mimeType, att.fileName) ? await toVCardContent(att) : toAttachmentContent(att);
170
+
171
+ // src/providers/imessage/local/inbound.ts
165
172
  var toMessages = async (message) => {
166
173
  const { chatId, chatKind } = message;
167
174
  if (!chatId || chatKind === "unknown") {
@@ -180,7 +187,7 @@ var toMessages = async (message) => {
180
187
  message.attachments.map(async (att) => ({
181
188
  ...base,
182
189
  id: `${message.id}:${att.id}`,
183
- content: isVCardAttachment(att.mimeType, att.fileName) ? await toVCardContent(att) : toAttachmentContent(att)
190
+ content: await localAttachmentContent(att)
184
191
  }))
185
192
  );
186
193
  }
@@ -212,10 +219,23 @@ var messages = (client) => stream((emit, end) => {
212
219
  });
213
220
  };
214
221
  });
215
- var vcardFileName = (content) => {
216
- const base = content.name?.formatted ?? content.user?.id ?? "contact";
217
- return `${base.replace(/[^a-zA-Z0-9_\-.]/g, "_")}.vcf`;
218
- };
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
+ });
219
239
  var sendTempFile = async (client, spaceId, name, data) => {
220
240
  const safeName = basename(name) || DEFAULT_ATTACHMENT_NAME;
221
241
  const dir = await mkdtemp(join(tmpdir(), "spectrum-"));
@@ -246,246 +266,181 @@ var send = async (client, spaceId, content) => {
246
266
  );
247
267
  return synthSendResult();
248
268
  }
269
+ case "poll":
270
+ throw unsupportedLocalContent("poll");
249
271
  default:
250
- throw UnsupportedError.content(content.type, "iMessage (local mode)");
272
+ throw unsupportedLocalContent(content.type);
251
273
  }
252
274
  };
253
275
  var getMessage = async (_client, _id) => void 0;
254
276
 
255
- // src/providers/imessage/remote.ts
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);
281
+
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);
289
+ }
290
+ return remote;
291
+ };
292
+
293
+ // src/providers/imessage/remote/inbound.ts
256
294
  import {
257
- chatGuid,
258
295
  messageGuid,
259
- Reaction
296
+ NotFoundError
260
297
  } from "@photon-ai/advanced-imessage";
261
298
 
262
- // src/utils/audio.ts
263
- import { spawn } from "child_process";
264
- import { mkdtemp as mkdtemp2, readFile as readFile2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
265
- import { tmpdir as tmpdir2 } from "os";
266
- import { join as join2 } from "path";
267
- var M4A_BRANDS = /* @__PURE__ */ new Set([
268
- "M4A ",
269
- "M4B ",
270
- "M4P ",
271
- "mp42",
272
- "mp41",
273
- "isom",
274
- "iso2"
275
- ]);
276
- var M4A_MIME_TYPES = /* @__PURE__ */ new Set([
277
- "audio/mp4",
278
- "audio/mp4a-latm",
279
- "audio/x-m4a",
280
- "audio/aac",
281
- "audio/aacp"
282
- ]);
283
- var FFMPEG_MISSING_MESSAGE = "voice content: input is not m4a/aac and ffmpeg is unavailable. Install `ffmpeg-static` or ensure `ffmpeg` is on PATH.";
284
- var isM4a = (buffer) => {
285
- if (buffer.length < 12) {
286
- return false;
287
- }
288
- if (buffer.toString("ascii", 4, 8) !== "ftyp") {
289
- return false;
290
- }
291
- return M4A_BRANDS.has(buffer.toString("ascii", 8, 12));
292
- };
293
- var isM4aMimeType = (mimeType) => M4A_MIME_TYPES.has(mimeType.toLowerCase());
294
- var cachedFfmpegPath;
295
- var tryStaticBinary = async () => {
296
- try {
297
- const mod = await import("ffmpeg-static");
298
- return mod.default ?? void 0;
299
- } catch {
300
- return void 0;
301
- }
302
- };
303
- var resolveFfmpegPath = async () => {
304
- if (cachedFfmpegPath) {
305
- return cachedFfmpegPath;
306
- }
307
- cachedFfmpegPath = await tryStaticBinary() ?? "ffmpeg";
308
- return cachedFfmpegPath;
309
- };
310
- var collectStream = (stream2) => {
311
- if (!stream2) {
312
- return Promise.resolve("");
299
+ // src/providers/imessage/cache.ts
300
+ var DEFAULT_MAX = 1e3;
301
+ var MessageCache = class {
302
+ map = /* @__PURE__ */ new Map();
303
+ max;
304
+ constructor(max = DEFAULT_MAX) {
305
+ this.max = max;
313
306
  }
314
- return new Promise((resolve, reject) => {
315
- const chunks = [];
316
- stream2.on("data", (chunk) => {
317
- chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
318
- });
319
- stream2.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
320
- stream2.on("error", reject);
321
- });
322
- };
323
- var isMissingBinaryError = (err) => err?.code === "ENOENT";
324
- var runFfmpeg = (ffmpegPath, args) => {
325
- const proc = spawn(ffmpegPath, args, { stdio: ["ignore", "ignore", "pipe"] });
326
- const stderr = collectStream(proc.stderr);
327
- const exit = new Promise((resolve, reject) => {
328
- proc.on(
329
- "error",
330
- (err) => reject(
331
- isMissingBinaryError(err) ? new Error(FFMPEG_MISSING_MESSAGE) : err
332
- )
333
- );
334
- proc.on("exit", (code) => resolve(code ?? -1));
335
- });
336
- return Promise.all([exit, stderr]).then(([code, text]) => ({
337
- code,
338
- stderr: text
339
- }));
340
- };
341
- var DURATION_PATTERN = /Duration:\s*(\d+):(\d{2}):(\d{2})(?:\.(\d{1,3}))?/;
342
- var parseDuration = (stderr) => {
343
- const match = stderr.match(DURATION_PATTERN);
344
- if (!match) {
345
- return void 0;
307
+ get(id) {
308
+ return this.map.get(id);
346
309
  }
347
- const [, hh, mm, ss, frac] = match;
348
- const seconds = Number(hh) * 3600 + Number(mm) * 60 + Number(ss) + Number(`0.${frac ?? 0}`);
349
- return Number.isFinite(seconds) ? seconds : void 0;
350
- };
351
- var transcodeToM4a = async (buffer) => {
352
- const ffmpeg = await resolveFfmpegPath();
353
- const dir = await mkdtemp2(join2(tmpdir2(), "spectrum-voice-"));
354
- const inPath = join2(dir, "in");
355
- const outPath = join2(dir, "out.m4a");
356
- try {
357
- await writeFile2(inPath, buffer);
358
- const { code, stderr } = await runFfmpeg(ffmpeg, [
359
- "-y",
360
- "-i",
361
- inPath,
362
- "-f",
363
- "ipod",
364
- "-c:a",
365
- "aac",
366
- outPath
367
- ]);
368
- if (code !== 0) {
369
- throw new Error(`ffmpeg conversion failed (exit ${code}): ${stderr}`);
310
+ set(id, message) {
311
+ if (this.map.has(id)) {
312
+ this.map.delete(id);
313
+ }
314
+ this.map.set(id, message);
315
+ if (this.map.size > this.max) {
316
+ const first = this.map.keys().next().value;
317
+ if (first !== void 0) {
318
+ this.map.delete(first);
319
+ }
370
320
  }
371
- const out = await readFile2(outPath);
372
- return { buffer: out, duration: parseDuration(stderr) };
373
- } finally {
374
- await rm2(dir, { recursive: true, force: true }).catch(() => {
375
- });
376
321
  }
377
- };
378
- var ensureM4a = async (buffer, mimeType) => {
379
- if (isM4aMimeType(mimeType) || isM4a(buffer)) {
380
- return { buffer };
322
+ clear() {
323
+ this.map.clear();
381
324
  }
382
- return transcodeToM4a(buffer);
383
325
  };
384
-
385
- // src/providers/imessage/cache.ts
386
- var DEFAULT_MAX = 1e3;
387
- var MessageCache = class {
326
+ var PollCache = class {
388
327
  map = /* @__PURE__ */ new Map();
389
328
  max;
329
+ selectionEventTimesByPoll = /* @__PURE__ */ new Map();
330
+ selectionsByPoll = /* @__PURE__ */ new Map();
390
331
  constructor(max = DEFAULT_MAX) {
391
332
  this.max = max;
392
333
  }
393
334
  get(id) {
394
335
  return this.map.get(id);
395
336
  }
396
- set(id, message) {
337
+ set(id, poll) {
397
338
  if (this.map.has(id)) {
398
339
  this.map.delete(id);
399
340
  }
400
- this.map.set(id, message);
341
+ this.map.set(id, poll);
401
342
  if (this.map.size > this.max) {
402
343
  const first = this.map.keys().next().value;
403
344
  if (first !== void 0) {
404
345
  this.map.delete(first);
346
+ this.selectionEventTimesByPoll.delete(first);
347
+ this.selectionsByPoll.delete(first);
405
348
  }
406
349
  }
407
350
  }
408
351
  clear() {
409
352
  this.map.clear();
353
+ this.selectionEventTimesByPoll.clear();
354
+ this.selectionsByPoll.clear();
355
+ }
356
+ actorSelectionDeltas(pollId, actorId, optionIds) {
357
+ const previous = this.selectionsByPoll.get(pollId)?.get(actorId);
358
+ if (!previous) {
359
+ return optionIds.map((optionId) => ({ optionId, selected: true }));
360
+ }
361
+ const current = new Set(optionIds);
362
+ const selected = optionIds.filter((optionId) => !previous.has(optionId)).map((optionId) => ({ optionId, selected: true }));
363
+ const deselected = [...previous].filter((optionId) => !current.has(optionId)).map((optionId) => ({ optionId, selected: false }));
364
+ return [...selected, ...deselected];
365
+ }
366
+ clearedActorSelectionDeltas(pollId, actorId) {
367
+ const previous = this.selectionsByPoll.get(pollId)?.get(actorId);
368
+ if (!previous) {
369
+ return [];
370
+ }
371
+ return [...previous].map((optionId) => ({ optionId, selected: false }));
372
+ }
373
+ actorSelection(pollId, actorId) {
374
+ const selection = this.selectionsByPoll.get(pollId)?.get(actorId);
375
+ return selection ? [...selection] : void 0;
376
+ }
377
+ commitActorSelection(pollId, actorId, optionIds, at) {
378
+ let selections = this.selectionsByPoll.get(pollId);
379
+ if (!selections) {
380
+ selections = /* @__PURE__ */ new Map();
381
+ this.selectionsByPoll.set(pollId, selections);
382
+ }
383
+ selections.set(actorId, new Set(optionIds));
384
+ if (!at) {
385
+ return;
386
+ }
387
+ let eventTimes = this.selectionEventTimesByPoll.get(pollId);
388
+ if (!eventTimes) {
389
+ eventTimes = /* @__PURE__ */ new Map();
390
+ this.selectionEventTimesByPoll.set(pollId, eventTimes);
391
+ }
392
+ const eventTime = at.getTime();
393
+ const previousTime = eventTimes.get(actorId);
394
+ if (previousTime === void 0 || eventTime >= previousTime) {
395
+ eventTimes.set(actorId, eventTime);
396
+ }
397
+ }
398
+ isStaleActorSelectionEvent(pollId, actorId, at) {
399
+ const previousTime = this.selectionEventTimesByPoll.get(pollId)?.get(actorId);
400
+ return previousTime !== void 0 && at.getTime() < previousTime;
410
401
  }
411
402
  };
412
- var caches = /* @__PURE__ */ new WeakMap();
403
+ var messageCaches = /* @__PURE__ */ new WeakMap();
404
+ var pollCaches = /* @__PURE__ */ new WeakMap();
413
405
  var getMessageCache = (owner) => {
414
- let cache = caches.get(owner);
406
+ let cache = messageCaches.get(owner);
415
407
  if (!cache) {
416
408
  cache = new MessageCache();
417
- caches.set(owner, cache);
409
+ messageCaches.set(owner, cache);
418
410
  }
419
411
  return cache;
420
412
  };
421
-
422
- // src/providers/imessage/remote.ts
423
- var PLATFORM = "iMessage";
424
- var URL_BALLOON_BUNDLE_ID = "com.apple.messages.URLBalloonProvider";
425
- var GROUP_ITEM_ALLOWED = /* @__PURE__ */ new Set([
426
- "attachment",
427
- "contact",
428
- "voice"
429
- ]);
430
- var unsupportedContent = (type, detail) => UnsupportedError.content(type, PLATFORM, detail);
431
- var toSendResult = (receipt) => ({
432
- id: receipt.guid,
433
- timestamp: /* @__PURE__ */ new Date()
434
- });
435
- var VCARD_MIME_TYPES2 = /* @__PURE__ */ new Set([
436
- "text/vcard",
437
- "text/x-vcard",
438
- "text/directory",
439
- "application/vcard",
440
- "application/x-vcard"
441
- ]);
442
- var isVCardAttachment2 = (mimeType, fileName) => {
443
- if (mimeType && VCARD_MIME_TYPES2.has(mimeType.toLowerCase())) {
444
- return true;
413
+ var getPollCache = (owner) => {
414
+ let cache = pollCaches.get(owner);
415
+ if (!cache) {
416
+ cache = new PollCache();
417
+ pollCaches.set(owner, cache);
445
418
  }
446
- return Boolean(fileName?.toLowerCase().endsWith(".vcf"));
447
- };
448
- var EMOJI_TO_TAPBACK = {
449
- "\u2764\uFE0F": Reaction.love,
450
- "\u{1F44D}": Reaction.like,
451
- "\u{1F44E}": Reaction.dislike,
452
- "\u{1F602}": Reaction.laugh,
453
- "\u203C\uFE0F": Reaction.emphasize,
454
- "\u2753": Reaction.question
419
+ return cache;
455
420
  };
456
- var TAPBACK_TO_EMOJI = Object.fromEntries(
457
- Object.entries(EMOJI_TO_TAPBACK).map(([emoji, kind]) => [kind, emoji])
458
- );
459
- var TAPBACK_CODE_TO_KIND = {
460
- "2000": Reaction.love,
461
- "2001": Reaction.like,
462
- "2002": Reaction.dislike,
463
- "2003": Reaction.laugh,
464
- "2004": Reaction.emphasize,
465
- "2005": Reaction.question,
466
- "2006": Reaction.emoji,
467
- "2007": Reaction.sticker
421
+
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 };
468
430
  };
469
- var isTapbackRemoval = (code) => code.startsWith("3");
470
- var resolveReactionEmoji = (type, emoji) => {
471
- if (emoji) {
472
- return emoji;
473
- }
474
- if (!type) {
431
+ var parseChildId = (id) => {
432
+ const match = id.match(PART_PREFIX);
433
+ if (!match) {
475
434
  return null;
476
435
  }
477
- const kind = TAPBACK_CODE_TO_KIND[type] ?? type;
478
- return TAPBACK_TO_EMOJI[kind] ?? null;
479
- };
480
- var getAssociatedMessageType = (message) => {
481
- const direct = message.associatedMessageType;
482
- if (typeof direct === "string") {
483
- return direct;
484
- }
485
- const raw = message._raw;
486
- const fromRaw = raw?.associatedMessageType;
487
- return typeof fromRaw === "string" ? fromRaw : void 0;
436
+ return {
437
+ parentGuid: id.replace(PART_PREFIX, ""),
438
+ partIndex: Number(match[1])
439
+ };
488
440
  };
441
+
442
+ // src/providers/imessage/remote/inbound.ts
443
+ var URL_BALLOON_BUNDLE_ID = "com.apple.messages.URLBalloonProvider";
489
444
  var getBalloonBundleId = (message) => {
490
445
  const raw = message._raw;
491
446
  const id = raw?.balloonBundleId;
@@ -499,6 +454,31 @@ var resolveChatGuid = (message, hint) => {
499
454
  return first ?? "";
500
455
  };
501
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
+ });
502
482
  var toAttachmentContent2 = (client, info) => asAttachment({
503
483
  name: info.fileName,
504
484
  mimeType: info.mimeType,
@@ -510,22 +490,15 @@ var toVCardContent2 = async (client, info) => {
510
490
  try {
511
491
  const buf = Buffer.from(await client.attachments.downloadBuffer(info.guid));
512
492
  return asContact(fromVCard(buf.toString("utf8")));
513
- } 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
+ );
514
498
  return toAttachmentContent2(client, info);
515
499
  }
516
500
  };
517
- var attachmentContent = async (client, info) => isVCardAttachment2(info.mimeType, info.fileName) ? await toVCardContent2(client, info) : toAttachmentContent2(client, info);
518
- var baseShape = (message, chatGuidHint, timestamp) => {
519
- const chat = resolveChatGuid(message, chatGuidHint);
520
- return {
521
- sender: { id: resolveSenderId(message) },
522
- space: {
523
- id: chat,
524
- type: chat.includes(";+;") ? "group" : "dm"
525
- },
526
- timestamp
527
- };
528
- };
501
+ var attachmentContent = async (client, info) => isVCardAttachment(info.mimeType, info.fileName) ? await toVCardContent2(client, info) : toAttachmentContent2(client, info);
529
502
  var buildAttachmentMessage = async (client, base, info, id, partIndex, parentId) => {
530
503
  const content = await attachmentContent(client, info);
531
504
  const msg = { ...base, id, content, partIndex };
@@ -534,10 +507,26 @@ var buildAttachmentMessage = async (client, base, info, id, partIndex, parentId)
534
507
  }
535
508
  return msg;
536
509
  };
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
+ };
537
526
  var rebuildFromAppleMessage = async (client, message, chatGuidHint) => {
538
527
  const messageGuidStr = message.guid;
539
528
  const timestamp = message.dateCreated ?? /* @__PURE__ */ new Date();
540
- const base = baseShape(message, chatGuidHint, timestamp);
529
+ const base = buildMessageBase(message, chatGuidHint, timestamp);
541
530
  if (message.attachments.length === 1) {
542
531
  const info = message.attachments[0];
543
532
  if (!info) {
@@ -566,22 +555,13 @@ var rebuildFromAppleMessage = async (client, message, chatGuidHint) => {
566
555
  return {
567
556
  ...base,
568
557
  id: messageGuidStr,
569
- content: asGroup({ items })
558
+ content: asProviderGroup(items)
570
559
  };
571
560
  }
572
- const text = message.text;
573
561
  if (getBalloonBundleId(message) === URL_BALLOON_BUNDLE_ID) {
574
- const url = text ?? "";
575
- try {
576
- return { ...base, id: messageGuidStr, content: asRichlink({ url }) };
577
- } catch {
578
- return {
579
- ...base,
580
- id: messageGuidStr,
581
- content: url ? asText(url) : asCustom(message)
582
- };
583
- }
562
+ return toRichlinkMessage(message, base, messageGuidStr);
584
563
  }
564
+ const text = message.text;
585
565
  return {
586
566
  ...base,
587
567
  id: messageGuidStr,
@@ -592,403 +572,1116 @@ var cacheMessage = (cache, message) => {
592
572
  cache.set(message.id, message);
593
573
  if (message.content.type === "group") {
594
574
  for (const item of message.content.items) {
595
- cache.set(item.id, item);
575
+ if (isIMessageMessage(item)) {
576
+ cache.set(item.id, item);
577
+ }
596
578
  }
597
579
  }
598
580
  };
599
- var toRichlinkMessage = (event, base, id) => {
600
- const url = event.message.text ?? "";
601
- try {
602
- return { ...base, id, content: asRichlink({ url }) };
603
- } catch {
604
- 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 = {
605
623
  ...base,
606
- id,
607
- content: url ? asText(url) : asCustom(event.message)
624
+ id: messageGuidStr,
625
+ content: asProviderGroup(items)
608
626
  };
627
+ cacheMessage(cache, parent);
628
+ return [parent];
609
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];
610
638
  };
611
- var PART_PREFIX = /^p:(\d+)\//;
612
- var formatChildId = (partIndex, parentGuid) => `p:${partIndex}/${parentGuid}`;
613
- var parseTapbackTarget = (target) => {
614
- const match = target.match(PART_PREFIX);
615
- const guid = target.replace(PART_PREFIX, "");
616
- const partIndex = match ? Number(match[1]) : 0;
617
- 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
+ }
618
676
  };
619
- var parseChildId = (id) => {
620
- const match = id.match(PART_PREFIX);
621
- 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) {
622
711
  return null;
623
712
  }
713
+ const kind = TAPBACK_CODE_TO_KIND[type] ?? type;
714
+ return TAPBACK_TO_EMOJI[kind] ?? null;
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
+ });
730
+ var resolveReactionTarget = async (client, cache, strippedGuid, partIndex) => {
731
+ let candidate = cache.get(strippedGuid);
732
+ if (!candidate) {
733
+ try {
734
+ const fetched = await client.messages.get(messageGuid2(strippedGuid));
735
+ candidate = await rebuildFromAppleMessage(client, fetched);
736
+ cacheMessage(cache, candidate);
737
+ } catch {
738
+ return;
739
+ }
740
+ }
741
+ if (candidate.content.type === "group") {
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;
748
+ }
749
+ return candidate;
750
+ };
751
+ var toReactionMessages = async (client, cache, event, target) => {
752
+ const type = getAssociatedMessageType(event.message);
753
+ if (type && isTapbackRemoval(type)) {
754
+ return [];
755
+ }
756
+ const emoji = resolveReactionEmoji(
757
+ type,
758
+ event.message.associatedMessageEmoji
759
+ );
760
+ if (!emoji) {
761
+ return [];
762
+ }
763
+ const { guid: strippedGuid, partIndex } = parseTapbackTarget(target);
764
+ const resolved = await resolveReactionTarget(
765
+ client,
766
+ cache,
767
+ strippedGuid,
768
+ partIndex
769
+ );
770
+ if (!resolved) {
771
+ return [];
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);
778
+ return [
779
+ {
780
+ ...base,
781
+ id: messageId,
782
+ content: asProviderReaction(emoji, resolved)
783
+ }
784
+ ];
785
+ };
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);
796
+ }
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;
830
+ }
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}`);
913
+ }
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"
1087
+ );
1088
+ }
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;
1230
+ }
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
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);
1295
+ }
1296
+ };
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
1334
+ var isVotedPollEvent = (event) => event.delta.type === "voted";
1335
+ var isUnvotedPollEvent = (event) => event.delta.type === "unvoted";
1336
+ var toCachedPoll = (input) => {
1337
+ const poll = asPoll({
1338
+ title: input.title,
1339
+ options: input.options.map((optionInfo) => ({
1340
+ title: optionInfo.text
1341
+ }))
1342
+ });
1343
+ const optionsByIdentifier = /* @__PURE__ */ new Map();
1344
+ for (const [index, optionInfo] of input.options.entries()) {
1345
+ const option = poll.options[index];
1346
+ if (option && optionInfo.optionIdentifier) {
1347
+ optionsByIdentifier.set(optionInfo.optionIdentifier, option);
1348
+ }
1349
+ }
1350
+ return { poll, optionsByIdentifier };
1351
+ };
1352
+ var cachePollInfo = (cache, info) => {
1353
+ const cached = toCachedPoll(info);
1354
+ cache.set(info.messageGuid, cached);
1355
+ return cached;
1356
+ };
1357
+ var cachePollEvent = (cache, event) => {
1358
+ if (event.delta.type === "created" || event.delta.type === "optionAdded") {
1359
+ try {
1360
+ const cached = toCachedPoll({
1361
+ title: event.delta.title,
1362
+ options: event.delta.options
1363
+ });
1364
+ cache.set(event.pollMessageGuid, cached);
1365
+ return cached;
1366
+ } catch (e) {
1367
+ console.error("[spectrum-ts][imessage][poll] failed to cache poll", e);
1368
+ }
1369
+ }
1370
+ };
1371
+ var fetchPollInfo = async (client, cache, event) => {
1372
+ try {
1373
+ const info = await client.polls.get(event.pollMessageGuid);
1374
+ cachePollInfo(cache, info);
1375
+ return info;
1376
+ } catch (e) {
1377
+ console.error("[spectrum-ts][imessage][poll] failed to fetch poll", e);
1378
+ return;
1379
+ }
1380
+ };
1381
+ var resolvePoll = async (client, cache, event) => {
1382
+ const pollId = event.pollMessageGuid;
1383
+ const cached = cache.get(pollId);
1384
+ if (cached) {
1385
+ return cached;
1386
+ }
1387
+ try {
1388
+ const info = await client.polls.get(event.pollMessageGuid);
1389
+ return cachePollInfo(cache, info);
1390
+ } catch (e) {
1391
+ console.error("[spectrum-ts][imessage][poll] failed to resolve poll", e);
1392
+ return;
1393
+ }
1394
+ };
1395
+ var buildPollOptionMessage = (input) => {
1396
+ const option = input.cached.optionsByIdentifier.get(input.optionId);
1397
+ if (!option) {
1398
+ return;
1399
+ }
1400
+ const action = input.selected ? "selected" : "deselected";
1401
+ return {
1402
+ id: `${input.event.pollMessageGuid}:${input.senderAddress}:${input.optionId}:${action}:${input.event.at.getTime()}`,
1403
+ sender: { id: input.senderAddress },
1404
+ space: {
1405
+ id: input.chatGuid,
1406
+ type: input.chatGuid.includes(";+;") ? "group" : "dm"
1407
+ },
1408
+ timestamp: input.event.at,
1409
+ content: asPollOption({
1410
+ option,
1411
+ poll: input.cached.poll,
1412
+ selected: input.selected
1413
+ })
1414
+ };
1415
+ };
1416
+ var buildPollOptionMessages = (input) => {
1417
+ const messages5 = [];
1418
+ for (const delta of input.deltas) {
1419
+ const message = buildPollOptionMessage({
1420
+ cached: input.cached,
1421
+ chatGuid: input.chatGuid,
1422
+ event: input.event,
1423
+ optionId: delta.optionId,
1424
+ selected: delta.selected,
1425
+ senderAddress: input.senderAddress
1426
+ });
1427
+ if (message) {
1428
+ messages5.push(message);
1429
+ }
1430
+ }
1431
+ return messages5;
1432
+ };
1433
+ var allOptionIdsKnown = (cached, optionIds) => optionIds.every((optionId) => cached.optionsByIdentifier.has(optionId));
1434
+ var refreshPollMetadata = async (client, pollCache, event, fallbackOptionIds) => {
1435
+ const info = await fetchPollInfo(client, pollCache, event);
1436
+ if (!info) {
1437
+ return;
1438
+ }
1439
+ const refreshed = pollCache.get(info.messageGuid);
1440
+ if (!refreshed) {
1441
+ return;
1442
+ }
624
1443
  return {
625
- parentGuid: id.replace(PART_PREFIX, ""),
626
- partIndex: Number(match[1])
1444
+ optionIds: [...fallbackOptionIds],
1445
+ poll: refreshed
627
1446
  };
628
1447
  };
629
- var resolveReactionTarget = async (client, cache, strippedGuid, partIndex) => {
630
- let candidate = cache.get(strippedGuid);
631
- if (!candidate) {
632
- try {
633
- const fetched = await client.messages.get(messageGuid(strippedGuid));
634
- candidate = await rebuildFromAppleMessage(client, fetched);
635
- cacheMessage(cache, candidate);
636
- } catch {
637
- return;
1448
+ var toPollVoteMessages = async (client, pollCache, event) => {
1449
+ const senderAddress = event.actor.address;
1450
+ if (!senderAddress) {
1451
+ return [];
1452
+ }
1453
+ const pollId = event.pollMessageGuid;
1454
+ if (pollCache.isStaleActorSelectionEvent(pollId, senderAddress, event.at)) {
1455
+ return [];
1456
+ }
1457
+ const cached = await resolvePoll(client, pollCache, event);
1458
+ if (!cached) {
1459
+ return [];
1460
+ }
1461
+ const chatGuidStr = event.chatGuid;
1462
+ let currentOptionIds = [...event.delta.optionIdentifiers];
1463
+ let resolvedPoll = cached;
1464
+ if (currentOptionIds.some(
1465
+ (optionId) => !resolvedPoll.optionsByIdentifier.has(optionId)
1466
+ )) {
1467
+ const snapshot = await refreshPollMetadata(
1468
+ client,
1469
+ pollCache,
1470
+ event,
1471
+ currentOptionIds
1472
+ );
1473
+ if (snapshot) {
1474
+ currentOptionIds = snapshot.optionIds;
1475
+ resolvedPoll = snapshot.poll;
638
1476
  }
639
1477
  }
640
- if (candidate.content.type === "group") {
641
- const item = candidate.content.items[partIndex];
642
- return item ?? candidate;
1478
+ if (!allOptionIdsKnown(resolvedPoll, currentOptionIds)) {
1479
+ return [];
643
1480
  }
644
- return candidate;
1481
+ const deltas = pollCache.actorSelectionDeltas(
1482
+ pollId,
1483
+ senderAddress,
1484
+ currentOptionIds
1485
+ );
1486
+ const messages5 = buildPollOptionMessages({
1487
+ cached: resolvedPoll,
1488
+ chatGuid: chatGuidStr,
1489
+ deltas,
1490
+ event,
1491
+ senderAddress
1492
+ });
1493
+ pollCache.commitActorSelection(
1494
+ pollId,
1495
+ senderAddress,
1496
+ currentOptionIds,
1497
+ event.at
1498
+ );
1499
+ return messages5;
645
1500
  };
646
- var toReactionMessage = async (client, cache, event, base, id, target) => {
647
- const type = getAssociatedMessageType(event.message);
648
- if (type && isTapbackRemoval(type)) {
1501
+ var toPollUnvoteMessages = async (client, pollCache, event) => {
1502
+ const senderAddress = event.actor.address;
1503
+ if (!senderAddress) {
649
1504
  return [];
650
1505
  }
651
- const emoji = resolveReactionEmoji(
652
- type,
653
- event.message.associatedMessageEmoji
654
- );
655
- if (!emoji) {
1506
+ const pollId = event.pollMessageGuid;
1507
+ if (pollCache.isStaleActorSelectionEvent(pollId, senderAddress, event.at)) {
656
1508
  return [];
657
1509
  }
658
- const { guid: strippedGuid, partIndex } = parseTapbackTarget(target);
659
- const resolved = await resolveReactionTarget(
660
- client,
661
- cache,
662
- strippedGuid,
663
- partIndex
664
- );
665
- if (!resolved) {
1510
+ const cached = await resolvePoll(client, pollCache, event);
1511
+ if (!cached) {
666
1512
  return [];
667
1513
  }
668
- return [
669
- {
670
- ...base,
671
- id,
672
- content: asReaction({ emoji, target: resolved })
673
- }
674
- ];
1514
+ const chatGuidStr = event.chatGuid;
1515
+ const deltas = pollCache.clearedActorSelectionDeltas(pollId, senderAddress);
1516
+ const messages5 = buildPollOptionMessages({
1517
+ cached,
1518
+ chatGuid: chatGuidStr,
1519
+ deltas,
1520
+ event,
1521
+ senderAddress
1522
+ });
1523
+ pollCache.commitActorSelection(pollId, senderAddress, [], event.at);
1524
+ return messages5;
675
1525
  };
676
- var toMessages2 = async (client, cache, event) => {
677
- const base = baseShape(event.message, event.chatGuid, event.timestamp);
678
- const messageGuidStr = event.message.guid;
679
- const assoc = event.message.associatedMessageGuid;
680
- if (assoc) {
681
- return toReactionMessage(client, cache, event, base, messageGuidStr, assoc);
1526
+ var toPollDeltaMessages = async (client, pollCache, event) => {
1527
+ if (isVotedPollEvent(event)) {
1528
+ return toPollVoteMessages(client, pollCache, event);
682
1529
  }
683
- if (getBalloonBundleId(event.message) === URL_BALLOON_BUNDLE_ID) {
684
- const msg2 = toRichlinkMessage(event, base, messageGuidStr);
685
- cacheMessage(cache, msg2);
686
- return [msg2];
1530
+ if (isUnvotedPollEvent(event)) {
1531
+ return toPollUnvoteMessages(client, pollCache, event);
687
1532
  }
688
- if (event.message.attachments.length === 1) {
689
- const info = event.message.attachments[0];
690
- if (!info) {
691
- throw new Error("Unreachable: attachments.length === 1 but no element");
692
- }
693
- const msg2 = await buildAttachmentMessage(
694
- client,
695
- base,
696
- info,
697
- messageGuidStr,
698
- 0
699
- );
700
- cacheMessage(cache, msg2);
701
- return [msg2];
1533
+ return [];
1534
+ };
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;
702
1541
  }
703
- if (event.message.attachments.length > 1) {
704
- const items = [];
705
- for (let i = 0; i < event.message.attachments.length; i++) {
706
- const info = event.message.attachments[i];
707
- if (!info) {
708
- continue;
709
- }
710
- items.push(
711
- await buildAttachmentMessage(
712
- client,
713
- base,
714
- info,
715
- formatChildId(i, messageGuidStr),
716
- i,
717
- messageGuidStr
718
- )
719
- );
720
- }
721
- const parent = {
722
- ...base,
723
- id: messageGuidStr,
724
- content: asGroup({ items })
725
- };
726
- cacheMessage(cache, parent);
727
- return [parent];
1542
+ if (error instanceof IMessageError) {
1543
+ return true;
728
1544
  }
729
- const text = event.message.text;
730
- const msg = {
731
- ...base,
732
- id: messageGuidStr,
733
- content: text ? asText(text) : asCustom(event.message)
734
- };
735
- cacheMessage(cache, msg);
736
- return [msg];
1545
+ return false;
737
1546
  };
738
- var clientStream = (client) => {
739
- const sub = client.messages.subscribe("message.received");
1547
+ var toMessageItem = async (client, event, cursor) => {
1548
+ const id = event.message.guid;
1549
+ if (event.message.isFromMe) {
1550
+ return { cursor, id, values: [] };
1551
+ }
740
1552
  const cache = getMessageCache(client);
741
- return stream((emit, end) => {
742
- const pump = (async () => {
743
- try {
744
- for await (const event of sub) {
745
- if (event.message.isFromMe) {
746
- continue;
747
- }
748
- for (const message of await toMessages2(client, cache, event)) {
749
- await emit(message);
750
- }
751
- }
752
- end();
753
- } catch (e) {
754
- end(e);
755
- }
756
- })();
757
- return async () => {
758
- sub.close();
759
- await pump;
760
- };
761
- });
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 };
762
1556
  };
763
- var sendVCardAttachment = (remote, name, vcf) => remote.attachments.upload({
764
- data: Buffer.from(vcf, "utf8"),
765
- fileName: name,
766
- 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")
767
1563
  });
768
- var vcardFileName2 = (contact) => {
769
- const base = contact.name?.formatted ?? contact.user?.id ?? "contact";
770
- return `${base.replace(/[^a-zA-Z0-9_\-.]/g, "_")}.vcf`;
771
- };
772
- var sendContactAttachment = async (remote, content) => {
773
- const vcf = await toVCard(content);
774
- const upload = await sendVCardAttachment(remote, vcardFileName2(content), vcf);
775
- return upload.guid;
1564
+ var logPollStreamError = (error) => {
1565
+ console.error("[spectrum-ts][imessage][poll] stream failed", error);
776
1566
  };
777
- var messages2 = (clients) => mergeStreams(clients.map(clientStream));
778
- var startTyping = async (clients, spaceId) => {
779
- const remote = clients[0];
780
- if (!remote) {
1567
+ var emitPollMessages = async (client, pollCache, event, emit) => {
1568
+ cachePollEvent(pollCache, event);
1569
+ if (event.actor.isFromMe) {
781
1570
  return;
782
1571
  }
783
- await remote.chats.startTyping(chatGuid(spaceId));
784
- };
785
- var stopTyping = async (clients, spaceId) => {
786
- const remote = clients[0];
787
- if (!remote) {
788
- return;
1572
+ const messages5 = await toPollDeltaMessages(client, pollCache, event);
1573
+ for (const vote of messages5) {
1574
+ await emit(vote);
789
1575
  }
790
- await remote.chats.stopTyping(chatGuid(spaceId));
791
1576
  };
792
- var sendSingle = async (remote, chat, content) => {
793
- switch (content.type) {
794
- case "text":
795
- return toSendResult(await remote.messages.send(chat, content.text));
796
- case "richlink":
797
- return toSendResult(
798
- await remote.messages.send(chat, content.url, { richLink: true })
799
- );
800
- case "attachment": {
801
- const attachment = await remote.attachments.upload({
802
- data: await content.read(),
803
- fileName: content.name,
804
- mimeType: content.mimeType
805
- });
806
- return toSendResult(
807
- await remote.messages.send(chat, "", { attachment: attachment.guid })
808
- );
809
- }
810
- case "contact": {
811
- const attachment = await sendContactAttachment(remote, content);
812
- return toSendResult(await remote.messages.send(chat, "", { attachment }));
813
- }
814
- case "voice": {
815
- const { buffer } = await ensureM4a(
816
- await content.read(),
817
- content.mimeType
818
- );
819
- const attachment = await remote.attachments.upload({
820
- data: buffer,
821
- fileName: content.name ?? "voice.m4a",
822
- mimeType: "audio/x-m4a"
823
- });
824
- return toSendResult(
825
- await remote.messages.send(chat, "", {
826
- attachment: attachment.guid,
827
- audioMessage: true
828
- })
829
- );
830
- }
831
- default:
832
- 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);
833
1581
  }
834
1582
  };
835
- var send2 = async (clients, spaceId, content) => {
836
- const remote = clients[0];
837
- if (!remote) {
838
- throw new Error("No remote iMessage client available");
839
- }
840
- const chat = chatGuid(spaceId);
841
- if (content.type === "group") {
842
- for (const sub of content.items) {
843
- const itemType = sub.content.type;
844
- if (!GROUP_ITEM_ALLOWED.has(itemType)) {
845
- throw unsupportedContent(
846
- "group",
847
- `"${itemType}" items are not supported inside a group`
848
- );
849
- }
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;
850
1592
  }
851
- const groupMembers = [];
852
- for (const sub of content.items) {
853
- 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;
854
1604
  }
855
- const first = groupMembers[0];
856
- if (!first) {
857
- 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
+ }
858
1626
  }
859
- return { ...first, groupMembers };
860
- }
861
- 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)]);
862
1638
  };
863
- var replyToMessage = async (clients, spaceId, msgId, content) => {
864
- const remote = clients[0];
865
- if (!remote) {
866
- throw new Error("No remote iMessage client available");
867
- }
868
- const chat = chatGuid(spaceId);
869
- const replyTo = messageGuid(msgId);
870
- switch (content.type) {
871
- case "text":
872
- return toSendResult(
873
- await remote.messages.send(chat, content.text, { replyTo })
874
- );
875
- case "richlink":
876
- return toSendResult(
877
- await remote.messages.send(chat, content.url, {
878
- richLink: true,
879
- replyTo
880
- })
881
- );
882
- case "attachment": {
883
- const attachment = await remote.attachments.upload({
884
- data: await content.read(),
885
- fileName: content.name,
886
- mimeType: content.mimeType
887
- });
888
- return toSendResult(
889
- await remote.messages.send(chat, "", {
890
- attachment: attachment.guid,
891
- replyTo
892
- })
893
- );
894
- }
895
- case "contact": {
896
- const attachment = await sendContactAttachment(remote, content);
897
- return toSendResult(
898
- await remote.messages.send(chat, "", { attachment, replyTo })
899
- );
900
- }
901
- case "voice": {
902
- const { buffer } = await ensureM4a(
903
- await content.read(),
904
- content.mimeType
905
- );
906
- const attachment = await remote.attachments.upload({
907
- data: buffer,
908
- fileName: content.name ?? "voice.m4a",
909
- mimeType: "audio/x-m4a"
910
- });
911
- return toSendResult(
912
- await remote.messages.send(chat, "", {
913
- attachment: attachment.guid,
914
- audioMessage: true,
915
- replyTo
916
- })
917
- );
918
- }
919
- default:
920
- throw unsupportedContent(content.type);
921
- }
1639
+ var messages3 = (clients) => {
1640
+ const pollCache = getPollCache(clients);
1641
+ return mergeStreams(clients.map((client) => clientStream(client, pollCache)));
922
1642
  };
923
- var editMessage = async (clients, spaceId, msgId, content) => {
924
- if (content.type !== "text") {
925
- throw UnsupportedError.content(
926
- content.type,
927
- PLATFORM,
928
- "only text content can be edited"
929
- );
930
- }
931
- 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);
932
1657
  if (!remote) {
933
- throw new Error("No remote iMessage client available");
1658
+ return;
934
1659
  }
935
- await remote.messages.edit(
936
- chatGuid(spaceId),
937
- messageGuid(msgId),
938
- content.text
939
- );
1660
+ await startTyping(remote, spaceId);
940
1661
  };
941
- var reactToMessage = async (clients, spaceId, target, reaction) => {
942
- const remote = clients[0];
1662
+ var stopTyping2 = async (clients, spaceId) => {
1663
+ const remote = firstRemoteClient(clients);
943
1664
  if (!remote) {
944
1665
  return;
945
1666
  }
946
- const chat = chatGuid(spaceId);
947
- const parentGuid = target.parentId ?? target.id;
948
- const guid = messageGuid(parentGuid);
949
- const opts = typeof target.partIndex === "number" ? { partIndex: target.partIndex } : void 0;
950
- const native = EMOJI_TO_TAPBACK[reaction];
951
- if (native) {
952
- await remote.messages.react(chat, guid, native, opts);
953
- } else {
954
- await remote.messages.reactEmoji(chat, guid, reaction, opts);
955
- }
1667
+ await stopTyping(remote, spaceId);
956
1668
  };
957
- var getMessage2 = async (clients, spaceId, msgId) => {
958
- 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);
959
1674
  if (!remote) {
960
1675
  return;
961
1676
  }
962
- const cache = getMessageCache(remote);
963
- const cached = cache.get(msgId);
964
- if (cached) {
965
- return cached;
966
- }
967
- const childRef = parseChildId(msgId);
968
- if (childRef) {
969
- try {
970
- const fetched = await remote.messages.get(
971
- messageGuid(childRef.parentGuid)
972
- );
973
- const parent = await rebuildFromAppleMessage(remote, fetched, spaceId);
974
- cacheMessage(cache, parent);
975
- if (parent.content.type !== "group") {
976
- return;
977
- }
978
- const items = parent.content.items;
979
- return items[childRef.partIndex];
980
- } catch {
981
- return;
982
- }
983
- }
984
- try {
985
- const fetched = await remote.messages.get(messageGuid(msgId));
986
- const rebuilt = await rebuildFromAppleMessage(remote, fetched, spaceId);
987
- cacheMessage(cache, rebuilt);
988
- return rebuilt;
989
- } catch {
1677
+ await reactToMessage(remote, spaceId, target, reaction);
1678
+ };
1679
+ var getMessage4 = async (clients, spaceId, msgId) => {
1680
+ const remote = firstRemoteClient(clients);
1681
+ if (!remote) {
990
1682
  return;
991
1683
  }
1684
+ return getMessage3(remote, spaceId, msgId);
992
1685
  };
993
1686
 
994
1687
  // src/providers/imessage/types.ts
@@ -1014,6 +1707,7 @@ var messageSchema = z.object({
1014
1707
  });
1015
1708
 
1016
1709
  // src/providers/imessage/index.ts
1710
+ var isPollContent = (content) => content.type === "poll" || content.type === "poll_option";
1017
1711
  var imessage = definePlatform("iMessage", {
1018
1712
  config: configSchema,
1019
1713
  user: {
@@ -1082,55 +1776,69 @@ var imessage = definePlatform("iMessage", {
1082
1776
  }
1083
1777
  },
1084
1778
  events: {
1085
- messages: ({ client }) => isLocal(client) ? messages(client) : messages2(client)
1779
+ messages: ({ client }) => isLocal(client) ? messages2(client) : messages4(client)
1086
1780
  },
1087
1781
  actions: {
1088
1782
  send: async ({ space, content, client }) => {
1089
1783
  if (isLocal(client)) {
1090
- return await send(client, space.id, content);
1784
+ return await send2(client, space.id, content);
1091
1785
  }
1092
- return await send2(client, space.id, content);
1786
+ return await send4(client, space.id, content);
1093
1787
  },
1094
1788
  startTyping: async ({ space, client }) => {
1095
1789
  if (isLocal(client)) {
1096
1790
  return;
1097
1791
  }
1098
- await startTyping(client, space.id);
1792
+ await startTyping2(client, space.id);
1099
1793
  },
1100
1794
  stopTyping: async ({ space, client }) => {
1101
1795
  if (isLocal(client)) {
1102
1796
  return;
1103
1797
  }
1104
- await stopTyping(client, space.id);
1798
+ await stopTyping2(client, space.id);
1105
1799
  },
1106
1800
  reactToMessage: async ({ space, target, reaction, client }) => {
1107
1801
  if (isLocal(client)) {
1108
1802
  throw UnsupportedError.action("react", "iMessage (local mode)");
1109
1803
  }
1110
- 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(
1111
1812
  client,
1112
1813
  space.id,
1113
1814
  target,
1114
1815
  reaction
1115
1816
  );
1116
1817
  },
1117
- replyToMessage: async ({ space, messageId, content, client }) => {
1818
+ replyToMessage: async ({ space, messageId, target, content, client }) => {
1118
1819
  if (isLocal(client)) {
1119
1820
  throw UnsupportedError.action("reply", "iMessage (local mode)");
1120
1821
  }
1121
- 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);
1122
1830
  },
1123
1831
  editMessage: async ({ space, messageId, content, client }) => {
1124
1832
  if (isLocal(client)) {
1125
1833
  throw UnsupportedError.action("edit", "iMessage (local mode)");
1126
1834
  }
1127
- await editMessage(client, space.id, messageId, content);
1835
+ await editMessage2(client, space.id, messageId, content);
1128
1836
  },
1129
1837
  getMessage: async ({ space, messageId, client }) => {
1130
1838
  if (isLocal(client)) {
1131
- return getMessage(client, messageId);
1839
+ return getMessage2(client, messageId);
1132
1840
  }
1133
- return getMessage2(client, space.id, messageId);
1841
+ return getMessage4(client, space.id, messageId);
1134
1842
  }
1135
1843
  }
1136
1844
  });