@photon-ai/chat-adapter-imessage 2.1.0 → 2.2.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.
- package/README.md +107 -8
- package/dist/index.d.ts +299 -8
- package/dist/index.js +585 -39
- 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,44 @@ 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
|
+
/**
|
|
180
|
+
* Reaction messages this session sent via `addReaction`, keyed by their
|
|
181
|
+
* target so `removeReaction` can `unsend()` them. spectrum-ts models a
|
|
182
|
+
* tapback as its own message, and the only handle to retract one is the
|
|
183
|
+
* `Message` that `react()` returns — there is no by-target removal API.
|
|
184
|
+
*/
|
|
185
|
+
reactions = /* @__PURE__ */ new Map();
|
|
60
186
|
maxSpaces;
|
|
61
187
|
maxMessages;
|
|
62
188
|
constructor(maxSpaces = DEFAULT_MAX_SPACES, maxMessages = DEFAULT_MAX_MESSAGES) {
|
|
@@ -87,7 +213,24 @@ var InboundCache = class {
|
|
|
87
213
|
getMessage(id) {
|
|
88
214
|
return this.messages.get(id);
|
|
89
215
|
}
|
|
216
|
+
/** Record the reaction message returned by `react()` for later removal. */
|
|
217
|
+
rememberReaction(targetMessageId, glyph, reaction) {
|
|
218
|
+
this.reactions.set(reactionKey(targetMessageId, glyph), reaction);
|
|
219
|
+
evict(this.reactions, this.maxMessages);
|
|
220
|
+
}
|
|
221
|
+
/** Look up (and forget) a tracked reaction so it can be `unsend()`-ed once. */
|
|
222
|
+
takeReaction(targetMessageId, glyph) {
|
|
223
|
+
const key = reactionKey(targetMessageId, glyph);
|
|
224
|
+
const reaction = this.reactions.get(key);
|
|
225
|
+
if (reaction) {
|
|
226
|
+
this.reactions.delete(key);
|
|
227
|
+
}
|
|
228
|
+
return reaction;
|
|
229
|
+
}
|
|
90
230
|
};
|
|
231
|
+
function reactionKey(targetMessageId, glyph) {
|
|
232
|
+
return `${targetMessageId}::${glyph}`;
|
|
233
|
+
}
|
|
91
234
|
function evict(map, max) {
|
|
92
235
|
if (map.size <= max) {
|
|
93
236
|
return;
|
|
@@ -168,21 +311,21 @@ var MessagePump = class {
|
|
|
168
311
|
import { Message, parseMarkdown } from "chat";
|
|
169
312
|
|
|
170
313
|
// src/internal/thread.ts
|
|
171
|
-
import { ValidationError as
|
|
314
|
+
import { ValidationError as ValidationError4 } from "@chat-adapter/shared";
|
|
172
315
|
var THREAD_PREFIX = "imessage:";
|
|
173
316
|
function encodeThreadId(platformData) {
|
|
174
317
|
return `${THREAD_PREFIX}${platformData.chatGuid}`;
|
|
175
318
|
}
|
|
176
319
|
function decodeThreadId(threadId) {
|
|
177
320
|
if (!threadId.startsWith(THREAD_PREFIX)) {
|
|
178
|
-
throw new
|
|
321
|
+
throw new ValidationError4(
|
|
179
322
|
"imessage",
|
|
180
323
|
`Invalid iMessage thread ID: ${threadId}`
|
|
181
324
|
);
|
|
182
325
|
}
|
|
183
326
|
const chatGuid = threadId.slice(THREAD_PREFIX.length);
|
|
184
327
|
if (!chatGuid) {
|
|
185
|
-
throw new
|
|
328
|
+
throw new ValidationError4(
|
|
186
329
|
"imessage",
|
|
187
330
|
`Invalid iMessage thread ID: ${threadId} (empty chat GUID)`
|
|
188
331
|
);
|
|
@@ -317,8 +460,8 @@ function titleKey(chatGuid, title) {
|
|
|
317
460
|
}
|
|
318
461
|
|
|
319
462
|
// src/internal/outbound.ts
|
|
320
|
-
import { ValidationError as
|
|
321
|
-
import { lookup as
|
|
463
|
+
import { ValidationError as ValidationError5 } from "@chat-adapter/shared";
|
|
464
|
+
import { lookup as lookupMimeType2 } from "mime-types";
|
|
322
465
|
import { attachment } from "spectrum-ts";
|
|
323
466
|
var EMOJI_GLYPHS = {
|
|
324
467
|
heart: "\u2764\uFE0F",
|
|
@@ -336,7 +479,7 @@ function emojiToGlyph(emoji) {
|
|
|
336
479
|
const name = typeof emoji === "string" ? emoji : emoji.name;
|
|
337
480
|
const glyph = EMOJI_GLYPHS[name];
|
|
338
481
|
if (!glyph) {
|
|
339
|
-
throw new
|
|
482
|
+
throw new ValidationError5(
|
|
340
483
|
"imessage",
|
|
341
484
|
`Unsupported iMessage tapback: "${name}". Supported: heart, thumbs_up, thumbs_down, laugh, emphasize, question`
|
|
342
485
|
);
|
|
@@ -354,7 +497,7 @@ async function fileToAttachment(file) {
|
|
|
354
497
|
buffer = Buffer.from(data);
|
|
355
498
|
}
|
|
356
499
|
const name = file.filename || "attachment";
|
|
357
|
-
const mimeType = file.mimeType ||
|
|
500
|
+
const mimeType = file.mimeType || lookupMimeType2(name) || "application/octet-stream";
|
|
358
501
|
return attachment(buffer, { name, mimeType });
|
|
359
502
|
}
|
|
360
503
|
|
|
@@ -411,7 +554,8 @@ import {
|
|
|
411
554
|
isParagraphNode,
|
|
412
555
|
isStrongNode,
|
|
413
556
|
isTextNode,
|
|
414
|
-
parseMarkdown as parseMarkdown2
|
|
557
|
+
parseMarkdown as parseMarkdown2,
|
|
558
|
+
stringifyMarkdown
|
|
415
559
|
} from "chat";
|
|
416
560
|
var iMessageFormatConverter = class extends BaseFormatConverter {
|
|
417
561
|
/**
|
|
@@ -431,6 +575,30 @@ var iMessageFormatConverter = class extends BaseFormatConverter {
|
|
|
431
575
|
toAst(text) {
|
|
432
576
|
return parseMarkdown2(text);
|
|
433
577
|
}
|
|
578
|
+
/**
|
|
579
|
+
* Decide how a postable message should reach spectrum-ts.
|
|
580
|
+
*
|
|
581
|
+
* Markdown-typed inputs -- `{ markdown }` and `{ ast }` -- carry CommonMark
|
|
582
|
+
* the caller wants styled, so their source is preserved verbatim and flagged
|
|
583
|
+
* `markdown: true`; the adapter sends it via spectrum's `markdown()` builder,
|
|
584
|
+
* which renders bold/italic/links/lists as native iMessage styled text.
|
|
585
|
+
*
|
|
586
|
+
* Everything else is pass-through-as-is by contract -- a plain `string` or
|
|
587
|
+
* `{ raw }` must not have stray `*`/`_` reinterpreted as formatting, and
|
|
588
|
+
* cards fall back to plain text -- so those are rendered to plain text and
|
|
589
|
+
* flagged `markdown: false`.
|
|
590
|
+
*/
|
|
591
|
+
renderPostableContent(message) {
|
|
592
|
+
if (message && typeof message === "object") {
|
|
593
|
+
if ("markdown" in message && typeof message.markdown === "string") {
|
|
594
|
+
return { body: message.markdown, markdown: true };
|
|
595
|
+
}
|
|
596
|
+
if ("ast" in message && message.ast) {
|
|
597
|
+
return { body: stringifyMarkdown(message.ast), markdown: true };
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
return { body: this.renderPostable(message), markdown: false };
|
|
601
|
+
}
|
|
434
602
|
nodeToPlainText(node) {
|
|
435
603
|
if (isParagraphNode(node)) {
|
|
436
604
|
return getNodeChildren(node).map((child) => this.nodeToPlainText(child)).join("");
|
|
@@ -478,6 +646,169 @@ var iMessageFormatConverter = class extends BaseFormatConverter {
|
|
|
478
646
|
}
|
|
479
647
|
};
|
|
480
648
|
|
|
649
|
+
// src/miniapp.ts
|
|
650
|
+
import { ValidationError as ValidationError6 } from "@chat-adapter/shared";
|
|
651
|
+
async function toImageBytes(image) {
|
|
652
|
+
if (image instanceof Uint8Array) {
|
|
653
|
+
return new Uint8Array(image);
|
|
654
|
+
}
|
|
655
|
+
if (image instanceof ArrayBuffer) {
|
|
656
|
+
return new Uint8Array(image);
|
|
657
|
+
}
|
|
658
|
+
if (image instanceof Blob) {
|
|
659
|
+
return new Uint8Array(await image.arrayBuffer());
|
|
660
|
+
}
|
|
661
|
+
const data = image.data;
|
|
662
|
+
if (data === void 0) {
|
|
663
|
+
throw new ValidationError6(
|
|
664
|
+
"imessage",
|
|
665
|
+
"Mini-app card image must be a Uint8Array, ArrayBuffer, Blob, or FileUpload"
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
return toImageBytes(data);
|
|
669
|
+
}
|
|
670
|
+
function requireField(value, field) {
|
|
671
|
+
if (!value || value.trim().length === 0) {
|
|
672
|
+
throw new ValidationError6(
|
|
673
|
+
"imessage",
|
|
674
|
+
`Mini-app card requires a non-empty "${field}"`
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
return value;
|
|
678
|
+
}
|
|
679
|
+
async function resolveMiniApp(card) {
|
|
680
|
+
const url = typeof card.url === "string" ? card.url : card.url.toString();
|
|
681
|
+
try {
|
|
682
|
+
new URL(url);
|
|
683
|
+
} catch {
|
|
684
|
+
throw new ValidationError6(
|
|
685
|
+
"imessage",
|
|
686
|
+
`Mini-app card has an invalid url: "${url}"`
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
const layout = card.layout ?? {};
|
|
690
|
+
const image = layout.image ? await toImageBytes(layout.image) : void 0;
|
|
691
|
+
return {
|
|
692
|
+
appName: requireField(card.appName, "appName"),
|
|
693
|
+
teamId: requireField(card.teamId, "teamId"),
|
|
694
|
+
extensionBundleId: requireField(
|
|
695
|
+
card.extensionBundleId,
|
|
696
|
+
"extensionBundleId"
|
|
697
|
+
),
|
|
698
|
+
url,
|
|
699
|
+
...card.appStoreId === void 0 ? {} : { appStoreId: card.appStoreId },
|
|
700
|
+
layout: {
|
|
701
|
+
caption: layout.caption,
|
|
702
|
+
subcaption: layout.subcaption,
|
|
703
|
+
trailingCaption: layout.trailingCaption,
|
|
704
|
+
trailingSubcaption: layout.trailingSubcaption,
|
|
705
|
+
imageTitle: layout.imageTitle,
|
|
706
|
+
imageSubtitle: layout.imageSubtitle,
|
|
707
|
+
summary: layout.summary,
|
|
708
|
+
...image ? { image } : {}
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
function isAppUrl(input) {
|
|
713
|
+
return typeof input === "string" || typeof input === "function" || typeof input === "object" && input !== null && typeof input.then === "function";
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// src/voice.ts
|
|
717
|
+
import { ValidationError as ValidationError7 } from "@chat-adapter/shared";
|
|
718
|
+
import { lookup as lookupMimeType3 } from "mime-types";
|
|
719
|
+
import { voice as voiceContent } from "spectrum-ts";
|
|
720
|
+
var AUDIO_MIME_PATTERN = /^audio\//i;
|
|
721
|
+
async function toBytes2(input) {
|
|
722
|
+
if (input instanceof Uint8Array) {
|
|
723
|
+
return { bytes: Buffer.from(input) };
|
|
724
|
+
}
|
|
725
|
+
if (input instanceof ArrayBuffer) {
|
|
726
|
+
return { bytes: Buffer.from(input) };
|
|
727
|
+
}
|
|
728
|
+
if (input instanceof Blob) {
|
|
729
|
+
return {
|
|
730
|
+
bytes: Buffer.from(await input.arrayBuffer()),
|
|
731
|
+
mimeHint: input.type || void 0
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
const data = input.data;
|
|
735
|
+
if (data === void 0) {
|
|
736
|
+
throw new ValidationError7(
|
|
737
|
+
"imessage",
|
|
738
|
+
"Voice message must be a Uint8Array, ArrayBuffer, Blob, or FileUpload"
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
const nested = await toBytes2(data);
|
|
742
|
+
return {
|
|
743
|
+
bytes: nested.bytes,
|
|
744
|
+
mimeHint: input.mimeType || nested.mimeHint,
|
|
745
|
+
nameHint: input.filename || nested.nameHint
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
function toVoiceUrl(input) {
|
|
749
|
+
if (input instanceof URL) {
|
|
750
|
+
return input;
|
|
751
|
+
}
|
|
752
|
+
if (typeof input !== "string") {
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
let url;
|
|
756
|
+
try {
|
|
757
|
+
url = new URL(input);
|
|
758
|
+
} catch {
|
|
759
|
+
throw new ValidationError7(
|
|
760
|
+
"imessage",
|
|
761
|
+
`Voice message string input must be an http(s) URL, got "${input}". Pass audio bytes for local files.`
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
765
|
+
throw new ValidationError7(
|
|
766
|
+
"imessage",
|
|
767
|
+
`Voice message URL must be http(s), got "${url.protocol}"`
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
return url;
|
|
771
|
+
}
|
|
772
|
+
function resolveBytesMime2(hint, name) {
|
|
773
|
+
if (hint && AUDIO_MIME_PATTERN.test(hint)) {
|
|
774
|
+
return hint;
|
|
775
|
+
}
|
|
776
|
+
if (name) {
|
|
777
|
+
const inferred = lookupMimeType3(name);
|
|
778
|
+
if (inferred && AUDIO_MIME_PATTERN.test(inferred)) {
|
|
779
|
+
return inferred;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
throw new ValidationError7(
|
|
783
|
+
"imessage",
|
|
784
|
+
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'
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
async function resolveVoice(input, options = {}) {
|
|
788
|
+
if (options.mimeType && !AUDIO_MIME_PATTERN.test(options.mimeType)) {
|
|
789
|
+
throw new ValidationError7(
|
|
790
|
+
"imessage",
|
|
791
|
+
`Voice message requires an audio/* MIME type, got "${options.mimeType}"`
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
const url = toVoiceUrl(input);
|
|
795
|
+
if (url) {
|
|
796
|
+
return voiceContent(url, {
|
|
797
|
+
...options.mimeType ? { mimeType: options.mimeType } : {},
|
|
798
|
+
...options.name ? { name: options.name } : {},
|
|
799
|
+
...options.duration === void 0 ? {} : { duration: options.duration }
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
const { bytes, mimeHint, nameHint } = await toBytes2(input);
|
|
803
|
+
const name = options.name ?? nameHint;
|
|
804
|
+
const mimeType = resolveBytesMime2(options.mimeType ?? mimeHint, name);
|
|
805
|
+
return voiceContent(bytes, {
|
|
806
|
+
mimeType,
|
|
807
|
+
...name ? { name } : {},
|
|
808
|
+
...options.duration === void 0 ? {} : { duration: options.duration }
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
|
|
481
812
|
// src/adapter.ts
|
|
482
813
|
var TYPING_DURATION_MS = 3e3;
|
|
483
814
|
var iMessageAdapter = class {
|
|
@@ -502,7 +833,7 @@ var iMessageAdapter = class {
|
|
|
502
833
|
pump = null;
|
|
503
834
|
constructor(config) {
|
|
504
835
|
if (config.local && process.platform !== "darwin") {
|
|
505
|
-
throw new
|
|
836
|
+
throw new ValidationError8(
|
|
506
837
|
"imessage",
|
|
507
838
|
"iMessage adapter local mode requires macOS. Current platform: " + process.platform
|
|
508
839
|
);
|
|
@@ -532,7 +863,7 @@ var iMessageAdapter = class {
|
|
|
532
863
|
serverUrl: this.serverUrl
|
|
533
864
|
}
|
|
534
865
|
);
|
|
535
|
-
const providers = [
|
|
866
|
+
const providers = [imessage2.config(providerConfig)];
|
|
536
867
|
this.app = projectId && projectSecret ? await Spectrum({ providers, projectId, projectSecret }) : await Spectrum({ providers });
|
|
537
868
|
this.pump = new MessagePump(
|
|
538
869
|
() => {
|
|
@@ -612,26 +943,152 @@ var iMessageAdapter = class {
|
|
|
612
943
|
this.routeWebhookMessage(payload, options);
|
|
613
944
|
return new Response(null, { status: 200 });
|
|
614
945
|
}
|
|
946
|
+
/**
|
|
947
|
+
* Build the spectrum content for an outbound message. Markdown-typed inputs
|
|
948
|
+
* are sent via `markdown()` so remote iMessage renders them as native styled
|
|
949
|
+
* text; raw/string/card inputs stay plain `text()`. Returns the rendered
|
|
950
|
+
* `body` too so callers can skip an empty send.
|
|
951
|
+
*/
|
|
952
|
+
toSpectrumContent(message) {
|
|
953
|
+
const { body, markdown } = this.formatConverter.renderPostableContent(message);
|
|
954
|
+
return {
|
|
955
|
+
body,
|
|
956
|
+
content: markdown ? markdownContent(body) : textContent(body)
|
|
957
|
+
};
|
|
958
|
+
}
|
|
615
959
|
async postMessage(threadId, message) {
|
|
616
960
|
const space = await this.requireSpace(threadId, "postMessage");
|
|
617
|
-
const body = this.
|
|
961
|
+
const { body, content } = this.toSpectrumContent(message);
|
|
618
962
|
const files = extractFiles(message);
|
|
619
963
|
let first;
|
|
620
964
|
if (body && body.trim().length > 0) {
|
|
621
|
-
first = await space.send(
|
|
965
|
+
first = await space.send(content) ?? first;
|
|
622
966
|
}
|
|
623
967
|
for (const file of files) {
|
|
624
968
|
const sent = await space.send(await fileToAttachment(file)) ?? void 0;
|
|
625
969
|
first ??= sent;
|
|
626
970
|
}
|
|
627
971
|
if (!first) {
|
|
628
|
-
throw new
|
|
972
|
+
throw new ValidationError8(
|
|
629
973
|
"imessage",
|
|
630
974
|
"postMessage requires non-empty text or at least one attachment"
|
|
631
975
|
);
|
|
632
976
|
}
|
|
633
977
|
return { id: first.id, threadId, raw: first };
|
|
634
978
|
}
|
|
979
|
+
/**
|
|
980
|
+
* Send a message with an iMessage expressive-send effect — a bubble effect
|
|
981
|
+
* (`slam`, `loud`, `gentle`, `invisible`) or a full-screen effect
|
|
982
|
+
* (`confetti`, `fireworks`, `balloons`, `heart`, `lasers`, `celebration`,
|
|
983
|
+
* `sparkles`, `spotlight`, `echo`). Not part of the Chat SDK `Adapter`
|
|
984
|
+
* interface — exposed as an adapter-specific extra (e.g. celebratory confetti
|
|
985
|
+
* on task completion). Remote only.
|
|
986
|
+
*
|
|
987
|
+
* The `effect` argument accepts a friendly name (`"confetti"`) or a value from
|
|
988
|
+
* the re-exported `iMessageEffect` map. Effects attach to text content only,
|
|
989
|
+
* so this requires non-empty text.
|
|
990
|
+
*/
|
|
991
|
+
async sendEffect(threadId, message, effect) {
|
|
992
|
+
if (this.local) {
|
|
993
|
+
throw new NotImplementedError(
|
|
994
|
+
"sendEffect is not supported in local mode",
|
|
995
|
+
"sendEffect"
|
|
996
|
+
);
|
|
997
|
+
}
|
|
998
|
+
const space = await this.requireSpace(threadId, "sendEffect");
|
|
999
|
+
const { body, content } = this.toSpectrumContent(message);
|
|
1000
|
+
if (!body || body.trim().length === 0) {
|
|
1001
|
+
throw new ValidationError8(
|
|
1002
|
+
"imessage",
|
|
1003
|
+
"sendEffect requires non-empty text content"
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
const effectId = resolveEffect(effect);
|
|
1007
|
+
const sent = await space.send(effectContent(content, effectId));
|
|
1008
|
+
if (!sent) {
|
|
1009
|
+
throw new ValidationError8(
|
|
1010
|
+
"imessage",
|
|
1011
|
+
"sendEffect could not send the message"
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
return { id: sent.id, threadId, raw: sent };
|
|
1015
|
+
}
|
|
1016
|
+
async sendMiniApp(threadId, input) {
|
|
1017
|
+
if (this.local) {
|
|
1018
|
+
throw new NotImplementedError(
|
|
1019
|
+
"sendMiniApp is not supported in local mode",
|
|
1020
|
+
"sendMiniApp"
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
const space = await this.requireSpace(threadId, "sendMiniApp");
|
|
1024
|
+
const content = isAppUrl(input) ? appContent(input) : customizedMiniApp(await resolveMiniApp(input));
|
|
1025
|
+
const sent = await space.send(content);
|
|
1026
|
+
if (!sent) {
|
|
1027
|
+
throw new ValidationError8(
|
|
1028
|
+
"imessage",
|
|
1029
|
+
"sendMiniApp could not send the card"
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
return { id: sent.id, threadId, raw: sent };
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Send a native iMessage voice note — a real, playable waveform bubble (the
|
|
1036
|
+
* message renders with `isAudioMessage`), not an audio file dropped in as an
|
|
1037
|
+
* attachment. A natural fit for TTS-capable bots that reply with speech. Not
|
|
1038
|
+
* part of the Chat SDK `Adapter` interface — exposed as an adapter-specific
|
|
1039
|
+
* extra. Remote only.
|
|
1040
|
+
*
|
|
1041
|
+
* The `input` is either in-memory audio bytes (`Uint8Array` / `Buffer` /
|
|
1042
|
+
* `ArrayBuffer`, a `Blob`, or a Chat SDK `FileUpload`) or an `http(s)` URL (a
|
|
1043
|
+
* `URL` or a string) that is fetched at send time. Audio bytes need an
|
|
1044
|
+
* `audio/*` MIME type — supply `options.mimeType` (e.g. `"audio/mp4"`) or an
|
|
1045
|
+
* `options.name` with an audio extension when it can't be inferred.
|
|
1046
|
+
*/
|
|
1047
|
+
async sendVoice(threadId, input, options) {
|
|
1048
|
+
if (this.local) {
|
|
1049
|
+
throw new NotImplementedError(
|
|
1050
|
+
"sendVoice is not supported in local mode",
|
|
1051
|
+
"sendVoice"
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
const space = await this.requireSpace(threadId, "sendVoice");
|
|
1055
|
+
const content = await resolveVoice(input, options);
|
|
1056
|
+
const sent = await space.send(content);
|
|
1057
|
+
if (!sent) {
|
|
1058
|
+
throw new ValidationError8(
|
|
1059
|
+
"imessage",
|
|
1060
|
+
"sendVoice could not send the voice message"
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
return { id: sent.id, threadId, raw: sent };
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Set or clear the chat background — the wallpaper behind a conversation, an
|
|
1067
|
+
* iMessage-only touch with no analog on the plain-text competitors. Not part
|
|
1068
|
+
* of the Chat SDK `Adapter` interface — exposed as an adapter-specific extra.
|
|
1069
|
+
* Remote only.
|
|
1070
|
+
*
|
|
1071
|
+
* The `input` is either the literal `"clear"` (to remove the current
|
|
1072
|
+
* background), in-memory image bytes (`Uint8Array` / `Buffer` / `ArrayBuffer`,
|
|
1073
|
+
* a `Blob`, or a Chat SDK `FileUpload`), or an `http(s)` URL (a `URL` or a
|
|
1074
|
+
* string) that is fetched at send time. Image bytes need an `image/*` MIME
|
|
1075
|
+
* type — supply `options.mimeType` (e.g. `"image/jpeg"`) or an `options.name`
|
|
1076
|
+
* with an image extension when it can't be inferred.
|
|
1077
|
+
*
|
|
1078
|
+
* Fire-and-forget: iMessage acknowledges the control signal without returning
|
|
1079
|
+
* a message, so this resolves to `void` rather than a {@link RawMessage}.
|
|
1080
|
+
*/
|
|
1081
|
+
async setBackground(threadId, input, options) {
|
|
1082
|
+
if (this.local) {
|
|
1083
|
+
throw new NotImplementedError(
|
|
1084
|
+
"setBackground is not supported in local mode",
|
|
1085
|
+
"setBackground"
|
|
1086
|
+
);
|
|
1087
|
+
}
|
|
1088
|
+
const space = await this.requireSpace(threadId, "setBackground");
|
|
1089
|
+
const content = await resolveBackground(input, options);
|
|
1090
|
+
await space.send(content);
|
|
1091
|
+
}
|
|
635
1092
|
async editMessage(threadId, messageId, message) {
|
|
636
1093
|
if (this.local) {
|
|
637
1094
|
throw new NotImplementedError(
|
|
@@ -646,21 +1103,42 @@ var iMessageAdapter = class {
|
|
|
646
1103
|
"editMessage"
|
|
647
1104
|
);
|
|
648
1105
|
}
|
|
649
|
-
await target.edit(
|
|
650
|
-
textContent(this.formatConverter.renderPostable(message))
|
|
651
|
-
);
|
|
1106
|
+
await target.edit(this.toSpectrumContent(message).content);
|
|
652
1107
|
return { id: messageId, threadId, raw: target };
|
|
653
1108
|
}
|
|
654
|
-
async deleteMessage(
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
1109
|
+
async deleteMessage(threadId, messageId) {
|
|
1110
|
+
if (this.local) {
|
|
1111
|
+
throw new NotImplementedError(
|
|
1112
|
+
"deleteMessage is not supported in local mode",
|
|
1113
|
+
"deleteMessage"
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1116
|
+
const target = await this.resolveMessage(threadId, messageId);
|
|
1117
|
+
if (!target) {
|
|
1118
|
+
throw new NotImplementedError(
|
|
1119
|
+
"deleteMessage requires the target message to have been sent or received in this session",
|
|
1120
|
+
"deleteMessage"
|
|
1121
|
+
);
|
|
1122
|
+
}
|
|
1123
|
+
await target.unsend();
|
|
659
1124
|
}
|
|
660
1125
|
parseMessage(raw) {
|
|
661
1126
|
const message = raw;
|
|
662
1127
|
return buildChatMessage(message, message.space);
|
|
663
1128
|
}
|
|
1129
|
+
/**
|
|
1130
|
+
* Fetch a single message by id. spectrum-ts can resolve a message by id
|
|
1131
|
+
* (from the inbound cache or the provider's by-id lookup) even though it has
|
|
1132
|
+
* no paginated history API, so single-message reads work where
|
|
1133
|
+
* `fetchMessages` cannot. Returns `null` when the message can't be resolved.
|
|
1134
|
+
*/
|
|
1135
|
+
async fetchMessage(threadId, messageId) {
|
|
1136
|
+
const target = await this.resolveMessage(threadId, messageId);
|
|
1137
|
+
if (!target) {
|
|
1138
|
+
return null;
|
|
1139
|
+
}
|
|
1140
|
+
return this.parseMessage(target);
|
|
1141
|
+
}
|
|
664
1142
|
async fetchMessages(_threadId, _options) {
|
|
665
1143
|
throw new NotImplementedError(
|
|
666
1144
|
"fetchMessages (message history) is not supported by spectrum-ts",
|
|
@@ -691,13 +1169,27 @@ var iMessageAdapter = class {
|
|
|
691
1169
|
"addReaction"
|
|
692
1170
|
);
|
|
693
1171
|
}
|
|
694
|
-
await target.react(glyph);
|
|
1172
|
+
const reaction = await target.react(glyph);
|
|
1173
|
+
if (reaction) {
|
|
1174
|
+
this.cache.rememberReaction(messageId, glyph, reaction);
|
|
1175
|
+
}
|
|
695
1176
|
}
|
|
696
|
-
async removeReaction(_threadId,
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
1177
|
+
async removeReaction(_threadId, messageId, emoji) {
|
|
1178
|
+
if (this.local) {
|
|
1179
|
+
throw new NotImplementedError(
|
|
1180
|
+
"removeReaction is not supported in local mode",
|
|
1181
|
+
"removeReaction"
|
|
1182
|
+
);
|
|
1183
|
+
}
|
|
1184
|
+
const glyph = emojiToGlyph(emoji);
|
|
1185
|
+
const reaction = this.cache.takeReaction(messageId, glyph);
|
|
1186
|
+
if (!reaction) {
|
|
1187
|
+
throw new NotImplementedError(
|
|
1188
|
+
"removeReaction requires the reaction to have been added via addReaction in this session",
|
|
1189
|
+
"removeReaction"
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
await reaction.unsend();
|
|
701
1193
|
}
|
|
702
1194
|
async startTyping(threadId, _status) {
|
|
703
1195
|
if (this.local) {
|
|
@@ -713,6 +1205,44 @@ var iMessageAdapter = class {
|
|
|
713
1205
|
});
|
|
714
1206
|
}, TYPING_DURATION_MS);
|
|
715
1207
|
}
|
|
1208
|
+
/**
|
|
1209
|
+
* Cold-start a DM with a phone number / handle. spectrum-ts resolves (or
|
|
1210
|
+
* creates) the 1:1 conversation from the participant via `space.create`, so
|
|
1211
|
+
* the bot can message a user it has never received from. Returns the encoded
|
|
1212
|
+
* thread id, ready to pass to `postMessage`.
|
|
1213
|
+
*/
|
|
1214
|
+
async openDM(userId) {
|
|
1215
|
+
if (!this.app) {
|
|
1216
|
+
throw new NotImplementedError(
|
|
1217
|
+
"openDM requires the adapter to be initialized",
|
|
1218
|
+
"openDM"
|
|
1219
|
+
);
|
|
1220
|
+
}
|
|
1221
|
+
const space = await this.platformSpaces().create(userId);
|
|
1222
|
+
this.cache.rememberSpace(space);
|
|
1223
|
+
return encodeThreadId({ chatGuid: space.id });
|
|
1224
|
+
}
|
|
1225
|
+
/**
|
|
1226
|
+
* Mark a received message (and the conversation up to it) as read, surfacing
|
|
1227
|
+
* a read receipt where iMessage supports one. Remote only; not part of the
|
|
1228
|
+
* Chat SDK `Adapter` interface — exposed as an adapter-specific extra.
|
|
1229
|
+
*/
|
|
1230
|
+
async markRead(threadId, messageId) {
|
|
1231
|
+
if (this.local) {
|
|
1232
|
+
throw new NotImplementedError(
|
|
1233
|
+
"markRead is not supported in local mode",
|
|
1234
|
+
"markRead"
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
const target = await this.resolveMessage(threadId, messageId);
|
|
1238
|
+
if (!target) {
|
|
1239
|
+
throw new NotImplementedError(
|
|
1240
|
+
"markRead requires the target message to have been received in this session",
|
|
1241
|
+
"markRead"
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1244
|
+
await target.read();
|
|
1245
|
+
}
|
|
716
1246
|
async openModal(triggerId, modal, contextId) {
|
|
717
1247
|
if (this.local) {
|
|
718
1248
|
throw new NotImplementedError(
|
|
@@ -724,14 +1254,14 @@ var iMessageAdapter = class {
|
|
|
724
1254
|
(c) => c.type === "select"
|
|
725
1255
|
);
|
|
726
1256
|
if (!select) {
|
|
727
|
-
throw new
|
|
1257
|
+
throw new ValidationError8(
|
|
728
1258
|
"imessage",
|
|
729
1259
|
"openModal requires at least one Select child \u2014 iMessage modals map to native polls"
|
|
730
1260
|
);
|
|
731
1261
|
}
|
|
732
1262
|
const labels = select.options.map((o) => o.label);
|
|
733
1263
|
if (labels.length < 2 || labels.length > 10) {
|
|
734
|
-
throw new
|
|
1264
|
+
throw new ValidationError8(
|
|
735
1265
|
"imessage",
|
|
736
1266
|
`iMessage polls require between 2 and 10 options, received ${labels.length}`
|
|
737
1267
|
);
|
|
@@ -907,9 +1437,8 @@ var iMessageAdapter = class {
|
|
|
907
1437
|
if (!this.app) {
|
|
908
1438
|
return;
|
|
909
1439
|
}
|
|
910
|
-
const platform = imessage(this.app);
|
|
911
1440
|
try {
|
|
912
|
-
const space = await
|
|
1441
|
+
const space = await this.platformSpaces().get(chatGuid);
|
|
913
1442
|
this.cache.rememberSpace(space);
|
|
914
1443
|
return space;
|
|
915
1444
|
} catch (error) {
|
|
@@ -920,6 +1449,17 @@ var iMessageAdapter = class {
|
|
|
920
1449
|
return;
|
|
921
1450
|
}
|
|
922
1451
|
}
|
|
1452
|
+
/**
|
|
1453
|
+
* The iMessage provider's Space namespace (`get` / `create`). `HasProvider`
|
|
1454
|
+
* over the default provider tuple won't narrow to `true`, so `imessage(app)`
|
|
1455
|
+
* types as `never` — cast to the slice of the instance we use.
|
|
1456
|
+
*/
|
|
1457
|
+
platformSpaces() {
|
|
1458
|
+
if (!this.app) {
|
|
1459
|
+
throw new Error("Adapter not initialized");
|
|
1460
|
+
}
|
|
1461
|
+
return imessage2(this.app).space;
|
|
1462
|
+
}
|
|
923
1463
|
async requireSpace(threadId, action) {
|
|
924
1464
|
const space = await this.resolveSpace(threadId);
|
|
925
1465
|
if (!space) {
|
|
@@ -950,7 +1490,7 @@ function toClientArray(clients) {
|
|
|
950
1490
|
}
|
|
951
1491
|
|
|
952
1492
|
// src/factory.ts
|
|
953
|
-
import { ValidationError as
|
|
1493
|
+
import { ValidationError as ValidationError9 } from "@chat-adapter/shared";
|
|
954
1494
|
import { ConsoleLogger } from "chat";
|
|
955
1495
|
function resolveLocalMode(explicit, hasRemoteCreds) {
|
|
956
1496
|
if (explicit !== void 0) {
|
|
@@ -970,13 +1510,13 @@ function assertRemoteCredentials(creds) {
|
|
|
970
1510
|
return;
|
|
971
1511
|
}
|
|
972
1512
|
if (!creds.hasServerUrl) {
|
|
973
|
-
throw new
|
|
1513
|
+
throw new ValidationError9(
|
|
974
1514
|
"imessage",
|
|
975
1515
|
"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
1516
|
);
|
|
977
1517
|
}
|
|
978
1518
|
if (!creds.hasApiKey) {
|
|
979
|
-
throw new
|
|
1519
|
+
throw new ValidationError9(
|
|
980
1520
|
"imessage",
|
|
981
1521
|
"apiKey is required when local is false. Set IMESSAGE_API_KEY or provide it in config."
|
|
982
1522
|
);
|
|
@@ -1016,6 +1556,12 @@ export {
|
|
|
1016
1556
|
createiMessageAdapter,
|
|
1017
1557
|
deriveAddress,
|
|
1018
1558
|
iMessageAdapter,
|
|
1019
|
-
|
|
1559
|
+
iMessageEffect,
|
|
1560
|
+
iMessageFormatConverter,
|
|
1561
|
+
isAppUrl,
|
|
1562
|
+
resolveBackground,
|
|
1563
|
+
resolveEffect,
|
|
1564
|
+
resolveMiniApp,
|
|
1565
|
+
resolveVoice
|
|
1020
1566
|
};
|
|
1021
1567
|
//# sourceMappingURL=index.js.map
|