@photon-ai/chat-adapter-imessage 2.1.0 → 2.2.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.
- package/README.md +107 -8
- package/dist/index.d.ts +318 -19
- package/dist/index.js +654 -65
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,15 +1,115 @@
|
|
|
1
1
|
// src/adapter.ts
|
|
2
|
-
import { extractFiles, ValidationError as
|
|
2
|
+
import { extractFiles, ValidationError as ValidationError8 } from "@chat-adapter/shared";
|
|
3
3
|
import { NotImplementedError } from "chat";
|
|
4
4
|
import {
|
|
5
|
+
app as appContent,
|
|
6
|
+
markdown as markdownContent,
|
|
5
7
|
poll as pollContent,
|
|
6
8
|
Spectrum,
|
|
7
9
|
text as textContent
|
|
8
10
|
} from "spectrum-ts";
|
|
9
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
customizedMiniApp,
|
|
13
|
+
effect as effectContent,
|
|
14
|
+
imessage as imessage2
|
|
15
|
+
} from "spectrum-ts/providers/imessage";
|
|
10
16
|
|
|
11
|
-
// src/
|
|
17
|
+
// src/background.ts
|
|
12
18
|
import { ValidationError } from "@chat-adapter/shared";
|
|
19
|
+
import { lookup as lookupMimeType } from "mime-types";
|
|
20
|
+
import { background as backgroundContent } from "spectrum-ts/providers/imessage";
|
|
21
|
+
var IMAGE_MIME_PATTERN = /^image\//i;
|
|
22
|
+
async function toBytes(input) {
|
|
23
|
+
if (input instanceof Uint8Array) {
|
|
24
|
+
return { bytes: Buffer.from(input) };
|
|
25
|
+
}
|
|
26
|
+
if (input instanceof ArrayBuffer) {
|
|
27
|
+
return { bytes: Buffer.from(input) };
|
|
28
|
+
}
|
|
29
|
+
if (input instanceof Blob) {
|
|
30
|
+
return {
|
|
31
|
+
bytes: Buffer.from(await input.arrayBuffer()),
|
|
32
|
+
mimeHint: input.type || void 0
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const data = input.data;
|
|
36
|
+
if (data === void 0) {
|
|
37
|
+
throw new ValidationError(
|
|
38
|
+
"imessage",
|
|
39
|
+
"Chat background must be a Uint8Array, ArrayBuffer, Blob, or FileUpload"
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
const nested = await toBytes(data);
|
|
43
|
+
return {
|
|
44
|
+
bytes: nested.bytes,
|
|
45
|
+
mimeHint: input.mimeType || nested.mimeHint,
|
|
46
|
+
nameHint: input.filename || nested.nameHint
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function toBackgroundUrl(input) {
|
|
50
|
+
if (input instanceof URL) {
|
|
51
|
+
return input;
|
|
52
|
+
}
|
|
53
|
+
if (typeof input !== "string") {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
let url;
|
|
57
|
+
try {
|
|
58
|
+
url = new URL(input);
|
|
59
|
+
} catch {
|
|
60
|
+
throw new ValidationError(
|
|
61
|
+
"imessage",
|
|
62
|
+
`Chat background string input must be an http(s) URL, got "${input}". Pass image bytes for local files.`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
66
|
+
throw new ValidationError(
|
|
67
|
+
"imessage",
|
|
68
|
+
`Chat background URL must be http(s), got "${url.protocol}"`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
return url;
|
|
72
|
+
}
|
|
73
|
+
function resolveBytesMime(hint, name) {
|
|
74
|
+
if (hint && IMAGE_MIME_PATTERN.test(hint)) {
|
|
75
|
+
return hint;
|
|
76
|
+
}
|
|
77
|
+
if (name) {
|
|
78
|
+
const inferred = lookupMimeType(name);
|
|
79
|
+
if (inferred && IMAGE_MIME_PATTERN.test(inferred)) {
|
|
80
|
+
return inferred;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
throw new ValidationError(
|
|
84
|
+
"imessage",
|
|
85
|
+
hint ? `Chat background requires an image/* MIME type, got "${hint}"` : 'Chat background requires an image/* MIME type \u2014 pass options.mimeType (e.g. "image/jpeg") or a name with an image extension'
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
async function resolveBackground(input, options = {}) {
|
|
89
|
+
if (input === "clear") {
|
|
90
|
+
return backgroundContent("clear");
|
|
91
|
+
}
|
|
92
|
+
if (options.mimeType && !IMAGE_MIME_PATTERN.test(options.mimeType)) {
|
|
93
|
+
throw new ValidationError(
|
|
94
|
+
"imessage",
|
|
95
|
+
`Chat background requires an image/* MIME type, got "${options.mimeType}"`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
const url = toBackgroundUrl(input);
|
|
99
|
+
if (url) {
|
|
100
|
+
return backgroundContent(
|
|
101
|
+
url,
|
|
102
|
+
options.mimeType ? { mimeType: options.mimeType } : void 0
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
const { bytes, mimeHint, nameHint } = await toBytes(input);
|
|
106
|
+
const name = options.name ?? nameHint;
|
|
107
|
+
const mimeType = resolveBytesMime(options.mimeType ?? mimeHint, name);
|
|
108
|
+
return backgroundContent(bytes, { mimeType });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// src/config.ts
|
|
112
|
+
import { ValidationError as ValidationError2 } from "@chat-adapter/shared";
|
|
13
113
|
var SHARED_PHONE = "shared";
|
|
14
114
|
var URL_SCHEME_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
15
115
|
function deriveAddress(serverUrl) {
|
|
@@ -45,18 +145,47 @@ function resolveSpectrumConfig(local, auth) {
|
|
|
45
145
|
}
|
|
46
146
|
};
|
|
47
147
|
}
|
|
48
|
-
throw new
|
|
148
|
+
throw new ValidationError2(
|
|
49
149
|
"imessage",
|
|
50
150
|
"Remote mode requires Spectrum Cloud credentials (projectId + projectSecret), explicit clients, or serverUrl + apiKey."
|
|
51
151
|
);
|
|
52
152
|
}
|
|
53
153
|
|
|
154
|
+
// src/effects.ts
|
|
155
|
+
import { ValidationError as ValidationError3 } from "@chat-adapter/shared";
|
|
156
|
+
import { imessage } from "spectrum-ts/providers/imessage";
|
|
157
|
+
var iMessageEffect = imessage.effect.message;
|
|
158
|
+
var EFFECT_IDS = new Set(Object.values(iMessageEffect));
|
|
159
|
+
var EFFECT_NAMES = Object.keys(iMessageEffect);
|
|
160
|
+
function resolveEffect(effect) {
|
|
161
|
+
if (effect in iMessageEffect) {
|
|
162
|
+
return iMessageEffect[effect];
|
|
163
|
+
}
|
|
164
|
+
if (EFFECT_IDS.has(effect)) {
|
|
165
|
+
return effect;
|
|
166
|
+
}
|
|
167
|
+
throw new ValidationError3(
|
|
168
|
+
"imessage",
|
|
169
|
+
`Unknown iMessage effect: "${effect}". Supported: ${EFFECT_NAMES.join(", ")}`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
54
173
|
// src/internal/cache.ts
|
|
55
174
|
var DEFAULT_MAX_SPACES = 256;
|
|
56
175
|
var DEFAULT_MAX_MESSAGES = 1024;
|
|
57
176
|
var InboundCache = class {
|
|
58
177
|
spaces = /* @__PURE__ */ new Map();
|
|
59
178
|
messages = /* @__PURE__ */ new Map();
|
|
179
|
+
/** Sending line last seen per chat GUID — a hint for rebuilding an uncached
|
|
180
|
+
* Space on the right line when several are configured. */
|
|
181
|
+
phones = /* @__PURE__ */ new Map();
|
|
182
|
+
/**
|
|
183
|
+
* Reaction messages this session sent via `addReaction`, keyed by their
|
|
184
|
+
* target so `removeReaction` can `unsend()` them. spectrum-ts models a
|
|
185
|
+
* tapback as its own message, and the only handle to retract one is the
|
|
186
|
+
* `Message` that `react()` returns — there is no by-target removal API.
|
|
187
|
+
*/
|
|
188
|
+
reactions = /* @__PURE__ */ new Map();
|
|
60
189
|
maxSpaces;
|
|
61
190
|
maxMessages;
|
|
62
191
|
constructor(maxSpaces = DEFAULT_MAX_SPACES, maxMessages = DEFAULT_MAX_MESSAGES) {
|
|
@@ -84,10 +213,38 @@ var InboundCache = class {
|
|
|
84
213
|
getSpace(chatGuid) {
|
|
85
214
|
return this.spaces.get(chatGuid);
|
|
86
215
|
}
|
|
216
|
+
/** Record the sending line seen for a chat GUID, if the delivery carried one. */
|
|
217
|
+
rememberPhone(chatGuid, phone) {
|
|
218
|
+
if (!phone) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
this.phones.set(chatGuid, phone);
|
|
222
|
+
evict(this.phones, this.maxSpaces);
|
|
223
|
+
}
|
|
224
|
+
getPhone(chatGuid) {
|
|
225
|
+
return this.phones.get(chatGuid);
|
|
226
|
+
}
|
|
87
227
|
getMessage(id) {
|
|
88
228
|
return this.messages.get(id);
|
|
89
229
|
}
|
|
230
|
+
/** Record the reaction message returned by `react()` for later removal. */
|
|
231
|
+
rememberReaction(targetMessageId, glyph, reaction) {
|
|
232
|
+
this.reactions.set(reactionKey(targetMessageId, glyph), reaction);
|
|
233
|
+
evict(this.reactions, this.maxMessages);
|
|
234
|
+
}
|
|
235
|
+
/** Look up (and forget) a tracked reaction so it can be `unsend()`-ed once. */
|
|
236
|
+
takeReaction(targetMessageId, glyph) {
|
|
237
|
+
const key = reactionKey(targetMessageId, glyph);
|
|
238
|
+
const reaction = this.reactions.get(key);
|
|
239
|
+
if (reaction) {
|
|
240
|
+
this.reactions.delete(key);
|
|
241
|
+
}
|
|
242
|
+
return reaction;
|
|
243
|
+
}
|
|
90
244
|
};
|
|
245
|
+
function reactionKey(targetMessageId, glyph) {
|
|
246
|
+
return `${targetMessageId}::${glyph}`;
|
|
247
|
+
}
|
|
91
248
|
function evict(map, max) {
|
|
92
249
|
if (map.size <= max) {
|
|
93
250
|
return;
|
|
@@ -168,26 +325,31 @@ var MessagePump = class {
|
|
|
168
325
|
import { Message, parseMarkdown } from "chat";
|
|
169
326
|
|
|
170
327
|
// src/internal/thread.ts
|
|
171
|
-
import { ValidationError as
|
|
328
|
+
import { ValidationError as ValidationError4 } from "@chat-adapter/shared";
|
|
172
329
|
var THREAD_PREFIX = "imessage:";
|
|
330
|
+
var LINE_SEP = "~";
|
|
173
331
|
function encodeThreadId(platformData) {
|
|
174
|
-
|
|
332
|
+
const { chatGuid, phone } = platformData;
|
|
333
|
+
return phone ? `${THREAD_PREFIX}${chatGuid}${LINE_SEP}${phone}` : `${THREAD_PREFIX}${chatGuid}`;
|
|
175
334
|
}
|
|
176
335
|
function decodeThreadId(threadId) {
|
|
177
336
|
if (!threadId.startsWith(THREAD_PREFIX)) {
|
|
178
|
-
throw new
|
|
337
|
+
throw new ValidationError4(
|
|
179
338
|
"imessage",
|
|
180
339
|
`Invalid iMessage thread ID: ${threadId}`
|
|
181
340
|
);
|
|
182
341
|
}
|
|
183
|
-
const
|
|
342
|
+
const rest = threadId.slice(THREAD_PREFIX.length);
|
|
343
|
+
const sepIndex = rest.indexOf(LINE_SEP);
|
|
344
|
+
const chatGuid = sepIndex === -1 ? rest : rest.slice(0, sepIndex);
|
|
345
|
+
const phone = sepIndex === -1 ? void 0 : rest.slice(sepIndex + 1);
|
|
184
346
|
if (!chatGuid) {
|
|
185
|
-
throw new
|
|
347
|
+
throw new ValidationError4(
|
|
186
348
|
"imessage",
|
|
187
349
|
`Invalid iMessage thread ID: ${threadId} (empty chat GUID)`
|
|
188
350
|
);
|
|
189
351
|
}
|
|
190
|
-
return { chatGuid };
|
|
352
|
+
return phone ? { chatGuid, phone } : { chatGuid };
|
|
191
353
|
}
|
|
192
354
|
function isDMChatGuid(chatGuid) {
|
|
193
355
|
return chatGuid.includes(";-;");
|
|
@@ -198,7 +360,7 @@ function buildChatMessageFromFields(fields) {
|
|
|
198
360
|
const text = extractText(fields.content);
|
|
199
361
|
return new Message({
|
|
200
362
|
id: fields.id,
|
|
201
|
-
threadId: encodeThreadId({ chatGuid: fields.chatGuid }),
|
|
363
|
+
threadId: encodeThreadId({ chatGuid: fields.chatGuid, phone: fields.phone }),
|
|
202
364
|
text,
|
|
203
365
|
formatted: parseMarkdown(text),
|
|
204
366
|
author: {
|
|
@@ -226,6 +388,7 @@ function buildChatMessage(message, space) {
|
|
|
226
388
|
content: message.content,
|
|
227
389
|
senderId: message.sender?.id ?? "",
|
|
228
390
|
isOutbound: message.direction === "outbound",
|
|
391
|
+
phone: space.phone,
|
|
229
392
|
timestamp: message.timestamp,
|
|
230
393
|
raw: message
|
|
231
394
|
});
|
|
@@ -317,8 +480,8 @@ function titleKey(chatGuid, title) {
|
|
|
317
480
|
}
|
|
318
481
|
|
|
319
482
|
// src/internal/outbound.ts
|
|
320
|
-
import { ValidationError as
|
|
321
|
-
import { lookup as
|
|
483
|
+
import { ValidationError as ValidationError5 } from "@chat-adapter/shared";
|
|
484
|
+
import { lookup as lookupMimeType2 } from "mime-types";
|
|
322
485
|
import { attachment } from "spectrum-ts";
|
|
323
486
|
var EMOJI_GLYPHS = {
|
|
324
487
|
heart: "\u2764\uFE0F",
|
|
@@ -336,7 +499,7 @@ function emojiToGlyph(emoji) {
|
|
|
336
499
|
const name = typeof emoji === "string" ? emoji : emoji.name;
|
|
337
500
|
const glyph = EMOJI_GLYPHS[name];
|
|
338
501
|
if (!glyph) {
|
|
339
|
-
throw new
|
|
502
|
+
throw new ValidationError5(
|
|
340
503
|
"imessage",
|
|
341
504
|
`Unsupported iMessage tapback: "${name}". Supported: heart, thumbs_up, thumbs_down, laugh, emphasize, question`
|
|
342
505
|
);
|
|
@@ -354,7 +517,7 @@ async function fileToAttachment(file) {
|
|
|
354
517
|
buffer = Buffer.from(data);
|
|
355
518
|
}
|
|
356
519
|
const name = file.filename || "attachment";
|
|
357
|
-
const mimeType = file.mimeType ||
|
|
520
|
+
const mimeType = file.mimeType || lookupMimeType2(name) || "application/octet-stream";
|
|
358
521
|
return attachment(buffer, { name, mimeType });
|
|
359
522
|
}
|
|
360
523
|
|
|
@@ -390,6 +553,7 @@ function buildChatMessageFromWebhook(message, space) {
|
|
|
390
553
|
content: message.content,
|
|
391
554
|
senderId: message.sender?.id ?? "",
|
|
392
555
|
isOutbound: message.direction === "outbound",
|
|
556
|
+
phone: space.phone ?? message.space?.phone,
|
|
393
557
|
timestamp: message.timestamp ? new Date(message.timestamp) : /* @__PURE__ */ new Date(),
|
|
394
558
|
raw: message
|
|
395
559
|
});
|
|
@@ -411,7 +575,8 @@ import {
|
|
|
411
575
|
isParagraphNode,
|
|
412
576
|
isStrongNode,
|
|
413
577
|
isTextNode,
|
|
414
|
-
parseMarkdown as parseMarkdown2
|
|
578
|
+
parseMarkdown as parseMarkdown2,
|
|
579
|
+
stringifyMarkdown
|
|
415
580
|
} from "chat";
|
|
416
581
|
var iMessageFormatConverter = class extends BaseFormatConverter {
|
|
417
582
|
/**
|
|
@@ -431,6 +596,30 @@ var iMessageFormatConverter = class extends BaseFormatConverter {
|
|
|
431
596
|
toAst(text) {
|
|
432
597
|
return parseMarkdown2(text);
|
|
433
598
|
}
|
|
599
|
+
/**
|
|
600
|
+
* Decide how a postable message should reach spectrum-ts.
|
|
601
|
+
*
|
|
602
|
+
* Markdown-typed inputs -- `{ markdown }` and `{ ast }` -- carry CommonMark
|
|
603
|
+
* the caller wants styled, so their source is preserved verbatim and flagged
|
|
604
|
+
* `markdown: true`; the adapter sends it via spectrum's `markdown()` builder,
|
|
605
|
+
* which renders bold/italic/links/lists as native iMessage styled text.
|
|
606
|
+
*
|
|
607
|
+
* Everything else is pass-through-as-is by contract -- a plain `string` or
|
|
608
|
+
* `{ raw }` must not have stray `*`/`_` reinterpreted as formatting, and
|
|
609
|
+
* cards fall back to plain text -- so those are rendered to plain text and
|
|
610
|
+
* flagged `markdown: false`.
|
|
611
|
+
*/
|
|
612
|
+
renderPostableContent(message) {
|
|
613
|
+
if (message && typeof message === "object") {
|
|
614
|
+
if ("markdown" in message && typeof message.markdown === "string") {
|
|
615
|
+
return { body: message.markdown, markdown: true };
|
|
616
|
+
}
|
|
617
|
+
if ("ast" in message && message.ast) {
|
|
618
|
+
return { body: stringifyMarkdown(message.ast), markdown: true };
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
return { body: this.renderPostable(message), markdown: false };
|
|
622
|
+
}
|
|
434
623
|
nodeToPlainText(node) {
|
|
435
624
|
if (isParagraphNode(node)) {
|
|
436
625
|
return getNodeChildren(node).map((child) => this.nodeToPlainText(child)).join("");
|
|
@@ -478,6 +667,169 @@ var iMessageFormatConverter = class extends BaseFormatConverter {
|
|
|
478
667
|
}
|
|
479
668
|
};
|
|
480
669
|
|
|
670
|
+
// src/miniapp.ts
|
|
671
|
+
import { ValidationError as ValidationError6 } from "@chat-adapter/shared";
|
|
672
|
+
async function toImageBytes(image) {
|
|
673
|
+
if (image instanceof Uint8Array) {
|
|
674
|
+
return new Uint8Array(image);
|
|
675
|
+
}
|
|
676
|
+
if (image instanceof ArrayBuffer) {
|
|
677
|
+
return new Uint8Array(image);
|
|
678
|
+
}
|
|
679
|
+
if (image instanceof Blob) {
|
|
680
|
+
return new Uint8Array(await image.arrayBuffer());
|
|
681
|
+
}
|
|
682
|
+
const data = image.data;
|
|
683
|
+
if (data === void 0) {
|
|
684
|
+
throw new ValidationError6(
|
|
685
|
+
"imessage",
|
|
686
|
+
"Mini-app card image must be a Uint8Array, ArrayBuffer, Blob, or FileUpload"
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
return toImageBytes(data);
|
|
690
|
+
}
|
|
691
|
+
function requireField(value, field) {
|
|
692
|
+
if (!value || value.trim().length === 0) {
|
|
693
|
+
throw new ValidationError6(
|
|
694
|
+
"imessage",
|
|
695
|
+
`Mini-app card requires a non-empty "${field}"`
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
return value;
|
|
699
|
+
}
|
|
700
|
+
async function resolveMiniApp(card) {
|
|
701
|
+
const url = typeof card.url === "string" ? card.url : card.url.toString();
|
|
702
|
+
try {
|
|
703
|
+
new URL(url);
|
|
704
|
+
} catch {
|
|
705
|
+
throw new ValidationError6(
|
|
706
|
+
"imessage",
|
|
707
|
+
`Mini-app card has an invalid url: "${url}"`
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
const layout = card.layout ?? {};
|
|
711
|
+
const image = layout.image ? await toImageBytes(layout.image) : void 0;
|
|
712
|
+
return {
|
|
713
|
+
appName: requireField(card.appName, "appName"),
|
|
714
|
+
teamId: requireField(card.teamId, "teamId"),
|
|
715
|
+
extensionBundleId: requireField(
|
|
716
|
+
card.extensionBundleId,
|
|
717
|
+
"extensionBundleId"
|
|
718
|
+
),
|
|
719
|
+
url,
|
|
720
|
+
...card.appStoreId === void 0 ? {} : { appStoreId: card.appStoreId },
|
|
721
|
+
layout: {
|
|
722
|
+
caption: layout.caption,
|
|
723
|
+
subcaption: layout.subcaption,
|
|
724
|
+
trailingCaption: layout.trailingCaption,
|
|
725
|
+
trailingSubcaption: layout.trailingSubcaption,
|
|
726
|
+
imageTitle: layout.imageTitle,
|
|
727
|
+
imageSubtitle: layout.imageSubtitle,
|
|
728
|
+
summary: layout.summary,
|
|
729
|
+
...image ? { image } : {}
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
function isAppUrl(input) {
|
|
734
|
+
return typeof input === "string" || typeof input === "function" || typeof input === "object" && input !== null && typeof input.then === "function";
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// src/voice.ts
|
|
738
|
+
import { ValidationError as ValidationError7 } from "@chat-adapter/shared";
|
|
739
|
+
import { lookup as lookupMimeType3 } from "mime-types";
|
|
740
|
+
import { voice as voiceContent } from "spectrum-ts";
|
|
741
|
+
var AUDIO_MIME_PATTERN = /^audio\//i;
|
|
742
|
+
async function toBytes2(input) {
|
|
743
|
+
if (input instanceof Uint8Array) {
|
|
744
|
+
return { bytes: Buffer.from(input) };
|
|
745
|
+
}
|
|
746
|
+
if (input instanceof ArrayBuffer) {
|
|
747
|
+
return { bytes: Buffer.from(input) };
|
|
748
|
+
}
|
|
749
|
+
if (input instanceof Blob) {
|
|
750
|
+
return {
|
|
751
|
+
bytes: Buffer.from(await input.arrayBuffer()),
|
|
752
|
+
mimeHint: input.type || void 0
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
const data = input.data;
|
|
756
|
+
if (data === void 0) {
|
|
757
|
+
throw new ValidationError7(
|
|
758
|
+
"imessage",
|
|
759
|
+
"Voice message must be a Uint8Array, ArrayBuffer, Blob, or FileUpload"
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
const nested = await toBytes2(data);
|
|
763
|
+
return {
|
|
764
|
+
bytes: nested.bytes,
|
|
765
|
+
mimeHint: input.mimeType || nested.mimeHint,
|
|
766
|
+
nameHint: input.filename || nested.nameHint
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
function toVoiceUrl(input) {
|
|
770
|
+
if (input instanceof URL) {
|
|
771
|
+
return input;
|
|
772
|
+
}
|
|
773
|
+
if (typeof input !== "string") {
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
let url;
|
|
777
|
+
try {
|
|
778
|
+
url = new URL(input);
|
|
779
|
+
} catch {
|
|
780
|
+
throw new ValidationError7(
|
|
781
|
+
"imessage",
|
|
782
|
+
`Voice message string input must be an http(s) URL, got "${input}". Pass audio bytes for local files.`
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
786
|
+
throw new ValidationError7(
|
|
787
|
+
"imessage",
|
|
788
|
+
`Voice message URL must be http(s), got "${url.protocol}"`
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
return url;
|
|
792
|
+
}
|
|
793
|
+
function resolveBytesMime2(hint, name) {
|
|
794
|
+
if (hint && AUDIO_MIME_PATTERN.test(hint)) {
|
|
795
|
+
return hint;
|
|
796
|
+
}
|
|
797
|
+
if (name) {
|
|
798
|
+
const inferred = lookupMimeType3(name);
|
|
799
|
+
if (inferred && AUDIO_MIME_PATTERN.test(inferred)) {
|
|
800
|
+
return inferred;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
throw new ValidationError7(
|
|
804
|
+
"imessage",
|
|
805
|
+
hint ? `Voice message requires an audio/* MIME type, got "${hint}"` : 'Voice message requires an audio/* MIME type \u2014 pass options.mimeType (e.g. "audio/mp4") or a name with an audio extension'
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
async function resolveVoice(input, options = {}) {
|
|
809
|
+
if (options.mimeType && !AUDIO_MIME_PATTERN.test(options.mimeType)) {
|
|
810
|
+
throw new ValidationError7(
|
|
811
|
+
"imessage",
|
|
812
|
+
`Voice message requires an audio/* MIME type, got "${options.mimeType}"`
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
const url = toVoiceUrl(input);
|
|
816
|
+
if (url) {
|
|
817
|
+
return voiceContent(url, {
|
|
818
|
+
...options.mimeType ? { mimeType: options.mimeType } : {},
|
|
819
|
+
...options.name ? { name: options.name } : {},
|
|
820
|
+
...options.duration === void 0 ? {} : { duration: options.duration }
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
const { bytes, mimeHint, nameHint } = await toBytes2(input);
|
|
824
|
+
const name = options.name ?? nameHint;
|
|
825
|
+
const mimeType = resolveBytesMime2(options.mimeType ?? mimeHint, name);
|
|
826
|
+
return voiceContent(bytes, {
|
|
827
|
+
mimeType,
|
|
828
|
+
...name ? { name } : {},
|
|
829
|
+
...options.duration === void 0 ? {} : { duration: options.duration }
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
|
|
481
833
|
// src/adapter.ts
|
|
482
834
|
var TYPING_DURATION_MS = 3e3;
|
|
483
835
|
var iMessageAdapter = class {
|
|
@@ -491,8 +843,10 @@ var iMessageAdapter = class {
|
|
|
491
843
|
clients;
|
|
492
844
|
phone;
|
|
493
845
|
webhookSecret;
|
|
494
|
-
/** The spectrum-ts instance — null until `initialize()` runs. */
|
|
846
|
+
/** The spectrum-ts instance — null until `initialize()` or `ensureApp()` runs. */
|
|
495
847
|
app = null;
|
|
848
|
+
/** In-flight app build, so concurrent callers share one construction. */
|
|
849
|
+
appBuild = null;
|
|
496
850
|
chat = null;
|
|
497
851
|
logger;
|
|
498
852
|
formatConverter = new iMessageFormatConverter();
|
|
@@ -502,7 +856,7 @@ var iMessageAdapter = class {
|
|
|
502
856
|
pump = null;
|
|
503
857
|
constructor(config) {
|
|
504
858
|
if (config.local && process.platform !== "darwin") {
|
|
505
|
-
throw new
|
|
859
|
+
throw new ValidationError8(
|
|
506
860
|
"imessage",
|
|
507
861
|
"iMessage adapter local mode requires macOS. Current platform: " + process.platform
|
|
508
862
|
);
|
|
@@ -521,6 +875,34 @@ var iMessageAdapter = class {
|
|
|
521
875
|
}
|
|
522
876
|
async initialize(chat) {
|
|
523
877
|
this.chat = chat;
|
|
878
|
+
await this.ensureApp();
|
|
879
|
+
this.pump = new MessagePump(
|
|
880
|
+
() => {
|
|
881
|
+
if (!this.app) {
|
|
882
|
+
throw new Error("Adapter not initialized");
|
|
883
|
+
}
|
|
884
|
+
return this.app.messages;
|
|
885
|
+
},
|
|
886
|
+
(space, message) => this.routeInbound(space, message, this.gatewayOptions),
|
|
887
|
+
this.logger
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* Build the spectrum-ts app on demand. eve may call the adapter in an
|
|
892
|
+
* invocation that never ran `initialize()` (e.g. a Vercel Workflow reply
|
|
893
|
+
* callback), leaving `this.app` null — every send funnels through here first.
|
|
894
|
+
*/
|
|
895
|
+
async ensureApp() {
|
|
896
|
+
if (this.app) {
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
this.appBuild ??= this.buildApp().catch((error) => {
|
|
900
|
+
this.appBuild = null;
|
|
901
|
+
throw error;
|
|
902
|
+
});
|
|
903
|
+
await this.appBuild;
|
|
904
|
+
}
|
|
905
|
+
async buildApp() {
|
|
524
906
|
const { providerConfig, projectId, projectSecret } = resolveSpectrumConfig(
|
|
525
907
|
this.local,
|
|
526
908
|
{
|
|
@@ -532,18 +914,8 @@ var iMessageAdapter = class {
|
|
|
532
914
|
serverUrl: this.serverUrl
|
|
533
915
|
}
|
|
534
916
|
);
|
|
535
|
-
const providers = [
|
|
917
|
+
const providers = [imessage2.config(providerConfig)];
|
|
536
918
|
this.app = projectId && projectSecret ? await Spectrum({ providers, projectId, projectSecret }) : await Spectrum({ providers });
|
|
537
|
-
this.pump = new MessagePump(
|
|
538
|
-
() => {
|
|
539
|
-
if (!this.app) {
|
|
540
|
-
throw new Error("Adapter not initialized");
|
|
541
|
-
}
|
|
542
|
-
return this.app.messages;
|
|
543
|
-
},
|
|
544
|
-
(space, message) => this.routeInbound(space, message, this.gatewayOptions),
|
|
545
|
-
this.logger
|
|
546
|
-
);
|
|
547
919
|
let mode;
|
|
548
920
|
if (this.local) {
|
|
549
921
|
mode = "local";
|
|
@@ -612,26 +984,152 @@ var iMessageAdapter = class {
|
|
|
612
984
|
this.routeWebhookMessage(payload, options);
|
|
613
985
|
return new Response(null, { status: 200 });
|
|
614
986
|
}
|
|
987
|
+
/**
|
|
988
|
+
* Build the spectrum content for an outbound message. Markdown-typed inputs
|
|
989
|
+
* are sent via `markdown()` so remote iMessage renders them as native styled
|
|
990
|
+
* text; raw/string/card inputs stay plain `text()`. Returns the rendered
|
|
991
|
+
* `body` too so callers can skip an empty send.
|
|
992
|
+
*/
|
|
993
|
+
toSpectrumContent(message) {
|
|
994
|
+
const { body, markdown } = this.formatConverter.renderPostableContent(message);
|
|
995
|
+
return {
|
|
996
|
+
body,
|
|
997
|
+
content: markdown ? markdownContent(body) : textContent(body)
|
|
998
|
+
};
|
|
999
|
+
}
|
|
615
1000
|
async postMessage(threadId, message) {
|
|
616
1001
|
const space = await this.requireSpace(threadId, "postMessage");
|
|
617
|
-
const body = this.
|
|
1002
|
+
const { body, content } = this.toSpectrumContent(message);
|
|
618
1003
|
const files = extractFiles(message);
|
|
619
1004
|
let first;
|
|
620
1005
|
if (body && body.trim().length > 0) {
|
|
621
|
-
first = await space.send(
|
|
1006
|
+
first = await space.send(content) ?? first;
|
|
622
1007
|
}
|
|
623
1008
|
for (const file of files) {
|
|
624
1009
|
const sent = await space.send(await fileToAttachment(file)) ?? void 0;
|
|
625
1010
|
first ??= sent;
|
|
626
1011
|
}
|
|
627
1012
|
if (!first) {
|
|
628
|
-
throw new
|
|
1013
|
+
throw new ValidationError8(
|
|
629
1014
|
"imessage",
|
|
630
1015
|
"postMessage requires non-empty text or at least one attachment"
|
|
631
1016
|
);
|
|
632
1017
|
}
|
|
633
1018
|
return { id: first.id, threadId, raw: first };
|
|
634
1019
|
}
|
|
1020
|
+
/**
|
|
1021
|
+
* Send a message with an iMessage expressive-send effect — a bubble effect
|
|
1022
|
+
* (`slam`, `loud`, `gentle`, `invisible`) or a full-screen effect
|
|
1023
|
+
* (`confetti`, `fireworks`, `balloons`, `heart`, `lasers`, `celebration`,
|
|
1024
|
+
* `sparkles`, `spotlight`, `echo`). Not part of the Chat SDK `Adapter`
|
|
1025
|
+
* interface — exposed as an adapter-specific extra (e.g. celebratory confetti
|
|
1026
|
+
* on task completion). Remote only.
|
|
1027
|
+
*
|
|
1028
|
+
* The `effect` argument accepts a friendly name (`"confetti"`) or a value from
|
|
1029
|
+
* the re-exported `iMessageEffect` map. Effects attach to text content only,
|
|
1030
|
+
* so this requires non-empty text.
|
|
1031
|
+
*/
|
|
1032
|
+
async sendEffect(threadId, message, effect) {
|
|
1033
|
+
if (this.local) {
|
|
1034
|
+
throw new NotImplementedError(
|
|
1035
|
+
"sendEffect is not supported in local mode",
|
|
1036
|
+
"sendEffect"
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
const space = await this.requireSpace(threadId, "sendEffect");
|
|
1040
|
+
const { body, content } = this.toSpectrumContent(message);
|
|
1041
|
+
if (!body || body.trim().length === 0) {
|
|
1042
|
+
throw new ValidationError8(
|
|
1043
|
+
"imessage",
|
|
1044
|
+
"sendEffect requires non-empty text content"
|
|
1045
|
+
);
|
|
1046
|
+
}
|
|
1047
|
+
const effectId = resolveEffect(effect);
|
|
1048
|
+
const sent = await space.send(effectContent(content, effectId));
|
|
1049
|
+
if (!sent) {
|
|
1050
|
+
throw new ValidationError8(
|
|
1051
|
+
"imessage",
|
|
1052
|
+
"sendEffect could not send the message"
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
1055
|
+
return { id: sent.id, threadId, raw: sent };
|
|
1056
|
+
}
|
|
1057
|
+
async sendMiniApp(threadId, input) {
|
|
1058
|
+
if (this.local) {
|
|
1059
|
+
throw new NotImplementedError(
|
|
1060
|
+
"sendMiniApp is not supported in local mode",
|
|
1061
|
+
"sendMiniApp"
|
|
1062
|
+
);
|
|
1063
|
+
}
|
|
1064
|
+
const space = await this.requireSpace(threadId, "sendMiniApp");
|
|
1065
|
+
const content = isAppUrl(input) ? appContent(input) : customizedMiniApp(await resolveMiniApp(input));
|
|
1066
|
+
const sent = await space.send(content);
|
|
1067
|
+
if (!sent) {
|
|
1068
|
+
throw new ValidationError8(
|
|
1069
|
+
"imessage",
|
|
1070
|
+
"sendMiniApp could not send the card"
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
return { id: sent.id, threadId, raw: sent };
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Send a native iMessage voice note — a real, playable waveform bubble (the
|
|
1077
|
+
* message renders with `isAudioMessage`), not an audio file dropped in as an
|
|
1078
|
+
* attachment. A natural fit for TTS-capable bots that reply with speech. Not
|
|
1079
|
+
* part of the Chat SDK `Adapter` interface — exposed as an adapter-specific
|
|
1080
|
+
* extra. Remote only.
|
|
1081
|
+
*
|
|
1082
|
+
* The `input` is either in-memory audio bytes (`Uint8Array` / `Buffer` /
|
|
1083
|
+
* `ArrayBuffer`, a `Blob`, or a Chat SDK `FileUpload`) or an `http(s)` URL (a
|
|
1084
|
+
* `URL` or a string) that is fetched at send time. Audio bytes need an
|
|
1085
|
+
* `audio/*` MIME type — supply `options.mimeType` (e.g. `"audio/mp4"`) or an
|
|
1086
|
+
* `options.name` with an audio extension when it can't be inferred.
|
|
1087
|
+
*/
|
|
1088
|
+
async sendVoice(threadId, input, options) {
|
|
1089
|
+
if (this.local) {
|
|
1090
|
+
throw new NotImplementedError(
|
|
1091
|
+
"sendVoice is not supported in local mode",
|
|
1092
|
+
"sendVoice"
|
|
1093
|
+
);
|
|
1094
|
+
}
|
|
1095
|
+
const space = await this.requireSpace(threadId, "sendVoice");
|
|
1096
|
+
const content = await resolveVoice(input, options);
|
|
1097
|
+
const sent = await space.send(content);
|
|
1098
|
+
if (!sent) {
|
|
1099
|
+
throw new ValidationError8(
|
|
1100
|
+
"imessage",
|
|
1101
|
+
"sendVoice could not send the voice message"
|
|
1102
|
+
);
|
|
1103
|
+
}
|
|
1104
|
+
return { id: sent.id, threadId, raw: sent };
|
|
1105
|
+
}
|
|
1106
|
+
/**
|
|
1107
|
+
* Set or clear the chat background — the wallpaper behind a conversation, an
|
|
1108
|
+
* iMessage-only touch with no analog on the plain-text competitors. Not part
|
|
1109
|
+
* of the Chat SDK `Adapter` interface — exposed as an adapter-specific extra.
|
|
1110
|
+
* Remote only.
|
|
1111
|
+
*
|
|
1112
|
+
* The `input` is either the literal `"clear"` (to remove the current
|
|
1113
|
+
* background), in-memory image bytes (`Uint8Array` / `Buffer` / `ArrayBuffer`,
|
|
1114
|
+
* a `Blob`, or a Chat SDK `FileUpload`), or an `http(s)` URL (a `URL` or a
|
|
1115
|
+
* string) that is fetched at send time. Image bytes need an `image/*` MIME
|
|
1116
|
+
* type — supply `options.mimeType` (e.g. `"image/jpeg"`) or an `options.name`
|
|
1117
|
+
* with an image extension when it can't be inferred.
|
|
1118
|
+
*
|
|
1119
|
+
* Fire-and-forget: iMessage acknowledges the control signal without returning
|
|
1120
|
+
* a message, so this resolves to `void` rather than a {@link RawMessage}.
|
|
1121
|
+
*/
|
|
1122
|
+
async setBackground(threadId, input, options) {
|
|
1123
|
+
if (this.local) {
|
|
1124
|
+
throw new NotImplementedError(
|
|
1125
|
+
"setBackground is not supported in local mode",
|
|
1126
|
+
"setBackground"
|
|
1127
|
+
);
|
|
1128
|
+
}
|
|
1129
|
+
const space = await this.requireSpace(threadId, "setBackground");
|
|
1130
|
+
const content = await resolveBackground(input, options);
|
|
1131
|
+
await space.send(content);
|
|
1132
|
+
}
|
|
635
1133
|
async editMessage(threadId, messageId, message) {
|
|
636
1134
|
if (this.local) {
|
|
637
1135
|
throw new NotImplementedError(
|
|
@@ -646,21 +1144,42 @@ var iMessageAdapter = class {
|
|
|
646
1144
|
"editMessage"
|
|
647
1145
|
);
|
|
648
1146
|
}
|
|
649
|
-
await target.edit(
|
|
650
|
-
textContent(this.formatConverter.renderPostable(message))
|
|
651
|
-
);
|
|
1147
|
+
await target.edit(this.toSpectrumContent(message).content);
|
|
652
1148
|
return { id: messageId, threadId, raw: target };
|
|
653
1149
|
}
|
|
654
|
-
async deleteMessage(
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
1150
|
+
async deleteMessage(threadId, messageId) {
|
|
1151
|
+
if (this.local) {
|
|
1152
|
+
throw new NotImplementedError(
|
|
1153
|
+
"deleteMessage is not supported in local mode",
|
|
1154
|
+
"deleteMessage"
|
|
1155
|
+
);
|
|
1156
|
+
}
|
|
1157
|
+
const target = await this.resolveMessage(threadId, messageId);
|
|
1158
|
+
if (!target) {
|
|
1159
|
+
throw new NotImplementedError(
|
|
1160
|
+
"deleteMessage requires the target message to have been sent or received in this session",
|
|
1161
|
+
"deleteMessage"
|
|
1162
|
+
);
|
|
1163
|
+
}
|
|
1164
|
+
await target.unsend();
|
|
659
1165
|
}
|
|
660
1166
|
parseMessage(raw) {
|
|
661
1167
|
const message = raw;
|
|
662
1168
|
return buildChatMessage(message, message.space);
|
|
663
1169
|
}
|
|
1170
|
+
/**
|
|
1171
|
+
* Fetch a single message by id. spectrum-ts can resolve a message by id
|
|
1172
|
+
* (from the inbound cache or the provider's by-id lookup) even though it has
|
|
1173
|
+
* no paginated history API, so single-message reads work where
|
|
1174
|
+
* `fetchMessages` cannot. Returns `null` when the message can't be resolved.
|
|
1175
|
+
*/
|
|
1176
|
+
async fetchMessage(threadId, messageId) {
|
|
1177
|
+
const target = await this.resolveMessage(threadId, messageId);
|
|
1178
|
+
if (!target) {
|
|
1179
|
+
return null;
|
|
1180
|
+
}
|
|
1181
|
+
return this.parseMessage(target);
|
|
1182
|
+
}
|
|
664
1183
|
async fetchMessages(_threadId, _options) {
|
|
665
1184
|
throw new NotImplementedError(
|
|
666
1185
|
"fetchMessages (message history) is not supported by spectrum-ts",
|
|
@@ -691,13 +1210,27 @@ var iMessageAdapter = class {
|
|
|
691
1210
|
"addReaction"
|
|
692
1211
|
);
|
|
693
1212
|
}
|
|
694
|
-
await target.react(glyph);
|
|
1213
|
+
const reaction = await target.react(glyph);
|
|
1214
|
+
if (reaction) {
|
|
1215
|
+
this.cache.rememberReaction(messageId, glyph, reaction);
|
|
1216
|
+
}
|
|
695
1217
|
}
|
|
696
|
-
async removeReaction(_threadId,
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
1218
|
+
async removeReaction(_threadId, messageId, emoji) {
|
|
1219
|
+
if (this.local) {
|
|
1220
|
+
throw new NotImplementedError(
|
|
1221
|
+
"removeReaction is not supported in local mode",
|
|
1222
|
+
"removeReaction"
|
|
1223
|
+
);
|
|
1224
|
+
}
|
|
1225
|
+
const glyph = emojiToGlyph(emoji);
|
|
1226
|
+
const reaction = this.cache.takeReaction(messageId, glyph);
|
|
1227
|
+
if (!reaction) {
|
|
1228
|
+
throw new NotImplementedError(
|
|
1229
|
+
"removeReaction requires the reaction to have been added via addReaction in this session",
|
|
1230
|
+
"removeReaction"
|
|
1231
|
+
);
|
|
1232
|
+
}
|
|
1233
|
+
await reaction.unsend();
|
|
701
1234
|
}
|
|
702
1235
|
async startTyping(threadId, _status) {
|
|
703
1236
|
if (this.local) {
|
|
@@ -713,6 +1246,48 @@ var iMessageAdapter = class {
|
|
|
713
1246
|
});
|
|
714
1247
|
}, TYPING_DURATION_MS);
|
|
715
1248
|
}
|
|
1249
|
+
/**
|
|
1250
|
+
* Cold-start a DM with a phone number / handle. spectrum-ts resolves (or
|
|
1251
|
+
* creates) the 1:1 conversation from the participant via `space.create`, so
|
|
1252
|
+
* the bot can message a user it has never received from. Returns the encoded
|
|
1253
|
+
* thread id, ready to pass to `postMessage`.
|
|
1254
|
+
*/
|
|
1255
|
+
async openDM(userId) {
|
|
1256
|
+
await this.ensureApp();
|
|
1257
|
+
if (!this.app) {
|
|
1258
|
+
throw new NotImplementedError(
|
|
1259
|
+
"openDM requires the adapter to be initialized",
|
|
1260
|
+
"openDM"
|
|
1261
|
+
);
|
|
1262
|
+
}
|
|
1263
|
+
const space = await this.platformSpaces().create(userId);
|
|
1264
|
+
this.cache.rememberSpace(space);
|
|
1265
|
+
return encodeThreadId({
|
|
1266
|
+
chatGuid: space.id,
|
|
1267
|
+
phone: space.phone
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* Mark a received message (and the conversation up to it) as read, surfacing
|
|
1272
|
+
* a read receipt where iMessage supports one. Remote only; not part of the
|
|
1273
|
+
* Chat SDK `Adapter` interface — exposed as an adapter-specific extra.
|
|
1274
|
+
*/
|
|
1275
|
+
async markRead(threadId, messageId) {
|
|
1276
|
+
if (this.local) {
|
|
1277
|
+
throw new NotImplementedError(
|
|
1278
|
+
"markRead is not supported in local mode",
|
|
1279
|
+
"markRead"
|
|
1280
|
+
);
|
|
1281
|
+
}
|
|
1282
|
+
const target = await this.resolveMessage(threadId, messageId);
|
|
1283
|
+
if (!target) {
|
|
1284
|
+
throw new NotImplementedError(
|
|
1285
|
+
"markRead requires the target message to have been received in this session",
|
|
1286
|
+
"markRead"
|
|
1287
|
+
);
|
|
1288
|
+
}
|
|
1289
|
+
await target.read();
|
|
1290
|
+
}
|
|
716
1291
|
async openModal(triggerId, modal, contextId) {
|
|
717
1292
|
if (this.local) {
|
|
718
1293
|
throw new NotImplementedError(
|
|
@@ -724,14 +1299,14 @@ var iMessageAdapter = class {
|
|
|
724
1299
|
(c) => c.type === "select"
|
|
725
1300
|
);
|
|
726
1301
|
if (!select) {
|
|
727
|
-
throw new
|
|
1302
|
+
throw new ValidationError8(
|
|
728
1303
|
"imessage",
|
|
729
1304
|
"openModal requires at least one Select child \u2014 iMessage modals map to native polls"
|
|
730
1305
|
);
|
|
731
1306
|
}
|
|
732
1307
|
const labels = select.options.map((o) => o.label);
|
|
733
1308
|
if (labels.length < 2 || labels.length > 10) {
|
|
734
|
-
throw new
|
|
1309
|
+
throw new ValidationError8(
|
|
735
1310
|
"imessage",
|
|
736
1311
|
`iMessage polls require between 2 and 10 options, received ${labels.length}`
|
|
737
1312
|
);
|
|
@@ -813,6 +1388,7 @@ var iMessageAdapter = class {
|
|
|
813
1388
|
return;
|
|
814
1389
|
}
|
|
815
1390
|
const { message, space } = payload;
|
|
1391
|
+
this.cache.rememberPhone(space.id, space.phone ?? message.space?.phone);
|
|
816
1392
|
if (message.direction === "outbound") {
|
|
817
1393
|
return;
|
|
818
1394
|
}
|
|
@@ -887,29 +1463,25 @@ var iMessageAdapter = class {
|
|
|
887
1463
|
);
|
|
888
1464
|
}
|
|
889
1465
|
/**
|
|
890
|
-
* Resolve a sendable spectrum-ts `Space` for a thread.
|
|
891
|
-
*
|
|
892
|
-
*
|
|
893
|
-
*
|
|
894
|
-
* a cold send — it rebuilds the Space from the chat GUID via
|
|
895
|
-
* `imessage(app).space.get(chatGuid)`, which works for DMs and groups alike.
|
|
896
|
-
* The rebuild can still fail when several iMessage lines are configured and
|
|
897
|
-
* spectrum-ts cannot infer which line the chat belongs to (`space.get`
|
|
898
|
-
* requires `params.phone` there). Returns `undefined` when no Space can be
|
|
899
|
-
* obtained.
|
|
1466
|
+
* Resolve a sendable spectrum-ts `Space` for a thread. Prefers a cached live
|
|
1467
|
+
* Space; otherwise rebuilds it from the chat GUID, passing the sending line
|
|
1468
|
+
* so `space.get` can pick it when multiple lines are configured. Returns
|
|
1469
|
+
* `undefined` when no Space can be obtained.
|
|
900
1470
|
*/
|
|
901
1471
|
async resolveSpace(threadId) {
|
|
902
|
-
const { chatGuid } = decodeThreadId(threadId);
|
|
1472
|
+
const { chatGuid, phone: threadPhone } = decodeThreadId(threadId);
|
|
903
1473
|
const cached = this.cache.getSpace(chatGuid);
|
|
904
1474
|
if (cached) {
|
|
905
1475
|
return cached;
|
|
906
1476
|
}
|
|
1477
|
+
await this.ensureApp();
|
|
907
1478
|
if (!this.app) {
|
|
908
1479
|
return;
|
|
909
1480
|
}
|
|
910
|
-
const platform = imessage(this.app);
|
|
911
1481
|
try {
|
|
912
|
-
const
|
|
1482
|
+
const phone = threadPhone ?? this.cache.getPhone(chatGuid);
|
|
1483
|
+
const spaces = this.platformSpaces();
|
|
1484
|
+
const space = phone ? await spaces.get(chatGuid, { phone }) : await spaces.get(chatGuid);
|
|
913
1485
|
this.cache.rememberSpace(space);
|
|
914
1486
|
return space;
|
|
915
1487
|
} catch (error) {
|
|
@@ -920,6 +1492,17 @@ var iMessageAdapter = class {
|
|
|
920
1492
|
return;
|
|
921
1493
|
}
|
|
922
1494
|
}
|
|
1495
|
+
/**
|
|
1496
|
+
* The iMessage provider's Space namespace (`get` / `create`). `HasProvider`
|
|
1497
|
+
* over the default provider tuple won't narrow to `true`, so `imessage(app)`
|
|
1498
|
+
* types as `never` — cast to the slice of the instance we use.
|
|
1499
|
+
*/
|
|
1500
|
+
platformSpaces() {
|
|
1501
|
+
if (!this.app) {
|
|
1502
|
+
throw new Error("Adapter not initialized");
|
|
1503
|
+
}
|
|
1504
|
+
return imessage2(this.app).space;
|
|
1505
|
+
}
|
|
923
1506
|
async requireSpace(threadId, action) {
|
|
924
1507
|
const space = await this.resolveSpace(threadId);
|
|
925
1508
|
if (!space) {
|
|
@@ -950,7 +1533,7 @@ function toClientArray(clients) {
|
|
|
950
1533
|
}
|
|
951
1534
|
|
|
952
1535
|
// src/factory.ts
|
|
953
|
-
import { ValidationError as
|
|
1536
|
+
import { ValidationError as ValidationError9 } from "@chat-adapter/shared";
|
|
954
1537
|
import { ConsoleLogger } from "chat";
|
|
955
1538
|
function resolveLocalMode(explicit, hasRemoteCreds) {
|
|
956
1539
|
if (explicit !== void 0) {
|
|
@@ -970,13 +1553,13 @@ function assertRemoteCredentials(creds) {
|
|
|
970
1553
|
return;
|
|
971
1554
|
}
|
|
972
1555
|
if (!creds.hasServerUrl) {
|
|
973
|
-
throw new
|
|
1556
|
+
throw new ValidationError9(
|
|
974
1557
|
"imessage",
|
|
975
1558
|
"serverUrl is required when local is false. Set IMESSAGE_SERVER_URL (or use IMESSAGE_PROJECT_ID/IMESSAGE_PROJECT_SECRET for Spectrum Cloud), or provide it in config."
|
|
976
1559
|
);
|
|
977
1560
|
}
|
|
978
1561
|
if (!creds.hasApiKey) {
|
|
979
|
-
throw new
|
|
1562
|
+
throw new ValidationError9(
|
|
980
1563
|
"imessage",
|
|
981
1564
|
"apiKey is required when local is false. Set IMESSAGE_API_KEY or provide it in config."
|
|
982
1565
|
);
|
|
@@ -1016,6 +1599,12 @@ export {
|
|
|
1016
1599
|
createiMessageAdapter,
|
|
1017
1600
|
deriveAddress,
|
|
1018
1601
|
iMessageAdapter,
|
|
1019
|
-
|
|
1602
|
+
iMessageEffect,
|
|
1603
|
+
iMessageFormatConverter,
|
|
1604
|
+
isAppUrl,
|
|
1605
|
+
resolveBackground,
|
|
1606
|
+
resolveEffect,
|
|
1607
|
+
resolveMiniApp,
|
|
1608
|
+
resolveVoice
|
|
1020
1609
|
};
|
|
1021
1610
|
//# sourceMappingURL=index.js.map
|