@photon-ai/chat-adapter-imessage 1.1.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/LICENSE +21 -0
- package/README.md +358 -0
- package/dist/index.d.ts +175 -0
- package/dist/index.js +1021 -0
- package/dist/index.js.map +1 -0
- package/package.json +70 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1021 @@
|
|
|
1
|
+
// src/adapter.ts
|
|
2
|
+
import { extractFiles, ValidationError as ValidationError4 } from "@chat-adapter/shared";
|
|
3
|
+
import { NotImplementedError } from "chat";
|
|
4
|
+
import {
|
|
5
|
+
poll as pollContent,
|
|
6
|
+
Spectrum,
|
|
7
|
+
text as textContent
|
|
8
|
+
} from "spectrum-ts";
|
|
9
|
+
import { imessage } from "spectrum-ts/providers/imessage";
|
|
10
|
+
|
|
11
|
+
// src/config.ts
|
|
12
|
+
import { ValidationError } from "@chat-adapter/shared";
|
|
13
|
+
var SHARED_PHONE = "shared";
|
|
14
|
+
var URL_SCHEME_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
15
|
+
function deriveAddress(serverUrl) {
|
|
16
|
+
const trimmed = serverUrl.trim();
|
|
17
|
+
const hasScheme = URL_SCHEME_RE.test(trimmed);
|
|
18
|
+
const url = new URL(hasScheme ? trimmed : `https://${trimmed}`);
|
|
19
|
+
return `${url.hostname}:${url.port || "443"}`;
|
|
20
|
+
}
|
|
21
|
+
function resolveSpectrumConfig(local, auth) {
|
|
22
|
+
if (local) {
|
|
23
|
+
return { providerConfig: { local: true } };
|
|
24
|
+
}
|
|
25
|
+
if (auth.projectId && auth.projectSecret) {
|
|
26
|
+
return {
|
|
27
|
+
providerConfig: {},
|
|
28
|
+
projectId: auth.projectId,
|
|
29
|
+
projectSecret: auth.projectSecret
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (auth.clients?.length) {
|
|
33
|
+
return { providerConfig: { clients: auth.clients } };
|
|
34
|
+
}
|
|
35
|
+
if (auth.serverUrl && auth.apiKey) {
|
|
36
|
+
return {
|
|
37
|
+
providerConfig: {
|
|
38
|
+
clients: [
|
|
39
|
+
{
|
|
40
|
+
address: deriveAddress(auth.serverUrl),
|
|
41
|
+
token: auth.apiKey,
|
|
42
|
+
phone: auth.phone ?? SHARED_PHONE
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
throw new ValidationError(
|
|
49
|
+
"imessage",
|
|
50
|
+
"Remote mode requires Spectrum Cloud credentials (projectId + projectSecret), explicit clients, or serverUrl + apiKey."
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/internal/cache.ts
|
|
55
|
+
var DEFAULT_MAX_SPACES = 256;
|
|
56
|
+
var DEFAULT_MAX_MESSAGES = 1024;
|
|
57
|
+
var InboundCache = class {
|
|
58
|
+
spaces = /* @__PURE__ */ new Map();
|
|
59
|
+
messages = /* @__PURE__ */ new Map();
|
|
60
|
+
maxSpaces;
|
|
61
|
+
maxMessages;
|
|
62
|
+
constructor(maxSpaces = DEFAULT_MAX_SPACES, maxMessages = DEFAULT_MAX_MESSAGES) {
|
|
63
|
+
this.maxSpaces = maxSpaces;
|
|
64
|
+
this.maxMessages = maxMessages;
|
|
65
|
+
}
|
|
66
|
+
/** Cache the Space + Message (and any group sub-items) from one inbound event. */
|
|
67
|
+
remember(space, message) {
|
|
68
|
+
this.rememberSpace(space);
|
|
69
|
+
this.rememberMessage(message);
|
|
70
|
+
if (message.content.type === "group") {
|
|
71
|
+
for (const item of message.content.items) {
|
|
72
|
+
this.rememberMessage(item);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
rememberSpace(space) {
|
|
77
|
+
this.spaces.set(space.id, space);
|
|
78
|
+
evict(this.spaces, this.maxSpaces);
|
|
79
|
+
}
|
|
80
|
+
rememberMessage(message) {
|
|
81
|
+
this.messages.set(message.id, message);
|
|
82
|
+
evict(this.messages, this.maxMessages);
|
|
83
|
+
}
|
|
84
|
+
getSpace(chatGuid) {
|
|
85
|
+
return this.spaces.get(chatGuid);
|
|
86
|
+
}
|
|
87
|
+
getMessage(id) {
|
|
88
|
+
return this.messages.get(id);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
function evict(map, max) {
|
|
92
|
+
if (map.size <= max) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const overflow = map.size - max;
|
|
96
|
+
let removed = 0;
|
|
97
|
+
for (const key of map.keys()) {
|
|
98
|
+
if (removed >= overflow) {
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
map.delete(key);
|
|
102
|
+
removed += 1;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/internal/gateway.ts
|
|
107
|
+
var MessagePump = class {
|
|
108
|
+
started = false;
|
|
109
|
+
iterator = null;
|
|
110
|
+
source;
|
|
111
|
+
onMessage;
|
|
112
|
+
logger;
|
|
113
|
+
constructor(source, onMessage, logger) {
|
|
114
|
+
this.source = source;
|
|
115
|
+
this.onMessage = onMessage;
|
|
116
|
+
this.logger = logger;
|
|
117
|
+
}
|
|
118
|
+
/** Start consuming if not already running. Idempotent. */
|
|
119
|
+
ensureRunning() {
|
|
120
|
+
if (this.started) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
this.started = true;
|
|
124
|
+
const iterator = this.source()[Symbol.asyncIterator]();
|
|
125
|
+
this.iterator = iterator;
|
|
126
|
+
this.consume(iterator).catch(() => {
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/** Close the underlying stream and stop consuming. */
|
|
130
|
+
stop() {
|
|
131
|
+
const iterator = this.iterator;
|
|
132
|
+
this.iterator = null;
|
|
133
|
+
this.started = false;
|
|
134
|
+
iterator?.return?.().catch(() => {
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
async consume(iterator) {
|
|
138
|
+
try {
|
|
139
|
+
while (true) {
|
|
140
|
+
const next = await iterator.next();
|
|
141
|
+
if (next.done) {
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
const [space, message] = next.value;
|
|
145
|
+
try {
|
|
146
|
+
await this.onMessage(space, message);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
this.logger.error("iMessage inbound handler error", {
|
|
149
|
+
error: String(error)
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
} catch (error) {
|
|
154
|
+
this.logger.error("iMessage message stream error", {
|
|
155
|
+
error: String(error)
|
|
156
|
+
});
|
|
157
|
+
} finally {
|
|
158
|
+
if (this.iterator === iterator) {
|
|
159
|
+
this.iterator = null;
|
|
160
|
+
this.started = false;
|
|
161
|
+
}
|
|
162
|
+
this.logger.info("iMessage Gateway listener stopped");
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// src/internal/inbound.ts
|
|
168
|
+
import { Message, parseMarkdown } from "chat";
|
|
169
|
+
|
|
170
|
+
// src/internal/thread.ts
|
|
171
|
+
import { ValidationError as ValidationError2 } from "@chat-adapter/shared";
|
|
172
|
+
var THREAD_PREFIX = "imessage:";
|
|
173
|
+
function encodeThreadId(platformData) {
|
|
174
|
+
return `${THREAD_PREFIX}${platformData.chatGuid}`;
|
|
175
|
+
}
|
|
176
|
+
function decodeThreadId(threadId) {
|
|
177
|
+
if (!threadId.startsWith(THREAD_PREFIX)) {
|
|
178
|
+
throw new ValidationError2(
|
|
179
|
+
"imessage",
|
|
180
|
+
`Invalid iMessage thread ID: ${threadId}`
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
const chatGuid = threadId.slice(THREAD_PREFIX.length);
|
|
184
|
+
if (!chatGuid) {
|
|
185
|
+
throw new ValidationError2(
|
|
186
|
+
"imessage",
|
|
187
|
+
`Invalid iMessage thread ID: ${threadId} (empty chat GUID)`
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
return { chatGuid };
|
|
191
|
+
}
|
|
192
|
+
function isDMChatGuid(chatGuid) {
|
|
193
|
+
return chatGuid.includes(";-;");
|
|
194
|
+
}
|
|
195
|
+
var DM_SEPARATOR = ";-;";
|
|
196
|
+
function dmAddressFromChatGuid(chatGuid) {
|
|
197
|
+
const idx = chatGuid.indexOf(DM_SEPARATOR);
|
|
198
|
+
return idx === -1 ? "" : chatGuid.slice(idx + DM_SEPARATOR.length);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// src/internal/inbound.ts
|
|
202
|
+
function buildChatMessageFromFields(fields) {
|
|
203
|
+
const text = extractText(fields.content);
|
|
204
|
+
return new Message({
|
|
205
|
+
id: fields.id,
|
|
206
|
+
threadId: encodeThreadId({ chatGuid: fields.chatGuid }),
|
|
207
|
+
text,
|
|
208
|
+
formatted: parseMarkdown(text),
|
|
209
|
+
author: {
|
|
210
|
+
userId: fields.senderId,
|
|
211
|
+
userName: fields.senderId,
|
|
212
|
+
fullName: fields.senderId,
|
|
213
|
+
isBot: false,
|
|
214
|
+
isMe: fields.isOutbound
|
|
215
|
+
},
|
|
216
|
+
metadata: { dateSent: fields.timestamp, edited: false },
|
|
217
|
+
attachments: extractAttachments(fields.content).map((a) => ({
|
|
218
|
+
type: getAttachmentType(a.mimeType),
|
|
219
|
+
name: a.name,
|
|
220
|
+
mimeType: a.mimeType,
|
|
221
|
+
size: a.size ?? 0
|
|
222
|
+
})),
|
|
223
|
+
raw: fields.raw,
|
|
224
|
+
isMention: isDMChatGuid(fields.chatGuid)
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
function buildChatMessage(message, space) {
|
|
228
|
+
return buildChatMessageFromFields({
|
|
229
|
+
id: message.id,
|
|
230
|
+
chatGuid: space.id,
|
|
231
|
+
content: message.content,
|
|
232
|
+
senderId: message.sender?.id ?? "",
|
|
233
|
+
isOutbound: message.direction === "outbound",
|
|
234
|
+
timestamp: message.timestamp,
|
|
235
|
+
raw: message
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
function extractText(content) {
|
|
239
|
+
switch (content.type) {
|
|
240
|
+
case "text":
|
|
241
|
+
return content.text;
|
|
242
|
+
case "richlink":
|
|
243
|
+
return String(content.url);
|
|
244
|
+
case "poll":
|
|
245
|
+
return content.title;
|
|
246
|
+
case "group":
|
|
247
|
+
return content.items.map((item) => extractText(item.content)).filter((t) => t.length > 0).join("\n");
|
|
248
|
+
default:
|
|
249
|
+
return "";
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function extractAttachments(content) {
|
|
253
|
+
const out = [];
|
|
254
|
+
const visit = (c) => {
|
|
255
|
+
if (c.type === "attachment") {
|
|
256
|
+
out.push({ name: c.name, mimeType: c.mimeType, size: c.size });
|
|
257
|
+
} else if (c.type === "voice") {
|
|
258
|
+
const voice = c;
|
|
259
|
+
out.push({
|
|
260
|
+
name: voice.name ?? "voice",
|
|
261
|
+
mimeType: voice.mimeType,
|
|
262
|
+
size: voice.size
|
|
263
|
+
});
|
|
264
|
+
} else if (c.type === "group") {
|
|
265
|
+
for (const item of c.items) {
|
|
266
|
+
visit(item.content);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
visit(content);
|
|
271
|
+
return out;
|
|
272
|
+
}
|
|
273
|
+
function getAttachmentType(mimeType) {
|
|
274
|
+
if (!mimeType) {
|
|
275
|
+
return "file";
|
|
276
|
+
}
|
|
277
|
+
if (mimeType.startsWith("image/")) {
|
|
278
|
+
return "image";
|
|
279
|
+
}
|
|
280
|
+
if (mimeType.startsWith("video/")) {
|
|
281
|
+
return "video";
|
|
282
|
+
}
|
|
283
|
+
if (mimeType.startsWith("audio/")) {
|
|
284
|
+
return "audio";
|
|
285
|
+
}
|
|
286
|
+
return "file";
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// src/internal/modals.ts
|
|
290
|
+
var DEFAULT_MAX_MODALS = 512;
|
|
291
|
+
var ModalPollRegistry = class {
|
|
292
|
+
byTitle = /* @__PURE__ */ new Map();
|
|
293
|
+
maxEntries;
|
|
294
|
+
constructor(maxEntries = DEFAULT_MAX_MODALS) {
|
|
295
|
+
this.maxEntries = maxEntries;
|
|
296
|
+
}
|
|
297
|
+
register(chatGuid, title, meta) {
|
|
298
|
+
const key = titleKey(chatGuid, title);
|
|
299
|
+
this.byTitle.delete(key);
|
|
300
|
+
this.byTitle.set(key, meta);
|
|
301
|
+
if (this.byTitle.size > this.maxEntries) {
|
|
302
|
+
const oldest = this.byTitle.keys().next().value;
|
|
303
|
+
if (oldest !== void 0) {
|
|
304
|
+
this.byTitle.delete(oldest);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
resolveVote(chatGuid, pollTitle, optionTitle) {
|
|
309
|
+
const meta = this.byTitle.get(titleKey(chatGuid, pollTitle));
|
|
310
|
+
if (!meta) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const option = meta.options.find((o) => o.label === optionTitle);
|
|
314
|
+
if (!option) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
return { meta, value: option.value };
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
function titleKey(chatGuid, title) {
|
|
321
|
+
return `${chatGuid}::${title}`;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// src/internal/outbound.ts
|
|
325
|
+
import { ValidationError as ValidationError3 } from "@chat-adapter/shared";
|
|
326
|
+
import { lookup as lookupMimeType } from "mime-types";
|
|
327
|
+
import { attachment } from "spectrum-ts";
|
|
328
|
+
var EMOJI_GLYPHS = {
|
|
329
|
+
heart: "\u2764\uFE0F",
|
|
330
|
+
love: "\u2764\uFE0F",
|
|
331
|
+
thumbs_up: "\u{1F44D}",
|
|
332
|
+
like: "\u{1F44D}",
|
|
333
|
+
thumbs_down: "\u{1F44E}",
|
|
334
|
+
dislike: "\u{1F44E}",
|
|
335
|
+
laugh: "\u{1F602}",
|
|
336
|
+
emphasize: "\u203C\uFE0F",
|
|
337
|
+
exclamation: "\u203C\uFE0F",
|
|
338
|
+
question: "\u2753"
|
|
339
|
+
};
|
|
340
|
+
function emojiToGlyph(emoji) {
|
|
341
|
+
const name = typeof emoji === "string" ? emoji : emoji.name;
|
|
342
|
+
const glyph = EMOJI_GLYPHS[name];
|
|
343
|
+
if (!glyph) {
|
|
344
|
+
throw new ValidationError3(
|
|
345
|
+
"imessage",
|
|
346
|
+
`Unsupported iMessage tapback: "${name}". Supported: heart, thumbs_up, thumbs_down, laugh, emphasize, question`
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
return glyph;
|
|
350
|
+
}
|
|
351
|
+
async function fileToAttachment(file) {
|
|
352
|
+
const data = file.data;
|
|
353
|
+
let buffer;
|
|
354
|
+
if (Buffer.isBuffer(data)) {
|
|
355
|
+
buffer = data;
|
|
356
|
+
} else if (data instanceof Blob) {
|
|
357
|
+
buffer = Buffer.from(await data.arrayBuffer());
|
|
358
|
+
} else {
|
|
359
|
+
buffer = Buffer.from(data);
|
|
360
|
+
}
|
|
361
|
+
const name = file.filename || "attachment";
|
|
362
|
+
const mimeType = file.mimeType || lookupMimeType(name) || "application/octet-stream";
|
|
363
|
+
return attachment(buffer, { name, mimeType });
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// src/internal/webhook.ts
|
|
367
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
368
|
+
var SPECTRUM_SIGNATURE_HEADER = "x-spectrum-signature";
|
|
369
|
+
var SPECTRUM_TIMESTAMP_HEADER = "x-spectrum-timestamp";
|
|
370
|
+
var SPECTRUM_EVENT_HEADER = "x-spectrum-event";
|
|
371
|
+
var SPECTRUM_MESSAGES_EVENT = "messages";
|
|
372
|
+
var TIMESTAMP_TOLERANCE_SEC = 5 * 60;
|
|
373
|
+
function verifySpectrumSignature(opts) {
|
|
374
|
+
const { rawBody, secret, signature, timestamp } = opts;
|
|
375
|
+
if (!(signature && timestamp)) {
|
|
376
|
+
return { ok: false, status: 400, reason: "missing signature headers" };
|
|
377
|
+
}
|
|
378
|
+
const nowSec = opts.now ?? Math.floor(Date.now() / 1e3);
|
|
379
|
+
const age = Math.abs(nowSec - Number(timestamp));
|
|
380
|
+
if (!Number.isFinite(age) || age > TIMESTAMP_TOLERANCE_SEC) {
|
|
381
|
+
return { ok: false, status: 400, reason: "stale or invalid timestamp" };
|
|
382
|
+
}
|
|
383
|
+
const expected = `v0=${createHmac("sha256", secret).update(`v0:${timestamp}:${rawBody}`).digest("hex")}`;
|
|
384
|
+
const a = Buffer.from(expected);
|
|
385
|
+
const b = Buffer.from(signature);
|
|
386
|
+
if (a.length !== b.length || !timingSafeEqual(a, b)) {
|
|
387
|
+
return { ok: false, status: 401, reason: "bad signature" };
|
|
388
|
+
}
|
|
389
|
+
return { ok: true };
|
|
390
|
+
}
|
|
391
|
+
function buildChatMessageFromWebhook(message, space) {
|
|
392
|
+
return buildChatMessageFromFields({
|
|
393
|
+
id: message.id,
|
|
394
|
+
chatGuid: space.id,
|
|
395
|
+
content: message.content,
|
|
396
|
+
senderId: message.sender?.id ?? "",
|
|
397
|
+
isOutbound: message.direction === "outbound",
|
|
398
|
+
timestamp: message.timestamp ? new Date(message.timestamp) : /* @__PURE__ */ new Date(),
|
|
399
|
+
raw: message
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// src/markdown.ts
|
|
404
|
+
import {
|
|
405
|
+
BaseFormatConverter,
|
|
406
|
+
getNodeChildren,
|
|
407
|
+
getNodeValue,
|
|
408
|
+
isBlockquoteNode,
|
|
409
|
+
isCodeNode,
|
|
410
|
+
isDeleteNode,
|
|
411
|
+
isEmphasisNode,
|
|
412
|
+
isInlineCodeNode,
|
|
413
|
+
isLinkNode,
|
|
414
|
+
isListItemNode,
|
|
415
|
+
isListNode,
|
|
416
|
+
isParagraphNode,
|
|
417
|
+
isStrongNode,
|
|
418
|
+
isTextNode,
|
|
419
|
+
parseMarkdown as parseMarkdown2
|
|
420
|
+
} from "chat";
|
|
421
|
+
var iMessageFormatConverter = class extends BaseFormatConverter {
|
|
422
|
+
/**
|
|
423
|
+
* Render an AST to iMessage plain text format.
|
|
424
|
+
* Strips all formatting markers since iMessage doesn't support rich text via API.
|
|
425
|
+
*/
|
|
426
|
+
fromAst(ast) {
|
|
427
|
+
return this.fromAstWithNodeConverter(
|
|
428
|
+
ast,
|
|
429
|
+
(node) => this.nodeToPlainText(node)
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Parse iMessage text into an AST.
|
|
434
|
+
* iMessage sends plain text, so we just parse it as markdown.
|
|
435
|
+
*/
|
|
436
|
+
toAst(text) {
|
|
437
|
+
return parseMarkdown2(text);
|
|
438
|
+
}
|
|
439
|
+
nodeToPlainText(node) {
|
|
440
|
+
if (isParagraphNode(node)) {
|
|
441
|
+
return getNodeChildren(node).map((child) => this.nodeToPlainText(child)).join("");
|
|
442
|
+
}
|
|
443
|
+
if (isTextNode(node)) {
|
|
444
|
+
return node.value;
|
|
445
|
+
}
|
|
446
|
+
if (isStrongNode(node) || isEmphasisNode(node) || isDeleteNode(node)) {
|
|
447
|
+
return getNodeChildren(node).map((child) => this.nodeToPlainText(child)).join("");
|
|
448
|
+
}
|
|
449
|
+
if (isInlineCodeNode(node)) {
|
|
450
|
+
return node.value;
|
|
451
|
+
}
|
|
452
|
+
if (isCodeNode(node)) {
|
|
453
|
+
return node.value;
|
|
454
|
+
}
|
|
455
|
+
if (isLinkNode(node)) {
|
|
456
|
+
const linkText = getNodeChildren(node).map((child) => this.nodeToPlainText(child)).join("");
|
|
457
|
+
return linkText ? `${linkText} (${node.url})` : node.url;
|
|
458
|
+
}
|
|
459
|
+
if (isBlockquoteNode(node)) {
|
|
460
|
+
return getNodeChildren(node).map((child) => `> ${this.nodeToPlainText(child)}`).join("\n");
|
|
461
|
+
}
|
|
462
|
+
if (isListNode(node)) {
|
|
463
|
+
return getNodeChildren(node).map((item, i) => {
|
|
464
|
+
const prefix = node.ordered ? `${i + 1}.` : "-";
|
|
465
|
+
const content = getNodeChildren(item).map((child) => this.nodeToPlainText(child)).join("");
|
|
466
|
+
return `${prefix} ${content}`;
|
|
467
|
+
}).join("\n");
|
|
468
|
+
}
|
|
469
|
+
if (isListItemNode(node)) {
|
|
470
|
+
return getNodeChildren(node).map((child) => this.nodeToPlainText(child)).join("");
|
|
471
|
+
}
|
|
472
|
+
if (node.type === "break") {
|
|
473
|
+
return "\n";
|
|
474
|
+
}
|
|
475
|
+
if (node.type === "thematicBreak") {
|
|
476
|
+
return "---";
|
|
477
|
+
}
|
|
478
|
+
const children = getNodeChildren(node);
|
|
479
|
+
if (children.length > 0) {
|
|
480
|
+
return children.map((child) => this.nodeToPlainText(child)).join("");
|
|
481
|
+
}
|
|
482
|
+
return getNodeValue(node);
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
// src/adapter.ts
|
|
487
|
+
var TYPING_DURATION_MS = 3e3;
|
|
488
|
+
var iMessageAdapter = class {
|
|
489
|
+
name = "imessage";
|
|
490
|
+
userName = "";
|
|
491
|
+
local;
|
|
492
|
+
serverUrl;
|
|
493
|
+
apiKey;
|
|
494
|
+
projectId;
|
|
495
|
+
projectSecret;
|
|
496
|
+
clients;
|
|
497
|
+
phone;
|
|
498
|
+
webhookSecret;
|
|
499
|
+
/** The spectrum-ts instance — null until `initialize()` runs. */
|
|
500
|
+
app = null;
|
|
501
|
+
chat = null;
|
|
502
|
+
logger;
|
|
503
|
+
formatConverter = new iMessageFormatConverter();
|
|
504
|
+
cache = new InboundCache();
|
|
505
|
+
modals = new ModalPollRegistry();
|
|
506
|
+
gatewayOptions;
|
|
507
|
+
pump = null;
|
|
508
|
+
constructor(config) {
|
|
509
|
+
if (config.local && process.platform !== "darwin") {
|
|
510
|
+
throw new ValidationError4(
|
|
511
|
+
"imessage",
|
|
512
|
+
"iMessage adapter local mode requires macOS. Current platform: " + process.platform
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
this.local = config.local;
|
|
516
|
+
this.logger = config.logger;
|
|
517
|
+
this.serverUrl = config.serverUrl;
|
|
518
|
+
this.apiKey = config.apiKey;
|
|
519
|
+
if (!config.local) {
|
|
520
|
+
this.projectId = config.projectId;
|
|
521
|
+
this.projectSecret = config.projectSecret;
|
|
522
|
+
this.phone = config.phone;
|
|
523
|
+
this.clients = toClientArray(config.clients);
|
|
524
|
+
this.webhookSecret = config.webhookSecret?.trim();
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
async initialize(chat) {
|
|
528
|
+
this.chat = chat;
|
|
529
|
+
const { providerConfig, projectId, projectSecret } = resolveSpectrumConfig(
|
|
530
|
+
this.local,
|
|
531
|
+
{
|
|
532
|
+
apiKey: this.apiKey,
|
|
533
|
+
clients: this.clients,
|
|
534
|
+
phone: this.phone,
|
|
535
|
+
projectId: this.projectId,
|
|
536
|
+
projectSecret: this.projectSecret,
|
|
537
|
+
serverUrl: this.serverUrl
|
|
538
|
+
}
|
|
539
|
+
);
|
|
540
|
+
const providers = [imessage.config(providerConfig)];
|
|
541
|
+
this.app = projectId && projectSecret ? await Spectrum({ providers, projectId, projectSecret }) : await Spectrum({ providers });
|
|
542
|
+
this.pump = new MessagePump(
|
|
543
|
+
() => {
|
|
544
|
+
if (!this.app) {
|
|
545
|
+
throw new Error("Adapter not initialized");
|
|
546
|
+
}
|
|
547
|
+
return this.app.messages;
|
|
548
|
+
},
|
|
549
|
+
(space, message) => this.routeInbound(space, message, this.gatewayOptions),
|
|
550
|
+
this.logger
|
|
551
|
+
);
|
|
552
|
+
let mode;
|
|
553
|
+
if (this.local) {
|
|
554
|
+
mode = "local";
|
|
555
|
+
} else if (projectId) {
|
|
556
|
+
mode = "cloud";
|
|
557
|
+
} else {
|
|
558
|
+
mode = "self-host";
|
|
559
|
+
}
|
|
560
|
+
this.logger.info("iMessage adapter initialized", {
|
|
561
|
+
local: this.local,
|
|
562
|
+
mode
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Handle a Spectrum Cloud webhook delivery (signed JSON `messages` event).
|
|
567
|
+
*
|
|
568
|
+
* Verifies the `X-Spectrum-Signature` HMAC, then routes the message into the
|
|
569
|
+
* Chat SDK. Webhook deliveries are receive-only: a delivered thread has no
|
|
570
|
+
* live spectrum-ts `Space`, so replying/reacting still requires the gateway
|
|
571
|
+
* stream (see `startGatewayListener`).
|
|
572
|
+
*
|
|
573
|
+
* @see https://photon.codes/docs/webhooks/overview
|
|
574
|
+
*/
|
|
575
|
+
async handleWebhook(request, options) {
|
|
576
|
+
if (!this.chat) {
|
|
577
|
+
return new Response("Chat instance not initialized", { status: 500 });
|
|
578
|
+
}
|
|
579
|
+
if (this.local) {
|
|
580
|
+
return new Response(
|
|
581
|
+
"Webhooks require remote (cloud) mode \u2014 local mode receives via startGatewayListener()",
|
|
582
|
+
{ status: 501 }
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
if (!this.webhookSecret) {
|
|
586
|
+
return new Response(
|
|
587
|
+
"Webhook signing secret not configured (set IMESSAGE_WEBHOOK_SECRET)",
|
|
588
|
+
{ status: 500 }
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
const rawBody = await request.text();
|
|
592
|
+
const verdict = verifySpectrumSignature({
|
|
593
|
+
secret: this.webhookSecret,
|
|
594
|
+
signature: request.headers.get(SPECTRUM_SIGNATURE_HEADER),
|
|
595
|
+
timestamp: request.headers.get(SPECTRUM_TIMESTAMP_HEADER),
|
|
596
|
+
rawBody
|
|
597
|
+
});
|
|
598
|
+
if (!verdict.ok) {
|
|
599
|
+
this.logger.warn("Rejected iMessage webhook delivery", {
|
|
600
|
+
reason: verdict.reason
|
|
601
|
+
});
|
|
602
|
+
return new Response(verdict.reason, { status: verdict.status });
|
|
603
|
+
}
|
|
604
|
+
const event = request.headers.get(SPECTRUM_EVENT_HEADER);
|
|
605
|
+
if (event && event !== SPECTRUM_MESSAGES_EVENT) {
|
|
606
|
+
return new Response(null, { status: 204 });
|
|
607
|
+
}
|
|
608
|
+
let payload;
|
|
609
|
+
try {
|
|
610
|
+
payload = JSON.parse(rawBody);
|
|
611
|
+
} catch {
|
|
612
|
+
return new Response("Invalid JSON body", { status: 400 });
|
|
613
|
+
}
|
|
614
|
+
if (payload.event !== SPECTRUM_MESSAGES_EVENT || !(payload.message && payload.space)) {
|
|
615
|
+
return new Response(null, { status: 204 });
|
|
616
|
+
}
|
|
617
|
+
this.routeWebhookMessage(payload, options);
|
|
618
|
+
return new Response(null, { status: 200 });
|
|
619
|
+
}
|
|
620
|
+
async postMessage(threadId, message) {
|
|
621
|
+
const space = await this.requireSpace(threadId, "postMessage");
|
|
622
|
+
const body = this.formatConverter.renderPostable(message);
|
|
623
|
+
const files = extractFiles(message);
|
|
624
|
+
let first;
|
|
625
|
+
if (body && body.trim().length > 0) {
|
|
626
|
+
first = await space.send(textContent(body)) ?? first;
|
|
627
|
+
}
|
|
628
|
+
for (const file of files) {
|
|
629
|
+
const sent = await space.send(await fileToAttachment(file)) ?? void 0;
|
|
630
|
+
first ??= sent;
|
|
631
|
+
}
|
|
632
|
+
if (!first) {
|
|
633
|
+
throw new ValidationError4(
|
|
634
|
+
"imessage",
|
|
635
|
+
"postMessage requires non-empty text or at least one attachment"
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
return { id: first.id, threadId, raw: first };
|
|
639
|
+
}
|
|
640
|
+
async editMessage(threadId, messageId, message) {
|
|
641
|
+
if (this.local) {
|
|
642
|
+
throw new NotImplementedError(
|
|
643
|
+
"editMessage is not supported in local mode",
|
|
644
|
+
"editMessage"
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
const target = await this.resolveMessage(threadId, messageId);
|
|
648
|
+
if (!target) {
|
|
649
|
+
throw new NotImplementedError(
|
|
650
|
+
"editMessage requires the original message to have been received in this session",
|
|
651
|
+
"editMessage"
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
await target.edit(
|
|
655
|
+
textContent(this.formatConverter.renderPostable(message))
|
|
656
|
+
);
|
|
657
|
+
return { id: messageId, threadId, raw: target };
|
|
658
|
+
}
|
|
659
|
+
async deleteMessage(_threadId, _messageId) {
|
|
660
|
+
throw new NotImplementedError(
|
|
661
|
+
"deleteMessage is not implemented",
|
|
662
|
+
"deleteMessage"
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
parseMessage(raw) {
|
|
666
|
+
const message = raw;
|
|
667
|
+
return buildChatMessage(message, message.space);
|
|
668
|
+
}
|
|
669
|
+
async fetchMessages(_threadId, _options) {
|
|
670
|
+
throw new NotImplementedError(
|
|
671
|
+
"fetchMessages (message history) is not supported by spectrum-ts",
|
|
672
|
+
"fetchMessages"
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
async fetchThread(_threadId) {
|
|
676
|
+
throw new NotImplementedError(
|
|
677
|
+
"fetchThread (chat info) is not supported by spectrum-ts",
|
|
678
|
+
"fetchThread"
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
channelIdFromThreadId(threadId) {
|
|
682
|
+
return threadId;
|
|
683
|
+
}
|
|
684
|
+
async addReaction(threadId, messageId, emoji) {
|
|
685
|
+
if (this.local) {
|
|
686
|
+
throw new NotImplementedError(
|
|
687
|
+
"addReaction is not supported in local mode",
|
|
688
|
+
"addReaction"
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
const glyph = emojiToGlyph(emoji);
|
|
692
|
+
const target = await this.resolveMessage(threadId, messageId);
|
|
693
|
+
if (!target) {
|
|
694
|
+
throw new NotImplementedError(
|
|
695
|
+
"addReaction requires the target message to have been received in this session",
|
|
696
|
+
"addReaction"
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
await target.react(glyph);
|
|
700
|
+
}
|
|
701
|
+
async removeReaction(_threadId, _messageId, _emoji) {
|
|
702
|
+
throw new NotImplementedError(
|
|
703
|
+
"removeReaction is not supported (spectrum-ts has no reaction-removal API)",
|
|
704
|
+
"removeReaction"
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
async startTyping(threadId, _status) {
|
|
708
|
+
if (this.local) {
|
|
709
|
+
throw new NotImplementedError(
|
|
710
|
+
"startTyping is not supported in local mode",
|
|
711
|
+
"startTyping"
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
const space = await this.requireSpace(threadId, "startTyping");
|
|
715
|
+
await space.startTyping();
|
|
716
|
+
setTimeout(() => {
|
|
717
|
+
space.stopTyping().catch(() => {
|
|
718
|
+
});
|
|
719
|
+
}, TYPING_DURATION_MS);
|
|
720
|
+
}
|
|
721
|
+
async openModal(triggerId, modal, contextId) {
|
|
722
|
+
if (this.local) {
|
|
723
|
+
throw new NotImplementedError(
|
|
724
|
+
"openModal is not supported in local mode",
|
|
725
|
+
"openModal"
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
const select = modal.children.find(
|
|
729
|
+
(c) => c.type === "select"
|
|
730
|
+
);
|
|
731
|
+
if (!select) {
|
|
732
|
+
throw new ValidationError4(
|
|
733
|
+
"imessage",
|
|
734
|
+
"openModal requires at least one Select child \u2014 iMessage modals map to native polls"
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
const labels = select.options.map((o) => o.label);
|
|
738
|
+
if (labels.length < 2 || labels.length > 10) {
|
|
739
|
+
throw new ValidationError4(
|
|
740
|
+
"imessage",
|
|
741
|
+
`iMessage polls require between 2 and 10 options, received ${labels.length}`
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
const { chatGuid } = decodeThreadId(triggerId);
|
|
745
|
+
const space = await this.requireSpace(triggerId, "openModal");
|
|
746
|
+
const sent = await space.send(pollContent(modal.title, labels));
|
|
747
|
+
const viewId = sent?.id ?? `poll-${Date.now()}`;
|
|
748
|
+
this.modals.register(chatGuid, modal.title, {
|
|
749
|
+
viewId,
|
|
750
|
+
callbackId: modal.callbackId,
|
|
751
|
+
selectId: select.id,
|
|
752
|
+
options: select.options,
|
|
753
|
+
contextId,
|
|
754
|
+
privateMetadata: modal.privateMetadata
|
|
755
|
+
});
|
|
756
|
+
return { viewId };
|
|
757
|
+
}
|
|
758
|
+
renderFormatted(content) {
|
|
759
|
+
return this.formatConverter.fromAst(content);
|
|
760
|
+
}
|
|
761
|
+
encodeThreadId(platformData) {
|
|
762
|
+
return encodeThreadId(platformData);
|
|
763
|
+
}
|
|
764
|
+
decodeThreadId(threadId) {
|
|
765
|
+
return decodeThreadId(threadId);
|
|
766
|
+
}
|
|
767
|
+
isDM(threadId) {
|
|
768
|
+
return isDMChatGuid(decodeThreadId(threadId).chatGuid);
|
|
769
|
+
}
|
|
770
|
+
async startGatewayListener(options, durationMs = 18e4, abortSignal) {
|
|
771
|
+
if (!this.chat) {
|
|
772
|
+
return new Response("Chat instance not initialized", { status: 500 });
|
|
773
|
+
}
|
|
774
|
+
if (!options.waitUntil) {
|
|
775
|
+
return new Response("waitUntil not provided", { status: 500 });
|
|
776
|
+
}
|
|
777
|
+
if (!(this.app && this.pump)) {
|
|
778
|
+
return new Response("Adapter not initialized", { status: 500 });
|
|
779
|
+
}
|
|
780
|
+
this.logger.info("Starting iMessage Gateway listener", {
|
|
781
|
+
durationMs,
|
|
782
|
+
mode: this.local ? "local" : "remote"
|
|
783
|
+
});
|
|
784
|
+
this.gatewayOptions = options;
|
|
785
|
+
this.pump.ensureRunning();
|
|
786
|
+
const listenerPromise = new Promise((resolve) => {
|
|
787
|
+
const timeout = setTimeout(resolve, durationMs);
|
|
788
|
+
if (abortSignal) {
|
|
789
|
+
const onAbort = () => {
|
|
790
|
+
this.logger.info("iMessage Gateway listener received abort signal");
|
|
791
|
+
clearTimeout(timeout);
|
|
792
|
+
this.pump?.stop();
|
|
793
|
+
resolve();
|
|
794
|
+
};
|
|
795
|
+
if (abortSignal.aborted) {
|
|
796
|
+
onAbort();
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
800
|
+
}
|
|
801
|
+
});
|
|
802
|
+
options.waitUntil(listenerPromise);
|
|
803
|
+
return new Response(
|
|
804
|
+
JSON.stringify({
|
|
805
|
+
status: "listening",
|
|
806
|
+
durationMs,
|
|
807
|
+
mode: this.local ? "local" : "remote",
|
|
808
|
+
message: `Gateway listener started, will run for ${durationMs / 1e3} seconds`
|
|
809
|
+
}),
|
|
810
|
+
{
|
|
811
|
+
status: 200,
|
|
812
|
+
headers: { "Content-Type": "application/json" }
|
|
813
|
+
}
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
routeWebhookMessage(payload, options) {
|
|
817
|
+
if (!this.chat) {
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
const { message, space } = payload;
|
|
821
|
+
if (message.direction === "outbound") {
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
if (message.content?.type === "reaction") {
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
const chatMessage = buildChatMessageFromWebhook(message, space);
|
|
828
|
+
this.chat.processMessage(this, chatMessage.threadId, chatMessage, options);
|
|
829
|
+
}
|
|
830
|
+
async routeInbound(space, message, options) {
|
|
831
|
+
if (!this.chat) {
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
this.cache.remember(space, message);
|
|
835
|
+
const contentType = message.content.type;
|
|
836
|
+
if (contentType === "poll_option") {
|
|
837
|
+
this.handlePollOption(space, message, options);
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
if (contentType === "reaction") {
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
if (message.direction === "outbound") {
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
const chatMessage = buildChatMessage(message, space);
|
|
847
|
+
this.chat.processMessage(this, chatMessage.threadId, chatMessage, options);
|
|
848
|
+
}
|
|
849
|
+
handlePollOption(space, message, options) {
|
|
850
|
+
if (!this.chat) {
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
const content = message.content;
|
|
854
|
+
if (content.type !== "poll_option") {
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
if (!content.selected) {
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
const resolved = this.modals.resolveVote(
|
|
861
|
+
space.id,
|
|
862
|
+
content.poll.title,
|
|
863
|
+
content.option.title
|
|
864
|
+
);
|
|
865
|
+
if (!resolved) {
|
|
866
|
+
this.logger.debug("Poll vote did not match a known modal, skipping", {
|
|
867
|
+
title: content.poll.title,
|
|
868
|
+
option: content.option.title
|
|
869
|
+
});
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
const { meta, value } = resolved;
|
|
873
|
+
const handle = message.sender?.id ?? "";
|
|
874
|
+
this.chat.processModalSubmit(
|
|
875
|
+
{
|
|
876
|
+
adapter: this,
|
|
877
|
+
callbackId: meta.callbackId,
|
|
878
|
+
privateMetadata: meta.privateMetadata,
|
|
879
|
+
viewId: meta.viewId,
|
|
880
|
+
user: {
|
|
881
|
+
userId: handle,
|
|
882
|
+
userName: handle,
|
|
883
|
+
fullName: handle,
|
|
884
|
+
isBot: false,
|
|
885
|
+
isMe: false
|
|
886
|
+
},
|
|
887
|
+
values: { [meta.selectId]: value },
|
|
888
|
+
raw: message
|
|
889
|
+
},
|
|
890
|
+
meta.contextId,
|
|
891
|
+
options
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Resolve a sendable spectrum-ts `Space` for a thread.
|
|
896
|
+
*
|
|
897
|
+
* Prefers a live `Space` cached from the inbound stream (correct sending
|
|
898
|
+
* line, no extra round-trip). On a miss — e.g. a webhook-only deployment, or
|
|
899
|
+
* a cold send — it reconstructs the Space over gRPC via
|
|
900
|
+
* `imessage(app).space([address])`. Only DMs can be rebuilt from a chatGuid:
|
|
901
|
+
* the resolver derives them from the peer address, whereas an unseen group
|
|
902
|
+
* has no by-id resolver (passing participants would create a *new* chat).
|
|
903
|
+
* Returns `undefined` when no Space can be obtained.
|
|
904
|
+
*/
|
|
905
|
+
async resolveSpace(threadId) {
|
|
906
|
+
const { chatGuid } = decodeThreadId(threadId);
|
|
907
|
+
const cached = this.cache.getSpace(chatGuid);
|
|
908
|
+
if (cached) {
|
|
909
|
+
return cached;
|
|
910
|
+
}
|
|
911
|
+
if (!this.app || this.local || !isDMChatGuid(chatGuid)) {
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
const address = dmAddressFromChatGuid(chatGuid);
|
|
915
|
+
if (!address) {
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
const platform = imessage(this.app);
|
|
919
|
+
const space = await platform.space([address]);
|
|
920
|
+
this.cache.rememberSpace(space);
|
|
921
|
+
return space;
|
|
922
|
+
}
|
|
923
|
+
async requireSpace(threadId, action) {
|
|
924
|
+
const space = await this.resolveSpace(threadId);
|
|
925
|
+
if (!space) {
|
|
926
|
+
throw new NotImplementedError(
|
|
927
|
+
`${action} requires a DM thread (rebuilt from its address) or a group received in this session; spectrum-ts cannot reconstruct an unseen group chat from its id. Respond within a received message's thread instead.`,
|
|
928
|
+
action
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
return space;
|
|
932
|
+
}
|
|
933
|
+
async resolveMessage(threadId, messageId) {
|
|
934
|
+
const cached = this.cache.getMessage(messageId);
|
|
935
|
+
if (cached) {
|
|
936
|
+
return cached;
|
|
937
|
+
}
|
|
938
|
+
const space = await this.resolveSpace(threadId);
|
|
939
|
+
if (!space) {
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
return await space.getMessage(messageId) ?? void 0;
|
|
943
|
+
}
|
|
944
|
+
};
|
|
945
|
+
function toClientArray(clients) {
|
|
946
|
+
if (!clients) {
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
return Array.isArray(clients) ? clients : [clients];
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// src/factory.ts
|
|
953
|
+
import { ValidationError as ValidationError5 } from "@chat-adapter/shared";
|
|
954
|
+
import { ConsoleLogger } from "chat";
|
|
955
|
+
function resolveLocalMode(explicit, hasRemoteCreds) {
|
|
956
|
+
if (explicit !== void 0) {
|
|
957
|
+
return explicit;
|
|
958
|
+
}
|
|
959
|
+
const env = process.env.IMESSAGE_LOCAL;
|
|
960
|
+
if (env === "false") {
|
|
961
|
+
return false;
|
|
962
|
+
}
|
|
963
|
+
if (env === void 0) {
|
|
964
|
+
return !hasRemoteCreds;
|
|
965
|
+
}
|
|
966
|
+
return true;
|
|
967
|
+
}
|
|
968
|
+
function assertRemoteCredentials(creds) {
|
|
969
|
+
if (creds.hasCloud || creds.hasClients) {
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
if (!creds.hasServerUrl) {
|
|
973
|
+
throw new ValidationError5(
|
|
974
|
+
"imessage",
|
|
975
|
+
"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
|
+
);
|
|
977
|
+
}
|
|
978
|
+
if (!creds.hasApiKey) {
|
|
979
|
+
throw new ValidationError5(
|
|
980
|
+
"imessage",
|
|
981
|
+
"apiKey is required when local is false. Set IMESSAGE_API_KEY or provide it in config."
|
|
982
|
+
);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
function createiMessageAdapter(config) {
|
|
986
|
+
const logger = config?.logger ?? new ConsoleLogger("info").child("imessage");
|
|
987
|
+
const projectId = config?.projectId ?? process.env.IMESSAGE_PROJECT_ID;
|
|
988
|
+
const projectSecret = config?.projectSecret ?? process.env.IMESSAGE_PROJECT_SECRET;
|
|
989
|
+
const clients = config?.clients;
|
|
990
|
+
const serverUrl = (config?.serverUrl ?? process.env.IMESSAGE_SERVER_URL)?.trim();
|
|
991
|
+
const apiKey = (config?.apiKey ?? process.env.IMESSAGE_API_KEY)?.trim();
|
|
992
|
+
const phone = config?.phone ?? process.env.IMESSAGE_PHONE;
|
|
993
|
+
const webhookSecret = (config?.webhookSecret ?? process.env.IMESSAGE_WEBHOOK_SECRET)?.trim();
|
|
994
|
+
const hasClients = Array.isArray(clients) ? clients.length > 0 : Boolean(clients);
|
|
995
|
+
const hasCloud = Boolean(projectId && projectSecret);
|
|
996
|
+
const hasServerUrl = Boolean(serverUrl);
|
|
997
|
+
const hasApiKey = Boolean(apiKey);
|
|
998
|
+
const hasRemoteCreds = hasCloud || hasClients || hasServerUrl && hasApiKey;
|
|
999
|
+
if (resolveLocalMode(config?.local, hasRemoteCreds)) {
|
|
1000
|
+
return new iMessageAdapter({ local: true, logger, serverUrl, apiKey });
|
|
1001
|
+
}
|
|
1002
|
+
assertRemoteCredentials({ hasApiKey, hasClients, hasCloud, hasServerUrl });
|
|
1003
|
+
return new iMessageAdapter({
|
|
1004
|
+
local: false,
|
|
1005
|
+
logger,
|
|
1006
|
+
projectId,
|
|
1007
|
+
projectSecret,
|
|
1008
|
+
clients,
|
|
1009
|
+
serverUrl,
|
|
1010
|
+
apiKey,
|
|
1011
|
+
phone,
|
|
1012
|
+
webhookSecret
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
export {
|
|
1016
|
+
createiMessageAdapter,
|
|
1017
|
+
deriveAddress,
|
|
1018
|
+
iMessageAdapter,
|
|
1019
|
+
iMessageFormatConverter
|
|
1020
|
+
};
|
|
1021
|
+
//# sourceMappingURL=index.js.map
|