spectrum-ts 1.5.0 → 1.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,14 +2,14 @@ import {
2
2
  asGroup,
3
3
  asRichlink,
4
4
  groupSchema
5
- } from "./chunk-66GJ45ZZ.js";
5
+ } from "./chunk-VO43HJ5B.js";
6
6
  import {
7
7
  asPoll,
8
8
  asPollOption,
9
9
  cloud,
10
10
  mergeStreams,
11
11
  stream
12
- } from "./chunk-L6LUFBLF.js";
12
+ } from "./chunk-HWADNTQF.js";
13
13
  import {
14
14
  UnsupportedError,
15
15
  asAttachment,
@@ -20,30 +20,99 @@ import {
20
20
  definePlatform,
21
21
  fromVCard,
22
22
  reactionSchema,
23
+ readSchema,
23
24
  text,
24
25
  textSchema,
25
26
  toVCard
26
- } from "./chunk-LH4YEBG3.js";
27
+ } from "./chunk-JWWIFSI7.js";
27
28
 
28
29
  // src/providers/imessage/index.ts
29
30
  import { createClient as createClient2, MessageEffect as MessageEffect2 } from "@photon-ai/advanced-imessage";
30
31
  import { IMessageSDK as IMessageSDK2 } from "@photon-ai/imessage-kit";
31
32
 
33
+ // src/providers/imessage/content/background.ts
34
+ import { readFile } from "fs/promises";
35
+ import { basename } from "path";
36
+ import { lookup as lookupMimeType } from "mime-types";
37
+ import z from "zod";
38
+ var backgroundActionSchema = z.discriminatedUnion("kind", [
39
+ z.object({
40
+ kind: z.literal("set"),
41
+ read: readSchema,
42
+ mimeType: z.string().nonempty()
43
+ }),
44
+ z.object({ kind: z.literal("clear") })
45
+ ]);
46
+ var backgroundSchema = z.object({
47
+ type: z.literal("background"),
48
+ __platform: z.literal("iMessage"),
49
+ __fireAndForget: z.literal(true),
50
+ action: backgroundActionSchema
51
+ });
52
+ var isBackground = (v) => backgroundSchema.safeParse(v).success;
53
+ var CLEAR_SENTINEL = "clear";
54
+ var resolveMimeType = (input, mimeType) => {
55
+ if (mimeType) {
56
+ return mimeType;
57
+ }
58
+ if (typeof input === "string") {
59
+ const resolved = lookupMimeType(basename(input));
60
+ if (resolved) {
61
+ return resolved;
62
+ }
63
+ }
64
+ throw new Error(
65
+ "Unable to resolve MIME type for background. Pass options.mimeType explicitly."
66
+ );
67
+ };
68
+ var cachedRead = (read) => {
69
+ let cached;
70
+ return () => {
71
+ cached ??= read().catch((err) => {
72
+ cached = void 0;
73
+ throw err;
74
+ });
75
+ return cached;
76
+ };
77
+ };
78
+ function background(input, options) {
79
+ if (input === CLEAR_SENTINEL) {
80
+ return {
81
+ build: async () => backgroundSchema.parse({
82
+ type: "background",
83
+ __platform: "iMessage",
84
+ __fireAndForget: true,
85
+ action: { kind: "clear" }
86
+ })
87
+ };
88
+ }
89
+ const mimeType = resolveMimeType(input, options?.mimeType);
90
+ const read = typeof input === "string" ? cachedRead(() => readFile(input)) : cachedRead(async () => input);
91
+ return {
92
+ build: async () => backgroundSchema.parse({
93
+ type: "background",
94
+ __platform: "iMessage",
95
+ __fireAndForget: true,
96
+ action: { kind: "set", read, mimeType }
97
+ })
98
+ };
99
+ }
100
+
32
101
  // src/providers/imessage/content/effect.ts
33
102
  import {
34
103
  MessageEffect
35
104
  } from "@photon-ai/advanced-imessage";
36
105
 
37
106
  // src/content/effect.ts
38
- import z from "zod";
39
- var effectInnerSchema = z.discriminatedUnion("type", [
107
+ import z2 from "zod";
108
+ var effectInnerSchema = z2.discriminatedUnion("type", [
40
109
  textSchema,
41
110
  attachmentSchema
42
111
  ]);
43
- var messageEffectSchema = z.object({
44
- type: z.literal("effect"),
112
+ var messageEffectSchema = z2.object({
113
+ type: z2.literal("effect"),
45
114
  content: effectInnerSchema,
46
- effect: z.string().nonempty()
115
+ effect: z2.string().nonempty()
47
116
  });
48
117
 
49
118
  // src/providers/imessage/content/effect.ts
@@ -77,33 +146,33 @@ import { createClient } from "@photon-ai/advanced-imessage";
77
146
 
78
147
  // src/providers/imessage/types.ts
79
148
  import { IMessageSDK } from "@photon-ai/imessage-kit";
80
- import z2 from "zod";
149
+ import z3 from "zod";
81
150
  var SHARED_PHONE = "shared";
82
151
  var isLocal = (client) => client instanceof IMessageSDK;
83
- var clientEntry = z2.object({
84
- address: z2.string(),
85
- token: z2.string(),
86
- phone: z2.string()
152
+ var clientEntry = z3.object({
153
+ address: z3.string(),
154
+ token: z3.string(),
155
+ phone: z3.string()
87
156
  });
88
- var configSchema = z2.union([
89
- z2.object({ local: z2.literal(true) }),
90
- z2.object({
91
- local: z2.literal(false).optional().default(false),
92
- clients: clientEntry.or(z2.array(clientEntry)).optional()
157
+ var configSchema = z3.union([
158
+ z3.object({ local: z3.literal(true) }),
159
+ z3.object({
160
+ local: z3.literal(false).optional().default(false),
161
+ clients: clientEntry.or(z3.array(clientEntry)).optional()
93
162
  })
94
163
  ]);
95
- var userSchema = z2.object({});
96
- var spaceSchema = z2.object({
97
- id: z2.string(),
98
- type: z2.enum(["dm", "group"]),
99
- phone: z2.string()
164
+ var userSchema = z3.object({});
165
+ var spaceSchema = z3.object({
166
+ id: z3.string(),
167
+ type: z3.enum(["dm", "group"]),
168
+ phone: z3.string()
100
169
  });
101
- var spaceParamsSchema = z2.object({
102
- phone: z2.string().optional()
170
+ var spaceParamsSchema = z3.object({
171
+ phone: z3.string().optional()
103
172
  });
104
- var messageSchema = z2.object({
105
- partIndex: z2.number().int().nonnegative().optional(),
106
- parentId: z2.string().optional()
173
+ var messageSchema = z3.object({
174
+ partIndex: z3.number().int().nonnegative().optional(),
175
+ parentId: z3.string().optional()
107
176
  });
108
177
 
109
178
  // src/providers/imessage/auth.ts
@@ -229,7 +298,7 @@ import { setTimeout as sleep } from "timers/promises";
229
298
 
230
299
  // src/providers/imessage/local/attachments.ts
231
300
  import { createReadStream } from "fs";
232
- import { readFile } from "fs/promises";
301
+ import { readFile as readFile2 } from "fs/promises";
233
302
  import { Readable } from "stream";
234
303
 
235
304
  // src/providers/imessage/shared/vcard.ts
@@ -260,7 +329,7 @@ var readLocalAttachment = async (att) => {
260
329
  `iMessage attachment ${att.id} has no local file available on disk`
261
330
  );
262
331
  }
263
- return readFile(att.localPath);
332
+ return readFile2(att.localPath);
264
333
  };
265
334
  var toAttachmentContent = (att) => {
266
335
  const { localPath } = att;
@@ -379,7 +448,7 @@ var messages = (client) => stream((emit, end) => {
379
448
  // src/providers/imessage/local/send.ts
380
449
  import { mkdtemp, rm, writeFile } from "fs/promises";
381
450
  import { tmpdir } from "os";
382
- import { basename, join } from "path";
451
+ import { basename as basename2, join } from "path";
383
452
 
384
453
  // src/providers/imessage/shared/errors.ts
385
454
  var IMESSAGE_PLATFORM = "iMessage";
@@ -395,7 +464,7 @@ var synthRecord = (spaceId, content) => ({
395
464
  timestamp: /* @__PURE__ */ new Date()
396
465
  });
397
466
  var sendTempFile = async (client, spaceId, name, data) => {
398
- const safeName = basename(name) || DEFAULT_ATTACHMENT_NAME;
467
+ const safeName = basename2(name) || DEFAULT_ATTACHMENT_NAME;
399
468
  const dir = await mkdtemp(join(tmpdir(), "spectrum-"));
400
469
  const tmp = join(dir, safeName);
401
470
  await writeFile(tmp, data);
@@ -442,6 +511,38 @@ var messages2 = (client) => messages(client);
442
511
  var send2 = async (client, spaceId, content) => send(client, spaceId, content);
443
512
  var getMessage2 = async (client, id) => getMessage(client, id);
444
513
 
514
+ // src/providers/imessage/remote/ids.ts
515
+ var PART_PREFIX = /^p:(\d+)\//;
516
+ var dmChatGuid = (address) => `any;-;${address}`;
517
+ var toChatGuid = (value) => value;
518
+ var toMessageGuid = (value) => value;
519
+ var formatChildId = (partIndex, parentGuid) => `p:${partIndex}/${parentGuid}`;
520
+ var parseChildId = (id) => {
521
+ const match = id.match(PART_PREFIX);
522
+ if (!match) {
523
+ return null;
524
+ }
525
+ return {
526
+ parentGuid: id.replace(PART_PREFIX, ""),
527
+ partIndex: Number(match[1])
528
+ };
529
+ };
530
+
531
+ // src/providers/imessage/remote/background.ts
532
+ var setBackground = async (remote, spaceId, content) => {
533
+ const chat = toChatGuid(spaceId);
534
+ if (content.action.kind === "clear") {
535
+ await remote.chats.removeBackground(chat);
536
+ return;
537
+ }
538
+ const buffer = await content.action.read();
539
+ await remote.chats.setBackground(
540
+ chat,
541
+ new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength),
542
+ content.action.mimeType
543
+ );
544
+ };
545
+
445
546
  // src/providers/imessage/remote/inbound.ts
446
547
  import {
447
548
  NotFoundError
@@ -570,23 +671,6 @@ var getPollCache = (owner) => {
570
671
  return cache;
571
672
  };
572
673
 
573
- // src/providers/imessage/remote/ids.ts
574
- var PART_PREFIX = /^p:(\d+)\//;
575
- var dmChatGuid = (address) => `any;-;${address}`;
576
- var toChatGuid = (value) => value;
577
- var toMessageGuid = (value) => value;
578
- var formatChildId = (partIndex, parentGuid) => `p:${partIndex}/${parentGuid}`;
579
- var parseChildId = (id) => {
580
- const match = id.match(PART_PREFIX);
581
- if (!match) {
582
- return null;
583
- }
584
- return {
585
- parentGuid: id.replace(PART_PREFIX, ""),
586
- partIndex: Number(match[1])
587
- };
588
- };
589
-
590
674
  // src/providers/imessage/remote/inbound.ts
591
675
  var URL_BALLOON_BUNDLE_ID = "com.apple.messages.URLBalloonProvider";
592
676
  var getBalloonBundleId = (message) => message.content.balloonBundleId;
@@ -988,7 +1072,7 @@ var reactToMessage = async (remote, spaceId, target, reaction) => {
988
1072
 
989
1073
  // src/utils/audio.ts
990
1074
  import { spawn } from "child_process";
991
- import { mkdtemp as mkdtemp2, readFile as readFile2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
1075
+ import { mkdtemp as mkdtemp2, readFile as readFile3, rm as rm2, writeFile as writeFile2 } from "fs/promises";
992
1076
  import { tmpdir as tmpdir2 } from "os";
993
1077
  import { join as join2 } from "path";
994
1078
  var M4A_BRANDS = /* @__PURE__ */ new Set([
@@ -1024,7 +1108,7 @@ var tryStaticBinary = async () => {
1024
1108
  const mod = await import("ffmpeg-static");
1025
1109
  return mod.default ?? void 0;
1026
1110
  } catch {
1027
- return void 0;
1111
+ return;
1028
1112
  }
1029
1113
  };
1030
1114
  var resolveFfmpegPath = async () => {
@@ -1069,7 +1153,7 @@ var DURATION_PATTERN = /Duration:\s*(\d+):(\d{2}):(\d{2})(?:\.(\d{1,3}))?/;
1069
1153
  var parseDuration = (stderr) => {
1070
1154
  const match = stderr.match(DURATION_PATTERN);
1071
1155
  if (!match) {
1072
- return void 0;
1156
+ return;
1073
1157
  }
1074
1158
  const [, hh, mm, ss, frac] = match;
1075
1159
  const seconds = Number(hh) * 3600 + Number(mm) * 60 + Number(ss) + Number(`0.${frac ?? 0}`);
@@ -1095,7 +1179,7 @@ var transcodeToM4a = async (buffer) => {
1095
1179
  if (code !== 0) {
1096
1180
  throw new Error(`ffmpeg conversion failed (exit ${code}): ${stderr}`);
1097
1181
  }
1098
- const out = await readFile2(outPath);
1182
+ const out = await readFile3(outPath);
1099
1183
  return { buffer: out, duration: parseDuration(stderr) };
1100
1184
  } finally {
1101
1185
  await rm2(dir, { recursive: true, force: true }).catch(() => {
@@ -1539,7 +1623,7 @@ var resumableOrderedStream = (options) => stream((emit, end) => {
1539
1623
  activeLive = void 0;
1540
1624
  }
1541
1625
  await closeIterable(live);
1542
- void livePump.pump.catch(ignoreCleanupError);
1626
+ await livePump.pump.catch(ignoreCleanupError);
1543
1627
  }
1544
1628
  };
1545
1629
  const run = async () => {
@@ -1574,7 +1658,7 @@ var resumableOrderedStream = (options) => stream((emit, end) => {
1574
1658
  closed = true;
1575
1659
  cancelSleep();
1576
1660
  await closeIterable(activeLive);
1577
- void pump.catch(ignoreCleanupError);
1661
+ await pump.catch(ignoreCleanupError);
1578
1662
  };
1579
1663
  });
1580
1664
 
@@ -1864,7 +1948,7 @@ async function* catchUpEvents(client, cursor, isWanted) {
1864
1948
  }
1865
1949
  var toResumeAfter = (cursor) => {
1866
1950
  if (!cursor) {
1867
- return void 0;
1951
+ return;
1868
1952
  }
1869
1953
  const sequence = Number(cursor);
1870
1954
  return Number.isSafeInteger(sequence) && sequence >= 0 ? sequence : void 0;
@@ -1922,6 +2006,7 @@ var stopTyping = async (remote, spaceId) => {
1922
2006
 
1923
2007
  // src/providers/imessage/remote/api.ts
1924
2008
  var messages4 = (clients) => messages3(clients);
2009
+ var setBackground2 = async (remote, spaceId, content) => setBackground(remote, spaceId, content);
1925
2010
  var startTyping2 = async (remote, spaceId) => {
1926
2011
  await startTyping(remote, spaceId);
1927
2012
  };
@@ -1972,6 +2057,31 @@ var randomPhone = (clients) => {
1972
2057
 
1973
2058
  // src/providers/imessage/index.ts
1974
2059
  var isPollContent = (content) => content.type === "poll" || content.type === "poll_option";
2060
+ var handleEdit = async (client, space, content) => {
2061
+ if (isLocal(client)) {
2062
+ throw UnsupportedError.action("edit", "iMessage (local mode)");
2063
+ }
2064
+ if (content.content.type !== "text") {
2065
+ throw UnsupportedError.content(
2066
+ "edit",
2067
+ "iMessage",
2068
+ `only text content can be edited (got "${content.content.type}")`
2069
+ );
2070
+ }
2071
+ const remote = clientForPhone(client, space.phone);
2072
+ await editMessage2(remote, space.id, content.target.id, content.content);
2073
+ };
2074
+ var handleBackground = async (client, space, content) => {
2075
+ if (isLocal(client)) {
2076
+ throw UnsupportedError.action(
2077
+ "background",
2078
+ "iMessage (local mode)",
2079
+ "chat backgrounds require remote iMessage"
2080
+ );
2081
+ }
2082
+ const remote = clientForPhone(client, space.phone);
2083
+ await setBackground2(remote, space.id, content);
2084
+ };
1975
2085
  var imessage = definePlatform("iMessage", {
1976
2086
  config: configSchema,
1977
2087
  static: {
@@ -2047,76 +2157,86 @@ var imessage = definePlatform("iMessage", {
2047
2157
  }
2048
2158
  const { chat } = await remote.chats.create(addresses);
2049
2159
  return { id: chat.guid, type: "group", phone };
2160
+ },
2161
+ actions: {
2162
+ // Sugar: `space.background(input, opts?)` →
2163
+ // `space.send(background(input, opts?))`. Wired through the universal
2164
+ // send pipeline so the unsupported-content + warn-and-skip path on
2165
+ // local-mode iMessage is identical to the canonical form.
2166
+ background
2050
2167
  }
2051
2168
  },
2052
2169
  message: {
2053
2170
  schema: messageSchema
2054
2171
  },
2055
- events: {
2056
- messages: ({ client }) => isLocal(client) ? messages2(client) : messages4(client)
2057
- },
2058
- actions: {
2059
- send: async ({ space, content, client }) => {
2172
+ messages: ({ client }) => isLocal(client) ? messages2(client) : messages4(client),
2173
+ send: async ({ space, content, client }) => {
2174
+ if (content.type === "reply") {
2060
2175
  if (isLocal(client)) {
2061
- return await send2(client, space.id, content);
2062
- }
2063
- const remote = clientForPhone(client, space.phone);
2064
- return await send4(remote, space.id, content);
2065
- },
2066
- startTyping: async ({ space, client }) => {
2067
- if (isLocal(client)) {
2068
- return;
2176
+ throw UnsupportedError.action("reply", "iMessage (local mode)");
2069
2177
  }
2070
- const remote = clientForPhone(client, space.phone);
2071
- await startTyping2(remote, space.id);
2072
- },
2073
- stopTyping: async ({ space, client }) => {
2074
- if (isLocal(client)) {
2075
- return;
2178
+ if (isPollContent(content.target.content)) {
2179
+ throw UnsupportedError.action(
2180
+ "reply",
2181
+ "iMessage",
2182
+ "iMessage polls do not support replies"
2183
+ );
2076
2184
  }
2077
- const remote = clientForPhone(client, space.phone);
2078
- await stopTyping2(remote, space.id);
2079
- },
2080
- reactToMessage: async ({ space, target, reaction, client }) => {
2185
+ const remote2 = clientForPhone(client, space.phone);
2186
+ return await replyToMessage2(
2187
+ remote2,
2188
+ space.id,
2189
+ content.target.id,
2190
+ content.content
2191
+ );
2192
+ }
2193
+ if (content.type === "reaction") {
2081
2194
  if (isLocal(client)) {
2082
2195
  throw UnsupportedError.action("react", "iMessage (local mode)");
2083
2196
  }
2084
- if (isPollContent(target.content)) {
2197
+ if (isPollContent(content.target.content)) {
2085
2198
  throw UnsupportedError.action(
2086
2199
  "react",
2087
2200
  "iMessage",
2088
2201
  "iMessage polls do not support reactions"
2089
2202
  );
2090
2203
  }
2091
- const remote = clientForPhone(client, space.phone);
2204
+ const remote2 = clientForPhone(client, space.phone);
2092
2205
  await reactToMessage2(
2093
- remote,
2206
+ remote2,
2094
2207
  space.id,
2095
- target,
2096
- reaction
2208
+ content.target,
2209
+ content.emoji
2097
2210
  );
2098
- },
2099
- replyToMessage: async ({ space, messageId, target, content, client }) => {
2211
+ return;
2212
+ }
2213
+ if (content.type === "typing") {
2100
2214
  if (isLocal(client)) {
2101
- throw UnsupportedError.action("reply", "iMessage (local mode)");
2102
- }
2103
- if (isPollContent(target.content)) {
2104
- throw UnsupportedError.action(
2105
- "reply",
2106
- "iMessage",
2107
- "iMessage polls do not support replies"
2108
- );
2215
+ return;
2109
2216
  }
2110
- const remote = clientForPhone(client, space.phone);
2111
- return await replyToMessage2(remote, space.id, messageId, content);
2112
- },
2113
- editMessage: async ({ space, messageId, content, client }) => {
2114
- if (isLocal(client)) {
2115
- throw UnsupportedError.action("edit", "iMessage (local mode)");
2217
+ const remote2 = clientForPhone(client, space.phone);
2218
+ if (content.state === "start") {
2219
+ await startTyping2(remote2, space.id);
2220
+ } else {
2221
+ await stopTyping2(remote2, space.id);
2116
2222
  }
2117
- const remote = clientForPhone(client, space.phone);
2118
- await editMessage2(remote, space.id, messageId, content);
2119
- },
2223
+ return;
2224
+ }
2225
+ if (content.type === "edit") {
2226
+ await handleEdit(client, space, content);
2227
+ return;
2228
+ }
2229
+ if (isBackground(content)) {
2230
+ await handleBackground(client, space, content);
2231
+ return;
2232
+ }
2233
+ if (isLocal(client)) {
2234
+ return await send2(client, space.id, content);
2235
+ }
2236
+ const remote = clientForPhone(client, space.phone);
2237
+ return await send4(remote, space.id, content);
2238
+ },
2239
+ actions: {
2120
2240
  getMessage: async ({ space, messageId, client }) => {
2121
2241
  if (isLocal(client)) {
2122
2242
  return getMessage2(client, messageId);
@@ -2128,6 +2248,7 @@ var imessage = definePlatform("iMessage", {
2128
2248
  });
2129
2249
 
2130
2250
  export {
2251
+ background,
2131
2252
  effect,
2132
2253
  imessage
2133
2254
  };