spectrum-ts 1.9.2 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,98 +2,56 @@ import {
2
2
  asGroup,
3
3
  asRichlink,
4
4
  groupSchema
5
- } from "./chunk-5NHNMN4H.js";
5
+ } from "./chunk-UFJZIZDO.js";
6
6
  import {
7
7
  asPoll,
8
- asPollOption,
8
+ asPollOption
9
+ } from "./chunk-KO67KDBD.js";
10
+ import {
9
11
  cloud,
10
12
  mergeStreams,
11
13
  stream
12
- } from "./chunk-HWADNTQF.js";
14
+ } from "./chunk-YKWKZ2PZ.js";
15
+ import {
16
+ asContact,
17
+ fromVCard,
18
+ toVCard
19
+ } from "./chunk-L3VXHUVY.js";
13
20
  import {
14
21
  UnsupportedError,
15
22
  asAttachment,
16
- asContact,
17
23
  asCustom,
18
24
  asText,
19
25
  attachmentSchema,
26
+ buildPhotoAction,
20
27
  definePlatform,
21
- fromVCard,
28
+ photoActionSchema,
22
29
  reactionSchema,
23
- readSchema,
24
30
  text,
25
- textSchema,
26
- toVCard
27
- } from "./chunk-XZSBR26X.js";
31
+ textSchema
32
+ } from "./chunk-YDHES53X.js";
28
33
 
29
34
  // src/providers/imessage/index.ts
30
35
  import { createClient as createClient2, MessageEffect as MessageEffect2 } from "@photon-ai/advanced-imessage";
31
36
  import { IMessageSDK as IMessageSDK2 } from "@photon-ai/imessage-kit";
32
37
 
33
38
  // 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
39
  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
40
  var backgroundSchema = z.object({
47
41
  type: z.literal("background"),
48
42
  __platform: z.literal("iMessage"),
49
43
  __fireAndForget: z.literal(true),
50
- action: backgroundActionSchema
44
+ action: photoActionSchema
51
45
  });
52
46
  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
47
  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);
48
+ const action = buildPhotoAction(input, options, "background");
91
49
  return {
92
50
  build: async () => backgroundSchema.parse({
93
51
  type: "background",
94
52
  __platform: "iMessage",
95
53
  __fireAndForget: true,
96
- action: { kind: "set", read, mimeType }
54
+ action
97
55
  })
98
56
  };
99
57
  }
@@ -141,38 +99,68 @@ function effect(input, messageEffect) {
141
99
  };
142
100
  }
143
101
 
102
+ // src/providers/imessage/content/read.ts
103
+ import z3 from "zod";
104
+ var isMessage = (v) => typeof v === "object" && v !== null && "id" in v && "content" in v;
105
+ var readSchema = z3.object({
106
+ type: z3.literal("read"),
107
+ __platform: z3.literal("iMessage"),
108
+ __fireAndForget: z3.literal(true),
109
+ target: z3.custom(isMessage, {
110
+ message: "read target must be a Message"
111
+ })
112
+ });
113
+ var isRead = (v) => readSchema.safeParse(v).success;
114
+ function read(target) {
115
+ return {
116
+ build: async () => {
117
+ if (target.direction !== "inbound") {
118
+ throw new Error(
119
+ `read() target must be an inbound message (got direction "${target.direction}", message id "${target.id}")`
120
+ );
121
+ }
122
+ return readSchema.parse({
123
+ type: "read",
124
+ __platform: "iMessage",
125
+ __fireAndForget: true,
126
+ target
127
+ });
128
+ }
129
+ };
130
+ }
131
+
144
132
  // src/providers/imessage/auth.ts
145
133
  import { createClient } from "@photon-ai/advanced-imessage";
146
134
 
147
135
  // src/providers/imessage/types.ts
148
136
  import { IMessageSDK } from "@photon-ai/imessage-kit";
149
- import z3 from "zod";
137
+ import z4 from "zod";
150
138
  var SHARED_PHONE = "shared";
151
139
  var isLocal = (client) => client instanceof IMessageSDK;
152
- var clientEntry = z3.object({
153
- address: z3.string(),
154
- token: z3.string(),
155
- phone: z3.string()
140
+ var clientEntry = z4.object({
141
+ address: z4.string(),
142
+ token: z4.string(),
143
+ phone: z4.string()
156
144
  });
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()
145
+ var configSchema = z4.union([
146
+ z4.object({ local: z4.literal(true) }),
147
+ z4.object({
148
+ local: z4.literal(false).optional().default(false),
149
+ clients: clientEntry.or(z4.array(clientEntry)).optional()
162
150
  })
163
151
  ]);
164
- var userSchema = z3.object({});
165
- var spaceSchema = z3.object({
166
- id: z3.string(),
167
- type: z3.enum(["dm", "group"]),
168
- phone: z3.string()
152
+ var userSchema = z4.object({});
153
+ var spaceSchema = z4.object({
154
+ id: z4.string(),
155
+ type: z4.enum(["dm", "group"]),
156
+ phone: z4.string()
169
157
  });
170
- var spaceParamsSchema = z3.object({
171
- phone: z3.string().optional()
158
+ var spaceParamsSchema = z4.object({
159
+ phone: z4.string().optional()
172
160
  });
173
- var messageSchema = z3.object({
174
- partIndex: z3.number().int().nonnegative().optional(),
175
- parentId: z3.string().optional()
161
+ var messageSchema = z4.object({
162
+ partIndex: z4.number().int().nonnegative().optional(),
163
+ parentId: z4.string().optional()
176
164
  });
177
165
 
178
166
  // src/providers/imessage/auth.ts
@@ -298,7 +286,7 @@ import { setTimeout as sleep } from "timers/promises";
298
286
 
299
287
  // src/providers/imessage/local/attachments.ts
300
288
  import { createReadStream } from "fs";
301
- import { readFile as readFile2 } from "fs/promises";
289
+ import { readFile } from "fs/promises";
302
290
  import { Readable } from "stream";
303
291
 
304
292
  // src/providers/imessage/shared/vcard.ts
@@ -329,7 +317,7 @@ var readLocalAttachment = async (att) => {
329
317
  `iMessage attachment ${att.id} has no local file available on disk`
330
318
  );
331
319
  }
332
- return readFile2(att.localPath);
320
+ return readFile(att.localPath);
333
321
  };
334
322
  var toAttachmentContent = (att) => {
335
323
  const { localPath } = att;
@@ -448,7 +436,7 @@ var messages = (client) => stream((emit, end) => {
448
436
  // src/providers/imessage/local/send.ts
449
437
  import { mkdtemp, rm, writeFile } from "fs/promises";
450
438
  import { tmpdir } from "os";
451
- import { basename as basename2, join } from "path";
439
+ import { basename, join } from "path";
452
440
 
453
441
  // src/providers/imessage/shared/errors.ts
454
442
  var IMESSAGE_PLATFORM = "iMessage";
@@ -464,7 +452,7 @@ var synthRecord = (spaceId, content) => ({
464
452
  timestamp: /* @__PURE__ */ new Date()
465
453
  });
466
454
  var sendTempFile = async (client, spaceId, name, data) => {
467
- const safeName = basename2(name) || DEFAULT_ATTACHMENT_NAME;
455
+ const safeName = basename(name) || DEFAULT_ATTACHMENT_NAME;
468
456
  const dir = await mkdtemp(join(tmpdir(), "spectrum-"));
469
457
  const tmp = join(dir, safeName);
470
458
  await writeFile(tmp, data);
@@ -528,6 +516,20 @@ var parseChildId = (id) => {
528
516
  };
529
517
  };
530
518
 
519
+ // src/providers/imessage/remote/avatar.ts
520
+ var setIcon = async (remote, spaceId, content) => {
521
+ const chat = toChatGuid(spaceId);
522
+ if (content.action.kind === "clear") {
523
+ await remote.groups.removeIcon(chat);
524
+ return;
525
+ }
526
+ const buffer = await content.action.read();
527
+ await remote.groups.setIcon(
528
+ chat,
529
+ new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength)
530
+ );
531
+ };
532
+
531
533
  // src/providers/imessage/remote/background.ts
532
534
  var setBackground = async (remote, spaceId, content) => {
533
535
  const chat = toChatGuid(spaceId);
@@ -1017,9 +1019,19 @@ var reactToMessage = async (remote, spaceId, target, reaction) => {
1017
1019
  }
1018
1020
  };
1019
1021
 
1022
+ // src/providers/imessage/remote/read.ts
1023
+ var markRead = async (remote, spaceId) => {
1024
+ await remote.chats.markRead(toChatGuid(spaceId));
1025
+ };
1026
+
1027
+ // src/providers/imessage/remote/rename.ts
1028
+ var setDisplayName = async (remote, spaceId, content) => {
1029
+ await remote.groups.setDisplayName(toChatGuid(spaceId), content.displayName);
1030
+ };
1031
+
1020
1032
  // src/utils/audio.ts
1021
1033
  import { spawn } from "child_process";
1022
- import { mkdtemp as mkdtemp2, readFile as readFile3, rm as rm2, writeFile as writeFile2 } from "fs/promises";
1034
+ import { mkdtemp as mkdtemp2, readFile as readFile2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
1023
1035
  import { tmpdir as tmpdir2 } from "os";
1024
1036
  import { join as join2 } from "path";
1025
1037
  var M4A_BRANDS = /* @__PURE__ */ new Set([
@@ -1126,7 +1138,7 @@ var transcodeToM4a = async (buffer) => {
1126
1138
  if (code !== 0) {
1127
1139
  throw new Error(`ffmpeg conversion failed (exit ${code}): ${stderr}`);
1128
1140
  }
1129
- const out = await readFile3(outPath);
1141
+ const out = await readFile2(outPath);
1130
1142
  return { buffer: out, duration: parseDuration(stderr) };
1131
1143
  } finally {
1132
1144
  await rm2(dir, { recursive: true, force: true }).catch(() => {
@@ -1879,6 +1891,11 @@ var stopTyping = async (remote, spaceId) => {
1879
1891
  // src/providers/imessage/remote/api.ts
1880
1892
  var messages4 = (clients) => messages3(clients);
1881
1893
  var setBackground2 = async (remote, spaceId, content) => setBackground(remote, spaceId, content);
1894
+ var setDisplayName2 = async (remote, spaceId, content) => setDisplayName(remote, spaceId, content);
1895
+ var setIcon2 = async (remote, spaceId, content) => setIcon(remote, spaceId, content);
1896
+ var markRead2 = async (remote, spaceId) => {
1897
+ await markRead(remote, spaceId);
1898
+ };
1882
1899
  var startTyping2 = async (remote, spaceId) => {
1883
1900
  await startTyping(remote, spaceId);
1884
1901
  };
@@ -1954,6 +1971,64 @@ var handleBackground = async (client, space, content) => {
1954
1971
  const remote = clientForPhone(client, space.phone);
1955
1972
  await setBackground2(remote, space.id, content);
1956
1973
  };
1974
+ var handleRead = async (client, space) => {
1975
+ if (isLocal(client)) {
1976
+ throw UnsupportedError.action(
1977
+ "read",
1978
+ "iMessage (local mode)",
1979
+ "marking chats as read requires remote iMessage"
1980
+ );
1981
+ }
1982
+ const remote = clientForPhone(client, space.phone);
1983
+ await markRead2(remote, space.id);
1984
+ };
1985
+ var handleTyping = async (client, space, state) => {
1986
+ if (isLocal(client)) {
1987
+ return;
1988
+ }
1989
+ const remote = clientForPhone(client, space.phone);
1990
+ if (state === "start") {
1991
+ await startTyping2(remote, space.id);
1992
+ } else {
1993
+ await stopTyping2(remote, space.id);
1994
+ }
1995
+ };
1996
+ var handleRename = async (client, space, content) => {
1997
+ if (isLocal(client)) {
1998
+ throw UnsupportedError.action(
1999
+ "rename",
2000
+ "iMessage (local mode)",
2001
+ "renaming chats requires remote iMessage"
2002
+ );
2003
+ }
2004
+ if (space.type !== "group") {
2005
+ throw UnsupportedError.action(
2006
+ "rename",
2007
+ "iMessage",
2008
+ "only group chats can be renamed (this space is a DM)"
2009
+ );
2010
+ }
2011
+ const remote = clientForPhone(client, space.phone);
2012
+ await setDisplayName2(remote, space.id, content);
2013
+ };
2014
+ var handleAvatar = async (client, space, content) => {
2015
+ if (isLocal(client)) {
2016
+ throw UnsupportedError.action(
2017
+ "avatar",
2018
+ "iMessage (local mode)",
2019
+ "setting group avatars requires remote iMessage"
2020
+ );
2021
+ }
2022
+ if (space.type !== "group") {
2023
+ throw UnsupportedError.action(
2024
+ "avatar",
2025
+ "iMessage",
2026
+ "only group chats have avatars (this space is a DM)"
2027
+ );
2028
+ }
2029
+ const remote = clientForPhone(client, space.phone);
2030
+ await setIcon2(remote, space.id, content);
2031
+ };
1957
2032
  var imessage = definePlatform("iMessage", {
1958
2033
  config: configSchema,
1959
2034
  static: {
@@ -2032,14 +2107,28 @@ var imessage = definePlatform("iMessage", {
2032
2107
  },
2033
2108
  actions: {
2034
2109
  // Sugar: `space.background(input, opts?)` →
2035
- // `space.send(background(input, opts?))`. Wired through the universal
2110
+ // `space.send(background(input, opts?))`. Routed through the universal
2036
2111
  // send pipeline so the unsupported-content + warn-and-skip path on
2037
2112
  // local-mode iMessage is identical to the canonical form.
2038
- background
2113
+ background: async (space, input, opts) => {
2114
+ await space.send(background(input, opts));
2115
+ },
2116
+ // Sugar: `space.read(message)` → `space.send(read(message))`.
2117
+ read: async (space, message) => {
2118
+ await space.send(read(message));
2119
+ }
2039
2120
  }
2040
2121
  },
2041
2122
  message: {
2042
- schema: messageSchema
2123
+ schema: messageSchema,
2124
+ actions: {
2125
+ // Sugar: `message.read()` → `message.space.send(read(self))`.
2126
+ // `buildMessage` injects the message as the first argument; callers
2127
+ // pass nothing.
2128
+ read: async (message) => {
2129
+ await message.space.send(read(message));
2130
+ }
2131
+ }
2043
2132
  },
2044
2133
  messages: ({ client }) => isLocal(client) ? messages2(client) : messages4(client),
2045
2134
  send: async ({ space, content, client }) => {
@@ -2083,25 +2172,29 @@ var imessage = definePlatform("iMessage", {
2083
2172
  return;
2084
2173
  }
2085
2174
  if (content.type === "typing") {
2086
- if (isLocal(client)) {
2087
- return;
2088
- }
2089
- const remote2 = clientForPhone(client, space.phone);
2090
- if (content.state === "start") {
2091
- await startTyping2(remote2, space.id);
2092
- } else {
2093
- await stopTyping2(remote2, space.id);
2094
- }
2175
+ await handleTyping(client, space, content.state);
2095
2176
  return;
2096
2177
  }
2097
2178
  if (content.type === "edit") {
2098
2179
  await handleEdit(client, space, content);
2099
2180
  return;
2100
2181
  }
2182
+ if (content.type === "rename") {
2183
+ await handleRename(client, space, content);
2184
+ return;
2185
+ }
2186
+ if (content.type === "avatar") {
2187
+ await handleAvatar(client, space, content);
2188
+ return;
2189
+ }
2101
2190
  if (isBackground(content)) {
2102
2191
  await handleBackground(client, space, content);
2103
2192
  return;
2104
2193
  }
2194
+ if (isRead(content)) {
2195
+ await handleRead(client, space);
2196
+ return;
2197
+ }
2105
2198
  if (isLocal(client)) {
2106
2199
  return await send2(client, space.id, content);
2107
2200
  }
@@ -2122,5 +2215,6 @@ var imessage = definePlatform("iMessage", {
2122
2215
  export {
2123
2216
  background,
2124
2217
  effect,
2218
+ read,
2125
2219
  imessage
2126
2220
  };
@@ -0,0 +1,61 @@
1
+ // src/content/poll.ts
2
+ import z from "zod";
3
+ var pollChoiceSchema = z.object({
4
+ title: z.string().nonempty()
5
+ });
6
+ var pollSchema = z.object({
7
+ type: z.literal("poll"),
8
+ title: z.string().nonempty().max(300),
9
+ options: z.array(pollChoiceSchema).min(2).max(10)
10
+ });
11
+ var pollOptionSchema = z.object({
12
+ type: z.literal("poll_option"),
13
+ option: pollChoiceSchema,
14
+ poll: pollSchema,
15
+ selected: z.boolean(),
16
+ title: z.string().nonempty()
17
+ }).superRefine((value, ctx) => {
18
+ if (value.title !== value.option.title) {
19
+ ctx.addIssue({
20
+ code: "custom",
21
+ message: "poll_option title must match option.title",
22
+ path: ["title"]
23
+ });
24
+ }
25
+ if (!value.poll.options.some(
26
+ (pollOption) => pollOption.title === value.option.title
27
+ )) {
28
+ ctx.addIssue({
29
+ code: "custom",
30
+ message: "poll_option option must exist in poll.options",
31
+ path: ["option"]
32
+ });
33
+ }
34
+ });
35
+ var asPoll = (input) => pollSchema.parse({ type: "poll", ...input });
36
+ var asPollOption = (input) => pollOptionSchema.parse({
37
+ type: "poll_option",
38
+ ...input,
39
+ title: input.option.title
40
+ });
41
+ var option = (title) => ({ title });
42
+ var normalize = (raw) => typeof raw === "string" ? { title: raw } : { title: raw.title };
43
+ var collectOptions = (args) => {
44
+ const [first] = args;
45
+ if (args.length === 1 && Array.isArray(first)) {
46
+ return first;
47
+ }
48
+ return args;
49
+ };
50
+ function poll(title, ...rest) {
51
+ return {
52
+ build: async () => asPoll({ title, options: collectOptions(rest).map(normalize) })
53
+ };
54
+ }
55
+
56
+ export {
57
+ asPoll,
58
+ asPollOption,
59
+ option,
60
+ poll
61
+ };