spectrum-ts 1.4.0 → 1.5.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/dist/chunk-4U4RINOV.js +2133 -0
- package/dist/{chunk-FF2R4EP3.js → chunk-L6LUFBLF.js} +4 -3
- package/dist/chunk-NIIJ6U34.js +843 -0
- package/dist/chunk-NNRUJOPT.js +849 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -4
- package/dist/providers/imessage/index.d.ts +3 -3
- package/dist/providers/imessage/index.js +6 -2097
- package/dist/providers/index.d.ts +12 -0
- package/dist/providers/index.js +18 -0
- package/dist/providers/terminal/index.d.ts +7 -7
- package/dist/providers/terminal/index.js +4 -838
- package/dist/providers/whatsapp-business/index.d.ts +3 -3
- package/dist/providers/whatsapp-business/index.js +4 -844
- package/dist/{types-BcCLW2VO.d.ts → types-D0QSU6kb.d.ts} +1 -1
- package/package.json +7 -3
|
@@ -0,0 +1,2133 @@
|
|
|
1
|
+
import {
|
|
2
|
+
asGroup,
|
|
3
|
+
asRichlink,
|
|
4
|
+
groupSchema
|
|
5
|
+
} from "./chunk-66GJ45ZZ.js";
|
|
6
|
+
import {
|
|
7
|
+
asPoll,
|
|
8
|
+
asPollOption,
|
|
9
|
+
cloud,
|
|
10
|
+
mergeStreams,
|
|
11
|
+
stream
|
|
12
|
+
} from "./chunk-L6LUFBLF.js";
|
|
13
|
+
import {
|
|
14
|
+
UnsupportedError,
|
|
15
|
+
asAttachment,
|
|
16
|
+
asContact,
|
|
17
|
+
asCustom,
|
|
18
|
+
asText,
|
|
19
|
+
attachmentSchema,
|
|
20
|
+
definePlatform,
|
|
21
|
+
fromVCard,
|
|
22
|
+
reactionSchema,
|
|
23
|
+
text,
|
|
24
|
+
textSchema,
|
|
25
|
+
toVCard
|
|
26
|
+
} from "./chunk-LH4YEBG3.js";
|
|
27
|
+
|
|
28
|
+
// src/providers/imessage/index.ts
|
|
29
|
+
import { createClient as createClient2, MessageEffect as MessageEffect2 } from "@photon-ai/advanced-imessage";
|
|
30
|
+
import { IMessageSDK as IMessageSDK2 } from "@photon-ai/imessage-kit";
|
|
31
|
+
|
|
32
|
+
// src/providers/imessage/content/effect.ts
|
|
33
|
+
import {
|
|
34
|
+
MessageEffect
|
|
35
|
+
} from "@photon-ai/advanced-imessage";
|
|
36
|
+
|
|
37
|
+
// src/content/effect.ts
|
|
38
|
+
import z from "zod";
|
|
39
|
+
var effectInnerSchema = z.discriminatedUnion("type", [
|
|
40
|
+
textSchema,
|
|
41
|
+
attachmentSchema
|
|
42
|
+
]);
|
|
43
|
+
var messageEffectSchema = z.object({
|
|
44
|
+
type: z.literal("effect"),
|
|
45
|
+
content: effectInnerSchema,
|
|
46
|
+
effect: z.string().nonempty()
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// src/providers/imessage/content/effect.ts
|
|
50
|
+
var SUPPORTED_EFFECTS = new Set(Object.values(MessageEffect));
|
|
51
|
+
var resolveContent = (input) => typeof input === "string" ? text(input).build() : input.build();
|
|
52
|
+
function effect(input, messageEffect) {
|
|
53
|
+
return {
|
|
54
|
+
build: async () => {
|
|
55
|
+
if (!SUPPORTED_EFFECTS.has(messageEffect)) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Unsupported iMessage message effect "${messageEffect}"`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
const inner = await resolveContent(input);
|
|
61
|
+
if (inner.type !== "text" && inner.type !== "attachment") {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`imessage effect() only supports text and attachment content, got "${inner.type}"`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
return messageEffectSchema.parse({
|
|
67
|
+
type: "effect",
|
|
68
|
+
content: inner,
|
|
69
|
+
effect: messageEffect
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/providers/imessage/auth.ts
|
|
76
|
+
import { createClient } from "@photon-ai/advanced-imessage";
|
|
77
|
+
|
|
78
|
+
// src/providers/imessage/types.ts
|
|
79
|
+
import { IMessageSDK } from "@photon-ai/imessage-kit";
|
|
80
|
+
import z2 from "zod";
|
|
81
|
+
var SHARED_PHONE = "shared";
|
|
82
|
+
var isLocal = (client) => client instanceof IMessageSDK;
|
|
83
|
+
var clientEntry = z2.object({
|
|
84
|
+
address: z2.string(),
|
|
85
|
+
token: z2.string(),
|
|
86
|
+
phone: z2.string()
|
|
87
|
+
});
|
|
88
|
+
var configSchema = z2.union([
|
|
89
|
+
z2.object({ local: z2.literal(true) }),
|
|
90
|
+
z2.object({
|
|
91
|
+
local: z2.literal(false).optional().default(false),
|
|
92
|
+
clients: clientEntry.or(z2.array(clientEntry)).optional()
|
|
93
|
+
})
|
|
94
|
+
]);
|
|
95
|
+
var userSchema = z2.object({});
|
|
96
|
+
var spaceSchema = z2.object({
|
|
97
|
+
id: z2.string(),
|
|
98
|
+
type: z2.enum(["dm", "group"]),
|
|
99
|
+
phone: z2.string()
|
|
100
|
+
});
|
|
101
|
+
var spaceParamsSchema = z2.object({
|
|
102
|
+
phone: z2.string().optional()
|
|
103
|
+
});
|
|
104
|
+
var messageSchema = z2.object({
|
|
105
|
+
partIndex: z2.number().int().nonnegative().optional(),
|
|
106
|
+
parentId: z2.string().optional()
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// src/providers/imessage/auth.ts
|
|
110
|
+
var RENEWAL_RATIO = 0.8;
|
|
111
|
+
var EXPIRY_BUFFER_MS = 3e4;
|
|
112
|
+
var RETRY_DELAY_MS = 3e4;
|
|
113
|
+
var cloudAuthState = /* @__PURE__ */ new WeakMap();
|
|
114
|
+
var requirePhone = (data, instanceId) => {
|
|
115
|
+
const phone = data.numbers?.[instanceId];
|
|
116
|
+
if (!phone) {
|
|
117
|
+
throw new Error(`iMessage instance ${instanceId} has no phone assigned`);
|
|
118
|
+
}
|
|
119
|
+
return phone;
|
|
120
|
+
};
|
|
121
|
+
async function createCloudClients(projectId, projectSecret) {
|
|
122
|
+
let tokenData = await cloud.issueImessageTokens(projectId, projectSecret);
|
|
123
|
+
let tokenExpiresAt = Date.now() + tokenData.expiresIn * 1e3;
|
|
124
|
+
let disposed = false;
|
|
125
|
+
let renewalTimer;
|
|
126
|
+
const records = [];
|
|
127
|
+
const syncPhones = (data) => {
|
|
128
|
+
for (const { entry, instanceId } of records) {
|
|
129
|
+
entry.phone = requirePhone(data, instanceId);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
const scheduleRenewal = () => {
|
|
133
|
+
if (disposed) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const ttlMs = tokenData.expiresIn * 1e3;
|
|
137
|
+
const renewInMs = Math.max(ttlMs * RENEWAL_RATIO, 5e3);
|
|
138
|
+
renewalTimer = setTimeout(async () => {
|
|
139
|
+
try {
|
|
140
|
+
tokenData = await cloud.issueImessageTokens(projectId, projectSecret);
|
|
141
|
+
tokenExpiresAt = Date.now() + tokenData.expiresIn * 1e3;
|
|
142
|
+
if (tokenData.type === "dedicated") {
|
|
143
|
+
syncPhones(tokenData);
|
|
144
|
+
}
|
|
145
|
+
scheduleRenewal();
|
|
146
|
+
} catch {
|
|
147
|
+
renewalTimer = setTimeout(() => scheduleRenewal(), RETRY_DELAY_MS);
|
|
148
|
+
renewalTimer?.unref?.();
|
|
149
|
+
}
|
|
150
|
+
}, renewInMs);
|
|
151
|
+
renewalTimer?.unref?.();
|
|
152
|
+
};
|
|
153
|
+
scheduleRenewal();
|
|
154
|
+
const refreshIfNeeded = async () => {
|
|
155
|
+
if (Date.now() < tokenExpiresAt - EXPIRY_BUFFER_MS) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
tokenData = await cloud.issueImessageTokens(projectId, projectSecret);
|
|
159
|
+
tokenExpiresAt = Date.now() + tokenData.expiresIn * 1e3;
|
|
160
|
+
if (tokenData.type === "dedicated") {
|
|
161
|
+
syncPhones(tokenData);
|
|
162
|
+
}
|
|
163
|
+
scheduleRenewal();
|
|
164
|
+
};
|
|
165
|
+
if (tokenData.type === "shared") {
|
|
166
|
+
const address = process.env.SPECTRUM_IMESSAGE_ADDRESS ?? "imessage.spectrum.photon.codes:443";
|
|
167
|
+
const entries2 = [
|
|
168
|
+
{
|
|
169
|
+
phone: SHARED_PHONE,
|
|
170
|
+
client: createClient({
|
|
171
|
+
address,
|
|
172
|
+
tls: true,
|
|
173
|
+
token: async () => {
|
|
174
|
+
await refreshIfNeeded();
|
|
175
|
+
return tokenData.token;
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
];
|
|
180
|
+
cloudAuthState.set(entries2, {
|
|
181
|
+
dispose: () => {
|
|
182
|
+
disposed = true;
|
|
183
|
+
if (renewalTimer !== void 0) {
|
|
184
|
+
clearTimeout(renewalTimer);
|
|
185
|
+
renewalTimer = void 0;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
return entries2;
|
|
190
|
+
}
|
|
191
|
+
const dedicated = tokenData;
|
|
192
|
+
for (const [instanceId, token] of Object.entries(dedicated.auth)) {
|
|
193
|
+
const entry = {
|
|
194
|
+
phone: requirePhone(dedicated, instanceId),
|
|
195
|
+
client: createClient({
|
|
196
|
+
address: `${instanceId}.imsg.photon.codes:443`,
|
|
197
|
+
tls: true,
|
|
198
|
+
token: async () => {
|
|
199
|
+
await refreshIfNeeded();
|
|
200
|
+
const data = tokenData;
|
|
201
|
+
return data.auth[instanceId] ?? token;
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
};
|
|
205
|
+
records.push({ entry, instanceId });
|
|
206
|
+
}
|
|
207
|
+
const entries = records.map((r) => r.entry);
|
|
208
|
+
cloudAuthState.set(entries, {
|
|
209
|
+
dispose: () => {
|
|
210
|
+
disposed = true;
|
|
211
|
+
if (renewalTimer !== void 0) {
|
|
212
|
+
clearTimeout(renewalTimer);
|
|
213
|
+
renewalTimer = void 0;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
return entries;
|
|
218
|
+
}
|
|
219
|
+
async function disposeCloudAuth(clients) {
|
|
220
|
+
const auth = cloudAuthState.get(clients);
|
|
221
|
+
if (auth) {
|
|
222
|
+
auth.dispose();
|
|
223
|
+
cloudAuthState.delete(clients);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// src/providers/imessage/local/inbound.ts
|
|
228
|
+
import { setTimeout as sleep } from "timers/promises";
|
|
229
|
+
|
|
230
|
+
// src/providers/imessage/local/attachments.ts
|
|
231
|
+
import { createReadStream } from "fs";
|
|
232
|
+
import { readFile } from "fs/promises";
|
|
233
|
+
import { Readable } from "stream";
|
|
234
|
+
|
|
235
|
+
// src/providers/imessage/shared/vcard.ts
|
|
236
|
+
var VCARD_MIME_TYPES = /* @__PURE__ */ new Set([
|
|
237
|
+
"text/vcard",
|
|
238
|
+
"text/x-vcard",
|
|
239
|
+
"text/directory",
|
|
240
|
+
"application/vcard",
|
|
241
|
+
"application/x-vcard"
|
|
242
|
+
]);
|
|
243
|
+
var normalizeMimeType = (mimeType) => (mimeType.split(";")[0] ?? "").trim().toLowerCase();
|
|
244
|
+
var isVCardAttachment = (mimeType, fileName) => {
|
|
245
|
+
if (mimeType && VCARD_MIME_TYPES.has(normalizeMimeType(mimeType))) {
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
return Boolean(fileName?.toLowerCase().endsWith(".vcf"));
|
|
249
|
+
};
|
|
250
|
+
var vcardFileName = (contact) => {
|
|
251
|
+
const base = contact.name?.formatted ?? contact.user?.id ?? "contact";
|
|
252
|
+
return `${base.replace(/[^a-zA-Z0-9_\-.]/g, "_")}.vcf`;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
// src/providers/imessage/local/attachments.ts
|
|
256
|
+
var DEFAULT_ATTACHMENT_NAME = "attachment";
|
|
257
|
+
var readLocalAttachment = async (att) => {
|
|
258
|
+
if (!att.localPath) {
|
|
259
|
+
throw new Error(
|
|
260
|
+
`iMessage attachment ${att.id} has no local file available on disk`
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
return readFile(att.localPath);
|
|
264
|
+
};
|
|
265
|
+
var toAttachmentContent = (att) => {
|
|
266
|
+
const { localPath } = att;
|
|
267
|
+
return asAttachment({
|
|
268
|
+
name: att.fileName ?? DEFAULT_ATTACHMENT_NAME,
|
|
269
|
+
mimeType: att.mimeType,
|
|
270
|
+
size: att.sizeBytes,
|
|
271
|
+
read: () => readLocalAttachment(att),
|
|
272
|
+
stream: localPath ? async () => Readable.toWeb(
|
|
273
|
+
createReadStream(localPath)
|
|
274
|
+
) : void 0
|
|
275
|
+
});
|
|
276
|
+
};
|
|
277
|
+
var toVCardContent = async (att) => {
|
|
278
|
+
try {
|
|
279
|
+
const buf = await readLocalAttachment(att);
|
|
280
|
+
return asContact(fromVCard(buf.toString("utf8")));
|
|
281
|
+
} catch {
|
|
282
|
+
return toAttachmentContent(att);
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
var localAttachmentContent = async (att) => isVCardAttachment(att.mimeType, att.fileName) ? await toVCardContent(att) : toAttachmentContent(att);
|
|
286
|
+
|
|
287
|
+
// src/providers/imessage/local/inbound.ts
|
|
288
|
+
var ATTACHMENT_PLACEHOLDER = "\uFFFC";
|
|
289
|
+
var ATTACHMENT_JOIN_RETRY_DELAY_MS = 250;
|
|
290
|
+
var ATTACHMENT_JOIN_RETRY_LIMIT = 8;
|
|
291
|
+
var ATTACHMENT_JOIN_FETCH_LIMIT = 10;
|
|
292
|
+
var hasAttachmentPlaceholder = (message) => message.text?.includes(ATTACHMENT_PLACEHOLDER) ?? false;
|
|
293
|
+
var isPendingAttachmentJoin = (message) => message.attachments.length === 0 && (message.hasAttachments || hasAttachmentPlaceholder(message));
|
|
294
|
+
var refetchUntilAttachmentsSettle = async (client, message) => {
|
|
295
|
+
if (!message.chatId) {
|
|
296
|
+
return message;
|
|
297
|
+
}
|
|
298
|
+
for (let attempt = 0; attempt < ATTACHMENT_JOIN_RETRY_LIMIT; attempt += 1) {
|
|
299
|
+
await sleep(ATTACHMENT_JOIN_RETRY_DELAY_MS);
|
|
300
|
+
let rows;
|
|
301
|
+
try {
|
|
302
|
+
rows = await client.getMessages({
|
|
303
|
+
chatId: message.chatId,
|
|
304
|
+
limit: ATTACHMENT_JOIN_FETCH_LIMIT,
|
|
305
|
+
since: message.createdAt
|
|
306
|
+
});
|
|
307
|
+
} catch {
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
const refreshed = rows.find((row) => row.id === message.id);
|
|
311
|
+
if (refreshed && !isPendingAttachmentJoin(refreshed)) {
|
|
312
|
+
return refreshed;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return message;
|
|
316
|
+
};
|
|
317
|
+
var toMessages = async (message) => {
|
|
318
|
+
const { chatId, chatKind } = message;
|
|
319
|
+
if (!chatId || chatKind === "unknown") {
|
|
320
|
+
return [];
|
|
321
|
+
}
|
|
322
|
+
if (message.reaction !== null || message.kind !== "text" || message.retractedAt !== null) {
|
|
323
|
+
return [];
|
|
324
|
+
}
|
|
325
|
+
if (isPendingAttachmentJoin(message)) {
|
|
326
|
+
return [];
|
|
327
|
+
}
|
|
328
|
+
const base = {
|
|
329
|
+
sender: { id: message.participant ?? "" },
|
|
330
|
+
// Local mode has no concept of "which-of-my-phones"; phone is empty.
|
|
331
|
+
space: {
|
|
332
|
+
id: chatId,
|
|
333
|
+
type: chatKind === "group" ? "group" : "dm",
|
|
334
|
+
phone: ""
|
|
335
|
+
},
|
|
336
|
+
timestamp: message.createdAt
|
|
337
|
+
};
|
|
338
|
+
if (message.attachments.length > 0) {
|
|
339
|
+
return Promise.all(
|
|
340
|
+
message.attachments.map(async (att) => ({
|
|
341
|
+
...base,
|
|
342
|
+
id: `${message.id}:${att.id}`,
|
|
343
|
+
content: await localAttachmentContent(att)
|
|
344
|
+
}))
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
return [
|
|
348
|
+
{
|
|
349
|
+
...base,
|
|
350
|
+
id: message.id,
|
|
351
|
+
content: { type: "text", text: message.text ?? "" }
|
|
352
|
+
}
|
|
353
|
+
];
|
|
354
|
+
};
|
|
355
|
+
var messages = (client) => stream((emit, end) => {
|
|
356
|
+
let lastPromise = Promise.resolve();
|
|
357
|
+
const handleIncoming = async (message) => {
|
|
358
|
+
const stableMessage = isPendingAttachmentJoin(message) ? await refetchUntilAttachmentsSettle(client, message) : message;
|
|
359
|
+
const ms = await toMessages(stableMessage);
|
|
360
|
+
for (const m of ms) {
|
|
361
|
+
await emit(m);
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
const startPromise = client.startWatching({
|
|
365
|
+
onIncomingMessage: (message) => {
|
|
366
|
+
lastPromise = lastPromise.then(() => handleIncoming(message)).catch(end);
|
|
367
|
+
},
|
|
368
|
+
onError: end
|
|
369
|
+
}).catch(end);
|
|
370
|
+
return async () => {
|
|
371
|
+
await startPromise.catch(() => {
|
|
372
|
+
});
|
|
373
|
+
await client.stopWatching();
|
|
374
|
+
await lastPromise.catch(() => {
|
|
375
|
+
});
|
|
376
|
+
};
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// src/providers/imessage/local/send.ts
|
|
380
|
+
import { mkdtemp, rm, writeFile } from "fs/promises";
|
|
381
|
+
import { tmpdir } from "os";
|
|
382
|
+
import { basename, join } from "path";
|
|
383
|
+
|
|
384
|
+
// src/providers/imessage/shared/errors.ts
|
|
385
|
+
var IMESSAGE_PLATFORM = "iMessage";
|
|
386
|
+
var LOCAL_IMESSAGE_PLATFORM = "iMessage (local mode)";
|
|
387
|
+
var unsupportedRemoteContent = (type, detail) => UnsupportedError.content(type, IMESSAGE_PLATFORM, detail);
|
|
388
|
+
var unsupportedLocalContent = (type, detail) => UnsupportedError.content(type, LOCAL_IMESSAGE_PLATFORM, detail);
|
|
389
|
+
|
|
390
|
+
// src/providers/imessage/local/send.ts
|
|
391
|
+
var synthRecord = (spaceId, content) => ({
|
|
392
|
+
id: crypto.randomUUID(),
|
|
393
|
+
content,
|
|
394
|
+
space: { id: spaceId },
|
|
395
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
396
|
+
});
|
|
397
|
+
var sendTempFile = async (client, spaceId, name, data) => {
|
|
398
|
+
const safeName = basename(name) || DEFAULT_ATTACHMENT_NAME;
|
|
399
|
+
const dir = await mkdtemp(join(tmpdir(), "spectrum-"));
|
|
400
|
+
const tmp = join(dir, safeName);
|
|
401
|
+
await writeFile(tmp, data);
|
|
402
|
+
try {
|
|
403
|
+
await client.send({ to: spaceId, attachments: [tmp] });
|
|
404
|
+
} finally {
|
|
405
|
+
await rm(dir, { recursive: true, force: true }).catch(() => {
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
var send = async (client, spaceId, content) => {
|
|
410
|
+
switch (content.type) {
|
|
411
|
+
case "text":
|
|
412
|
+
await client.send({ to: spaceId, text: content.text });
|
|
413
|
+
return synthRecord(spaceId, content);
|
|
414
|
+
case "attachment":
|
|
415
|
+
await sendTempFile(client, spaceId, content.name, await content.read());
|
|
416
|
+
return synthRecord(spaceId, content);
|
|
417
|
+
case "contact": {
|
|
418
|
+
const vcf = await toVCard(content);
|
|
419
|
+
await sendTempFile(
|
|
420
|
+
client,
|
|
421
|
+
spaceId,
|
|
422
|
+
vcardFileName(content),
|
|
423
|
+
Buffer.from(vcf, "utf8")
|
|
424
|
+
);
|
|
425
|
+
return synthRecord(spaceId, content);
|
|
426
|
+
}
|
|
427
|
+
case "effect":
|
|
428
|
+
throw unsupportedLocalContent(
|
|
429
|
+
"effect",
|
|
430
|
+
"message effects require remote iMessage"
|
|
431
|
+
);
|
|
432
|
+
case "poll":
|
|
433
|
+
throw unsupportedLocalContent("poll");
|
|
434
|
+
default:
|
|
435
|
+
throw unsupportedLocalContent(content.type);
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
var getMessage = async (_client, _id) => void 0;
|
|
439
|
+
|
|
440
|
+
// src/providers/imessage/local/api.ts
|
|
441
|
+
var messages2 = (client) => messages(client);
|
|
442
|
+
var send2 = async (client, spaceId, content) => send(client, spaceId, content);
|
|
443
|
+
var getMessage2 = async (client, id) => getMessage(client, id);
|
|
444
|
+
|
|
445
|
+
// src/providers/imessage/remote/inbound.ts
|
|
446
|
+
import {
|
|
447
|
+
NotFoundError
|
|
448
|
+
} from "@photon-ai/advanced-imessage";
|
|
449
|
+
|
|
450
|
+
// src/providers/imessage/cache.ts
|
|
451
|
+
var DEFAULT_MAX = 1e3;
|
|
452
|
+
var MessageCache = class {
|
|
453
|
+
map = /* @__PURE__ */ new Map();
|
|
454
|
+
max;
|
|
455
|
+
constructor(max = DEFAULT_MAX) {
|
|
456
|
+
this.max = max;
|
|
457
|
+
}
|
|
458
|
+
get(id) {
|
|
459
|
+
return this.map.get(id);
|
|
460
|
+
}
|
|
461
|
+
set(id, message) {
|
|
462
|
+
if (this.map.has(id)) {
|
|
463
|
+
this.map.delete(id);
|
|
464
|
+
}
|
|
465
|
+
this.map.set(id, message);
|
|
466
|
+
if (this.map.size > this.max) {
|
|
467
|
+
const first = this.map.keys().next().value;
|
|
468
|
+
if (first !== void 0) {
|
|
469
|
+
this.map.delete(first);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
clear() {
|
|
474
|
+
this.map.clear();
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
var PollCache = class {
|
|
478
|
+
map = /* @__PURE__ */ new Map();
|
|
479
|
+
max;
|
|
480
|
+
selectionEventTimesByPoll = /* @__PURE__ */ new Map();
|
|
481
|
+
selectionsByPoll = /* @__PURE__ */ new Map();
|
|
482
|
+
constructor(max = DEFAULT_MAX) {
|
|
483
|
+
this.max = max;
|
|
484
|
+
}
|
|
485
|
+
get(id) {
|
|
486
|
+
return this.map.get(id);
|
|
487
|
+
}
|
|
488
|
+
set(id, poll) {
|
|
489
|
+
if (this.map.has(id)) {
|
|
490
|
+
this.map.delete(id);
|
|
491
|
+
}
|
|
492
|
+
this.map.set(id, poll);
|
|
493
|
+
if (this.map.size > this.max) {
|
|
494
|
+
const first = this.map.keys().next().value;
|
|
495
|
+
if (first !== void 0) {
|
|
496
|
+
this.map.delete(first);
|
|
497
|
+
this.selectionEventTimesByPoll.delete(first);
|
|
498
|
+
this.selectionsByPoll.delete(first);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
clear() {
|
|
503
|
+
this.map.clear();
|
|
504
|
+
this.selectionEventTimesByPoll.clear();
|
|
505
|
+
this.selectionsByPoll.clear();
|
|
506
|
+
}
|
|
507
|
+
actorSelectionDeltas(pollId, actorId, optionIds) {
|
|
508
|
+
const previous = this.selectionsByPoll.get(pollId)?.get(actorId);
|
|
509
|
+
if (!previous) {
|
|
510
|
+
return optionIds.map((optionId) => ({ optionId, selected: true }));
|
|
511
|
+
}
|
|
512
|
+
const current = new Set(optionIds);
|
|
513
|
+
const selected = optionIds.filter((optionId) => !previous.has(optionId)).map((optionId) => ({ optionId, selected: true }));
|
|
514
|
+
const deselected = [...previous].filter((optionId) => !current.has(optionId)).map((optionId) => ({ optionId, selected: false }));
|
|
515
|
+
return [...selected, ...deselected];
|
|
516
|
+
}
|
|
517
|
+
clearedActorSelectionDeltas(pollId, actorId) {
|
|
518
|
+
const previous = this.selectionsByPoll.get(pollId)?.get(actorId);
|
|
519
|
+
if (!previous) {
|
|
520
|
+
return [];
|
|
521
|
+
}
|
|
522
|
+
return [...previous].map((optionId) => ({ optionId, selected: false }));
|
|
523
|
+
}
|
|
524
|
+
actorSelection(pollId, actorId) {
|
|
525
|
+
const selection = this.selectionsByPoll.get(pollId)?.get(actorId);
|
|
526
|
+
return selection ? [...selection] : void 0;
|
|
527
|
+
}
|
|
528
|
+
commitActorSelection(pollId, actorId, optionIds, at) {
|
|
529
|
+
let selections = this.selectionsByPoll.get(pollId);
|
|
530
|
+
if (!selections) {
|
|
531
|
+
selections = /* @__PURE__ */ new Map();
|
|
532
|
+
this.selectionsByPoll.set(pollId, selections);
|
|
533
|
+
}
|
|
534
|
+
selections.set(actorId, new Set(optionIds));
|
|
535
|
+
if (!at) {
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
let eventTimes = this.selectionEventTimesByPoll.get(pollId);
|
|
539
|
+
if (!eventTimes) {
|
|
540
|
+
eventTimes = /* @__PURE__ */ new Map();
|
|
541
|
+
this.selectionEventTimesByPoll.set(pollId, eventTimes);
|
|
542
|
+
}
|
|
543
|
+
const eventTime = at.getTime();
|
|
544
|
+
const previousTime = eventTimes.get(actorId);
|
|
545
|
+
if (previousTime === void 0 || eventTime >= previousTime) {
|
|
546
|
+
eventTimes.set(actorId, eventTime);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
isStaleActorSelectionEvent(pollId, actorId, at) {
|
|
550
|
+
const previousTime = this.selectionEventTimesByPoll.get(pollId)?.get(actorId);
|
|
551
|
+
return previousTime !== void 0 && at.getTime() < previousTime;
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
var messageCaches = /* @__PURE__ */ new WeakMap();
|
|
555
|
+
var pollCaches = /* @__PURE__ */ new WeakMap();
|
|
556
|
+
var getMessageCache = (owner) => {
|
|
557
|
+
let cache = messageCaches.get(owner);
|
|
558
|
+
if (!cache) {
|
|
559
|
+
cache = new MessageCache();
|
|
560
|
+
messageCaches.set(owner, cache);
|
|
561
|
+
}
|
|
562
|
+
return cache;
|
|
563
|
+
};
|
|
564
|
+
var getPollCache = (owner) => {
|
|
565
|
+
let cache = pollCaches.get(owner);
|
|
566
|
+
if (!cache) {
|
|
567
|
+
cache = new PollCache();
|
|
568
|
+
pollCaches.set(owner, cache);
|
|
569
|
+
}
|
|
570
|
+
return cache;
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
// src/providers/imessage/remote/ids.ts
|
|
574
|
+
var PART_PREFIX = /^p:(\d+)\//;
|
|
575
|
+
var dmChatGuid = (address) => `any;-;${address}`;
|
|
576
|
+
var toChatGuid = (value) => value;
|
|
577
|
+
var toMessageGuid = (value) => value;
|
|
578
|
+
var formatChildId = (partIndex, parentGuid) => `p:${partIndex}/${parentGuid}`;
|
|
579
|
+
var parseChildId = (id) => {
|
|
580
|
+
const match = id.match(PART_PREFIX);
|
|
581
|
+
if (!match) {
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
584
|
+
return {
|
|
585
|
+
parentGuid: id.replace(PART_PREFIX, ""),
|
|
586
|
+
partIndex: Number(match[1])
|
|
587
|
+
};
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
// src/providers/imessage/remote/inbound.ts
|
|
591
|
+
var URL_BALLOON_BUNDLE_ID = "com.apple.messages.URLBalloonProvider";
|
|
592
|
+
var getBalloonBundleId = (message) => message.content.balloonBundleId;
|
|
593
|
+
var messageAttachments = (message) => message.content.attachments;
|
|
594
|
+
var resolveChatGuid = (message, hint) => {
|
|
595
|
+
if (hint) {
|
|
596
|
+
return hint;
|
|
597
|
+
}
|
|
598
|
+
const first = message.chatGuids?.[0];
|
|
599
|
+
return first ?? "";
|
|
600
|
+
};
|
|
601
|
+
var resolveSenderId = (message) => message.sender?.address ?? "";
|
|
602
|
+
var isIMessageMessage = (value) => {
|
|
603
|
+
if (typeof value !== "object" || value === null) {
|
|
604
|
+
return false;
|
|
605
|
+
}
|
|
606
|
+
const record = value;
|
|
607
|
+
return typeof record.id === "string" && record.id.length > 0 && typeof record.content === "object" && record.content !== null && typeof record.sender === "object" && record.sender !== null && typeof record.space === "object" && record.space !== null;
|
|
608
|
+
};
|
|
609
|
+
var asProviderGroup = (items) => groupSchema.parse({ type: "group", items });
|
|
610
|
+
var buildMessageBase = (message, chatGuidHint, timestamp, phone) => {
|
|
611
|
+
const chat = resolveChatGuid(message, chatGuidHint);
|
|
612
|
+
return {
|
|
613
|
+
sender: { id: resolveSenderId(message) },
|
|
614
|
+
space: {
|
|
615
|
+
id: chat,
|
|
616
|
+
type: chat.includes(";+;") ? "group" : "dm",
|
|
617
|
+
phone
|
|
618
|
+
},
|
|
619
|
+
timestamp
|
|
620
|
+
};
|
|
621
|
+
};
|
|
622
|
+
var toAttachmentContent2 = (client, info) => asAttachment({
|
|
623
|
+
name: info.fileName,
|
|
624
|
+
mimeType: info.mimeType,
|
|
625
|
+
size: info.totalBytes,
|
|
626
|
+
read: async () => await downloadPrimaryAttachment(client, info.guid),
|
|
627
|
+
stream: async () => downloadPrimaryAttachmentStream(client, info.guid)
|
|
628
|
+
});
|
|
629
|
+
var downloadPrimaryAttachmentStream = (client, attachmentGuid) => {
|
|
630
|
+
const frames = client.attachments.downloadStream(attachmentGuid);
|
|
631
|
+
const iterator = frames[Symbol.asyncIterator]();
|
|
632
|
+
let closed = false;
|
|
633
|
+
const closeFrames = async () => {
|
|
634
|
+
if (closed) {
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
closed = true;
|
|
638
|
+
try {
|
|
639
|
+
await iterator.return?.();
|
|
640
|
+
} finally {
|
|
641
|
+
await frames.close();
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
return new ReadableStream({
|
|
645
|
+
async cancel() {
|
|
646
|
+
await closeFrames();
|
|
647
|
+
},
|
|
648
|
+
async pull(controller) {
|
|
649
|
+
try {
|
|
650
|
+
while (true) {
|
|
651
|
+
const result = await iterator.next();
|
|
652
|
+
if (result.done) {
|
|
653
|
+
controller.close();
|
|
654
|
+
await closeFrames();
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
if (result.value.type === "primaryChunk") {
|
|
658
|
+
controller.enqueue(result.value.data);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
} catch (error) {
|
|
663
|
+
await closeFrames();
|
|
664
|
+
throw error;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
};
|
|
669
|
+
var downloadPrimaryAttachment = async (client, attachmentGuid) => {
|
|
670
|
+
const chunks = [];
|
|
671
|
+
const frames = client.attachments.downloadStream(attachmentGuid);
|
|
672
|
+
try {
|
|
673
|
+
for await (const frame of frames) {
|
|
674
|
+
if (frame.type === "primaryChunk") {
|
|
675
|
+
chunks.push(Buffer.from(frame.data));
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
} finally {
|
|
679
|
+
await frames.close();
|
|
680
|
+
}
|
|
681
|
+
return Buffer.concat(chunks);
|
|
682
|
+
};
|
|
683
|
+
var toVCardContent2 = async (client, info) => {
|
|
684
|
+
try {
|
|
685
|
+
const buf = await downloadPrimaryAttachment(client, info.guid);
|
|
686
|
+
return asContact(fromVCard(buf.toString("utf8")));
|
|
687
|
+
} catch (err) {
|
|
688
|
+
console.warn(
|
|
689
|
+
"[spectrum-ts][imessage] failed to parse vCard attachment; falling back to attachment content",
|
|
690
|
+
{ error: err, guid: info.guid }
|
|
691
|
+
);
|
|
692
|
+
return toAttachmentContent2(client, info);
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
var attachmentContent = async (client, info) => isVCardAttachment(info.mimeType, info.fileName) ? await toVCardContent2(client, info) : toAttachmentContent2(client, info);
|
|
696
|
+
var buildAttachmentMessage = async (client, base, info, id, partIndex, parentId) => {
|
|
697
|
+
const content = await attachmentContent(client, info);
|
|
698
|
+
const msg = { ...base, id, content, partIndex };
|
|
699
|
+
if (parentId !== void 0) {
|
|
700
|
+
msg.parentId = parentId;
|
|
701
|
+
}
|
|
702
|
+
return msg;
|
|
703
|
+
};
|
|
704
|
+
var toRichlinkMessage = (message, base, id) => {
|
|
705
|
+
const url = message.content.text ?? "";
|
|
706
|
+
try {
|
|
707
|
+
return { ...base, id, content: asRichlink({ url }) };
|
|
708
|
+
} catch (err) {
|
|
709
|
+
console.warn(
|
|
710
|
+
"[spectrum-ts][imessage] failed to convert message to rich link; falling back to text/custom content",
|
|
711
|
+
{ error: err, message, url }
|
|
712
|
+
);
|
|
713
|
+
return {
|
|
714
|
+
...base,
|
|
715
|
+
id,
|
|
716
|
+
content: url ? asText(url) : asCustom(message)
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
var rebuildFromAppleMessage = async (client, message, phone, chatGuidHint) => {
|
|
721
|
+
const messageGuidStr = message.guid;
|
|
722
|
+
const timestamp = message.dateCreated ?? /* @__PURE__ */ new Date();
|
|
723
|
+
const base = buildMessageBase(message, chatGuidHint, timestamp, phone);
|
|
724
|
+
const attachments = messageAttachments(message);
|
|
725
|
+
if (attachments.length === 1) {
|
|
726
|
+
const info = attachments[0];
|
|
727
|
+
if (!info) {
|
|
728
|
+
throw new Error("Unreachable: attachments.length === 1 but no element");
|
|
729
|
+
}
|
|
730
|
+
return buildAttachmentMessage(client, base, info, messageGuidStr, 0);
|
|
731
|
+
}
|
|
732
|
+
if (attachments.length > 1) {
|
|
733
|
+
const items = [];
|
|
734
|
+
for (let i = 0; i < attachments.length; i++) {
|
|
735
|
+
const info = attachments[i];
|
|
736
|
+
if (!info) {
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
items.push(
|
|
740
|
+
await buildAttachmentMessage(
|
|
741
|
+
client,
|
|
742
|
+
base,
|
|
743
|
+
info,
|
|
744
|
+
formatChildId(i, messageGuidStr),
|
|
745
|
+
i,
|
|
746
|
+
messageGuidStr
|
|
747
|
+
)
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
return {
|
|
751
|
+
...base,
|
|
752
|
+
id: messageGuidStr,
|
|
753
|
+
content: asProviderGroup(items)
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
if (getBalloonBundleId(message) === URL_BALLOON_BUNDLE_ID) {
|
|
757
|
+
return toRichlinkMessage(message, base, messageGuidStr);
|
|
758
|
+
}
|
|
759
|
+
const text2 = message.content.text;
|
|
760
|
+
return {
|
|
761
|
+
...base,
|
|
762
|
+
id: messageGuidStr,
|
|
763
|
+
content: text2 ? asText(text2) : asCustom(message)
|
|
764
|
+
};
|
|
765
|
+
};
|
|
766
|
+
var cacheMessage = (cache, message) => {
|
|
767
|
+
cache.set(message.id, message);
|
|
768
|
+
if (message.content.type === "group") {
|
|
769
|
+
for (const item of message.content.items) {
|
|
770
|
+
if (isIMessageMessage(item)) {
|
|
771
|
+
cache.set(item.id, item);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
};
|
|
776
|
+
var toInboundMessages = async (client, cache, event, phone) => {
|
|
777
|
+
const base = buildMessageBase(
|
|
778
|
+
event.message,
|
|
779
|
+
event.chatGuid,
|
|
780
|
+
event.occurredAt,
|
|
781
|
+
phone
|
|
782
|
+
);
|
|
783
|
+
const messageGuidStr = event.message.guid;
|
|
784
|
+
if (getBalloonBundleId(event.message) === URL_BALLOON_BUNDLE_ID) {
|
|
785
|
+
const msg2 = toRichlinkMessage(event.message, base, messageGuidStr);
|
|
786
|
+
cacheMessage(cache, msg2);
|
|
787
|
+
return [msg2];
|
|
788
|
+
}
|
|
789
|
+
const attachments = messageAttachments(event.message);
|
|
790
|
+
if (attachments.length === 1) {
|
|
791
|
+
const info = attachments[0];
|
|
792
|
+
if (!info) {
|
|
793
|
+
throw new Error("Unreachable: attachments.length === 1 but no element");
|
|
794
|
+
}
|
|
795
|
+
const msg2 = await buildAttachmentMessage(
|
|
796
|
+
client,
|
|
797
|
+
base,
|
|
798
|
+
info,
|
|
799
|
+
messageGuidStr,
|
|
800
|
+
0
|
|
801
|
+
);
|
|
802
|
+
cacheMessage(cache, msg2);
|
|
803
|
+
return [msg2];
|
|
804
|
+
}
|
|
805
|
+
if (attachments.length > 1) {
|
|
806
|
+
const items = [];
|
|
807
|
+
for (let i = 0; i < attachments.length; i++) {
|
|
808
|
+
const info = attachments[i];
|
|
809
|
+
if (!info) {
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
items.push(
|
|
813
|
+
await buildAttachmentMessage(
|
|
814
|
+
client,
|
|
815
|
+
base,
|
|
816
|
+
info,
|
|
817
|
+
formatChildId(i, messageGuidStr),
|
|
818
|
+
i,
|
|
819
|
+
messageGuidStr
|
|
820
|
+
)
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
const parent = {
|
|
824
|
+
...base,
|
|
825
|
+
id: messageGuidStr,
|
|
826
|
+
content: asProviderGroup(items)
|
|
827
|
+
};
|
|
828
|
+
cacheMessage(cache, parent);
|
|
829
|
+
return [parent];
|
|
830
|
+
}
|
|
831
|
+
const text2 = event.message.content.text;
|
|
832
|
+
const msg = {
|
|
833
|
+
...base,
|
|
834
|
+
id: messageGuidStr,
|
|
835
|
+
content: text2 ? asText(text2) : asCustom(event.message)
|
|
836
|
+
};
|
|
837
|
+
cacheMessage(cache, msg);
|
|
838
|
+
return [msg];
|
|
839
|
+
};
|
|
840
|
+
var getMessage3 = async (remote, spaceId, msgId, phone) => {
|
|
841
|
+
const cache = getMessageCache(remote);
|
|
842
|
+
const cached = cache.get(msgId);
|
|
843
|
+
if (cached) {
|
|
844
|
+
return cached;
|
|
845
|
+
}
|
|
846
|
+
const childRef = parseChildId(msgId);
|
|
847
|
+
if (childRef) {
|
|
848
|
+
try {
|
|
849
|
+
const fetched = await remote.messages.get(
|
|
850
|
+
toChatGuid(spaceId),
|
|
851
|
+
toMessageGuid(childRef.parentGuid)
|
|
852
|
+
);
|
|
853
|
+
const parent = await rebuildFromAppleMessage(
|
|
854
|
+
remote,
|
|
855
|
+
fetched,
|
|
856
|
+
phone,
|
|
857
|
+
spaceId
|
|
858
|
+
);
|
|
859
|
+
cacheMessage(cache, parent);
|
|
860
|
+
if (parent.content.type !== "group") {
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
const item = parent.content.items[childRef.partIndex];
|
|
864
|
+
return isIMessageMessage(item) ? item : void 0;
|
|
865
|
+
} catch (err) {
|
|
866
|
+
if (err instanceof NotFoundError) {
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
throw err;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
try {
|
|
873
|
+
const fetched = await remote.messages.get(
|
|
874
|
+
toChatGuid(spaceId),
|
|
875
|
+
toMessageGuid(msgId)
|
|
876
|
+
);
|
|
877
|
+
const rebuilt = await rebuildFromAppleMessage(
|
|
878
|
+
remote,
|
|
879
|
+
fetched,
|
|
880
|
+
phone,
|
|
881
|
+
spaceId
|
|
882
|
+
);
|
|
883
|
+
cacheMessage(cache, rebuilt);
|
|
884
|
+
return rebuilt;
|
|
885
|
+
} catch (err) {
|
|
886
|
+
if (err instanceof NotFoundError) {
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
throw err;
|
|
890
|
+
}
|
|
891
|
+
};
|
|
892
|
+
|
|
893
|
+
// src/providers/imessage/remote/reactions.ts
|
|
894
|
+
var EMOJI_TO_TAPBACK = {
|
|
895
|
+
"\u2764\uFE0F": "love",
|
|
896
|
+
"\u{1F44D}": "like",
|
|
897
|
+
"\u{1F44E}": "dislike",
|
|
898
|
+
"\u{1F602}": "laugh",
|
|
899
|
+
"\u203C\uFE0F": "emphasize",
|
|
900
|
+
"\u2753": "question"
|
|
901
|
+
};
|
|
902
|
+
var TAPBACK_TO_EMOJI = Object.fromEntries(
|
|
903
|
+
Object.entries(EMOJI_TO_TAPBACK).map(([emoji, kind]) => [kind, emoji])
|
|
904
|
+
);
|
|
905
|
+
var reactionEmoji = (reaction) => reaction.kind === "emoji" ? reaction.emoji : TAPBACK_TO_EMOJI[reaction.kind];
|
|
906
|
+
var asProviderReaction = (emoji, target) => reactionSchema.parse({
|
|
907
|
+
emoji,
|
|
908
|
+
target,
|
|
909
|
+
type: "reaction"
|
|
910
|
+
});
|
|
911
|
+
var resolveReactionTarget = async (client, cache, chat, targetGuid, partIndex, phone) => {
|
|
912
|
+
let candidate = cache.get(targetGuid);
|
|
913
|
+
if (!candidate) {
|
|
914
|
+
try {
|
|
915
|
+
const fetched = await client.messages.get(
|
|
916
|
+
toChatGuid(chat),
|
|
917
|
+
toMessageGuid(targetGuid)
|
|
918
|
+
);
|
|
919
|
+
candidate = await rebuildFromAppleMessage(client, fetched, phone, chat);
|
|
920
|
+
cacheMessage(cache, candidate);
|
|
921
|
+
} catch {
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
if (candidate.content.type === "group") {
|
|
926
|
+
const items = candidate.content.items;
|
|
927
|
+
if (!Array.isArray(items)) {
|
|
928
|
+
return candidate;
|
|
929
|
+
}
|
|
930
|
+
const item = items[partIndex ?? 0];
|
|
931
|
+
return isIMessageMessage(item) ? item : candidate;
|
|
932
|
+
}
|
|
933
|
+
return candidate;
|
|
934
|
+
};
|
|
935
|
+
var toReactionMessages = async (client, cache, event, phone) => {
|
|
936
|
+
const emoji = reactionEmoji(event.reaction);
|
|
937
|
+
if (!emoji) {
|
|
938
|
+
return [];
|
|
939
|
+
}
|
|
940
|
+
const senderAddress = event.actor?.address;
|
|
941
|
+
if (!senderAddress) {
|
|
942
|
+
return [];
|
|
943
|
+
}
|
|
944
|
+
const resolved = await resolveReactionTarget(
|
|
945
|
+
client,
|
|
946
|
+
cache,
|
|
947
|
+
event.chatGuid,
|
|
948
|
+
event.messageGuid,
|
|
949
|
+
event.targetPartIndex,
|
|
950
|
+
phone
|
|
951
|
+
);
|
|
952
|
+
if (!resolved) {
|
|
953
|
+
return [];
|
|
954
|
+
}
|
|
955
|
+
const partSuffix = typeof event.targetPartIndex === "number" ? `:${event.targetPartIndex}` : "";
|
|
956
|
+
return [
|
|
957
|
+
{
|
|
958
|
+
sender: { id: senderAddress },
|
|
959
|
+
space: {
|
|
960
|
+
id: event.chatGuid,
|
|
961
|
+
type: event.chatGuid.includes(";+;") ? "group" : "dm",
|
|
962
|
+
phone
|
|
963
|
+
},
|
|
964
|
+
timestamp: event.occurredAt,
|
|
965
|
+
id: `${event.messageGuid}:reaction:${event.sequence}${partSuffix}`,
|
|
966
|
+
content: asProviderReaction(emoji, resolved)
|
|
967
|
+
}
|
|
968
|
+
];
|
|
969
|
+
};
|
|
970
|
+
var reactToMessage = async (remote, spaceId, target, reaction) => {
|
|
971
|
+
const chat = toChatGuid(spaceId);
|
|
972
|
+
const parentGuid = target.parentId ?? target.id;
|
|
973
|
+
const guid = toMessageGuid(parentGuid);
|
|
974
|
+
const opts = typeof target.partIndex === "number" ? { partIndex: target.partIndex } : void 0;
|
|
975
|
+
const native = EMOJI_TO_TAPBACK[reaction];
|
|
976
|
+
if (native) {
|
|
977
|
+
await remote.messages.setReaction(chat, guid, { kind: native }, true, opts);
|
|
978
|
+
} else {
|
|
979
|
+
await remote.messages.setReaction(
|
|
980
|
+
chat,
|
|
981
|
+
guid,
|
|
982
|
+
{ kind: "emoji", emoji: reaction },
|
|
983
|
+
true,
|
|
984
|
+
opts
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
};
|
|
988
|
+
|
|
989
|
+
// src/utils/audio.ts
|
|
990
|
+
import { spawn } from "child_process";
|
|
991
|
+
import { mkdtemp as mkdtemp2, readFile as readFile2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
992
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
993
|
+
import { join as join2 } from "path";
|
|
994
|
+
var M4A_BRANDS = /* @__PURE__ */ new Set([
|
|
995
|
+
"M4A ",
|
|
996
|
+
"M4B ",
|
|
997
|
+
"M4P ",
|
|
998
|
+
"mp42",
|
|
999
|
+
"mp41",
|
|
1000
|
+
"isom",
|
|
1001
|
+
"iso2"
|
|
1002
|
+
]);
|
|
1003
|
+
var M4A_MIME_TYPES = /* @__PURE__ */ new Set([
|
|
1004
|
+
"audio/mp4",
|
|
1005
|
+
"audio/mp4a-latm",
|
|
1006
|
+
"audio/x-m4a",
|
|
1007
|
+
"audio/aac",
|
|
1008
|
+
"audio/aacp"
|
|
1009
|
+
]);
|
|
1010
|
+
var FFMPEG_MISSING_MESSAGE = "voice content: input is not m4a/aac and ffmpeg is unavailable. Install `ffmpeg-static` or ensure `ffmpeg` is on PATH.";
|
|
1011
|
+
var isM4a = (buffer) => {
|
|
1012
|
+
if (buffer.length < 12) {
|
|
1013
|
+
return false;
|
|
1014
|
+
}
|
|
1015
|
+
if (buffer.toString("ascii", 4, 8) !== "ftyp") {
|
|
1016
|
+
return false;
|
|
1017
|
+
}
|
|
1018
|
+
return M4A_BRANDS.has(buffer.toString("ascii", 8, 12));
|
|
1019
|
+
};
|
|
1020
|
+
var isM4aMimeType = (mimeType) => M4A_MIME_TYPES.has(mimeType.toLowerCase());
|
|
1021
|
+
var cachedFfmpegPath;
|
|
1022
|
+
var tryStaticBinary = async () => {
|
|
1023
|
+
try {
|
|
1024
|
+
const mod = await import("ffmpeg-static");
|
|
1025
|
+
return mod.default ?? void 0;
|
|
1026
|
+
} catch {
|
|
1027
|
+
return void 0;
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1030
|
+
var resolveFfmpegPath = async () => {
|
|
1031
|
+
if (cachedFfmpegPath) {
|
|
1032
|
+
return cachedFfmpegPath;
|
|
1033
|
+
}
|
|
1034
|
+
cachedFfmpegPath = await tryStaticBinary() ?? "ffmpeg";
|
|
1035
|
+
return cachedFfmpegPath;
|
|
1036
|
+
};
|
|
1037
|
+
var collectStream = (stream2) => {
|
|
1038
|
+
if (!stream2) {
|
|
1039
|
+
return Promise.resolve("");
|
|
1040
|
+
}
|
|
1041
|
+
return new Promise((resolve, reject) => {
|
|
1042
|
+
const chunks = [];
|
|
1043
|
+
stream2.on("data", (chunk) => {
|
|
1044
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
1045
|
+
});
|
|
1046
|
+
stream2.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
1047
|
+
stream2.on("error", reject);
|
|
1048
|
+
});
|
|
1049
|
+
};
|
|
1050
|
+
var isMissingBinaryError = (err) => err?.code === "ENOENT";
|
|
1051
|
+
var runFfmpeg = (ffmpegPath, args) => {
|
|
1052
|
+
const proc = spawn(ffmpegPath, args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
1053
|
+
const stderr = collectStream(proc.stderr);
|
|
1054
|
+
const exit = new Promise((resolve, reject) => {
|
|
1055
|
+
proc.on(
|
|
1056
|
+
"error",
|
|
1057
|
+
(err) => reject(
|
|
1058
|
+
isMissingBinaryError(err) ? new Error(FFMPEG_MISSING_MESSAGE) : err
|
|
1059
|
+
)
|
|
1060
|
+
);
|
|
1061
|
+
proc.on("exit", (code) => resolve(code ?? -1));
|
|
1062
|
+
});
|
|
1063
|
+
return Promise.all([exit, stderr]).then(([code, text2]) => ({
|
|
1064
|
+
code,
|
|
1065
|
+
stderr: text2
|
|
1066
|
+
}));
|
|
1067
|
+
};
|
|
1068
|
+
var DURATION_PATTERN = /Duration:\s*(\d+):(\d{2}):(\d{2})(?:\.(\d{1,3}))?/;
|
|
1069
|
+
var parseDuration = (stderr) => {
|
|
1070
|
+
const match = stderr.match(DURATION_PATTERN);
|
|
1071
|
+
if (!match) {
|
|
1072
|
+
return void 0;
|
|
1073
|
+
}
|
|
1074
|
+
const [, hh, mm, ss, frac] = match;
|
|
1075
|
+
const seconds = Number(hh) * 3600 + Number(mm) * 60 + Number(ss) + Number(`0.${frac ?? 0}`);
|
|
1076
|
+
return Number.isFinite(seconds) ? seconds : void 0;
|
|
1077
|
+
};
|
|
1078
|
+
var transcodeToM4a = async (buffer) => {
|
|
1079
|
+
const ffmpeg = await resolveFfmpegPath();
|
|
1080
|
+
const dir = await mkdtemp2(join2(tmpdir2(), "spectrum-voice-"));
|
|
1081
|
+
const inPath = join2(dir, "in");
|
|
1082
|
+
const outPath = join2(dir, "out.m4a");
|
|
1083
|
+
try {
|
|
1084
|
+
await writeFile2(inPath, buffer);
|
|
1085
|
+
const { code, stderr } = await runFfmpeg(ffmpeg, [
|
|
1086
|
+
"-y",
|
|
1087
|
+
"-i",
|
|
1088
|
+
inPath,
|
|
1089
|
+
"-f",
|
|
1090
|
+
"ipod",
|
|
1091
|
+
"-c:a",
|
|
1092
|
+
"aac",
|
|
1093
|
+
outPath
|
|
1094
|
+
]);
|
|
1095
|
+
if (code !== 0) {
|
|
1096
|
+
throw new Error(`ffmpeg conversion failed (exit ${code}): ${stderr}`);
|
|
1097
|
+
}
|
|
1098
|
+
const out = await readFile2(outPath);
|
|
1099
|
+
return { buffer: out, duration: parseDuration(stderr) };
|
|
1100
|
+
} finally {
|
|
1101
|
+
await rm2(dir, { recursive: true, force: true }).catch(() => {
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
};
|
|
1105
|
+
var ensureM4a = async (buffer, mimeType) => {
|
|
1106
|
+
if (isM4aMimeType(mimeType) || isM4a(buffer)) {
|
|
1107
|
+
return { buffer };
|
|
1108
|
+
}
|
|
1109
|
+
return transcodeToM4a(buffer);
|
|
1110
|
+
};
|
|
1111
|
+
|
|
1112
|
+
// src/providers/imessage/remote/send.ts
|
|
1113
|
+
var GROUP_ITEM_ALLOWED = /* @__PURE__ */ new Set([
|
|
1114
|
+
"text",
|
|
1115
|
+
"attachment",
|
|
1116
|
+
"contact",
|
|
1117
|
+
"voice"
|
|
1118
|
+
]);
|
|
1119
|
+
var MAX_GROUP_TEXT_ITEMS = 1;
|
|
1120
|
+
var outboundRecord = (spaceId, id, content, timestamp, extras) => ({
|
|
1121
|
+
id,
|
|
1122
|
+
content,
|
|
1123
|
+
space: { id: spaceId },
|
|
1124
|
+
timestamp,
|
|
1125
|
+
...extras
|
|
1126
|
+
});
|
|
1127
|
+
var outboundGroupItem = (spaceId, id, content, timestamp, partIndex, parentId) => outboundRecord(spaceId, id, content, timestamp, {
|
|
1128
|
+
partIndex,
|
|
1129
|
+
parentId
|
|
1130
|
+
});
|
|
1131
|
+
var providerGroup = (items) => asGroup({ items });
|
|
1132
|
+
var withReply = (options, replyTo) => replyTo ? { ...options, replyTo } : options;
|
|
1133
|
+
var replyOptions = (replyTo) => replyTo ? { replyTo } : void 0;
|
|
1134
|
+
var effectOption = (effect2) => effect2 ? { effect: effect2 } : {};
|
|
1135
|
+
var replyTargetFromId = (messageId) => {
|
|
1136
|
+
const childRef = parseChildId(messageId);
|
|
1137
|
+
if (childRef) {
|
|
1138
|
+
return {
|
|
1139
|
+
guid: toMessageGuid(childRef.parentGuid),
|
|
1140
|
+
partIndex: childRef.partIndex
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
return toMessageGuid(messageId);
|
|
1144
|
+
};
|
|
1145
|
+
var outboundMessage = (spaceId, message, content) => outboundRecord(spaceId, message.guid, content, message.dateCreated);
|
|
1146
|
+
var outboundPoll = (spaceId, poll, content) => outboundRecord(spaceId, poll.pollMessageGuid, content, /* @__PURE__ */ new Date());
|
|
1147
|
+
var sendVCardAttachment = (remote, name, vcf) => remote.attachments.upload({
|
|
1148
|
+
data: Buffer.from(vcf, "utf8"),
|
|
1149
|
+
fileName: name
|
|
1150
|
+
});
|
|
1151
|
+
var sendContactAttachment = async (remote, content) => {
|
|
1152
|
+
const vcf = await toVCard(content);
|
|
1153
|
+
const name = vcardFileName(content);
|
|
1154
|
+
const upload = await sendVCardAttachment(remote, name, vcf);
|
|
1155
|
+
return { guid: upload.attachment.guid, name };
|
|
1156
|
+
};
|
|
1157
|
+
var uploadAttachment = async (remote, content) => {
|
|
1158
|
+
const attachment = await remote.attachments.upload({
|
|
1159
|
+
data: await content.read(),
|
|
1160
|
+
fileName: content.name
|
|
1161
|
+
});
|
|
1162
|
+
return { guid: attachment.attachment.guid, name: content.name };
|
|
1163
|
+
};
|
|
1164
|
+
var uploadVoice = async (remote, content) => {
|
|
1165
|
+
const { buffer } = await ensureM4a(await content.read(), content.mimeType);
|
|
1166
|
+
const name = content.name ?? "voice.m4a";
|
|
1167
|
+
const attachment = await remote.attachments.upload({
|
|
1168
|
+
data: buffer,
|
|
1169
|
+
fileName: name
|
|
1170
|
+
});
|
|
1171
|
+
return { guid: attachment.attachment.guid, name };
|
|
1172
|
+
};
|
|
1173
|
+
var sendContent = async (remote, spaceId, chat, content, replyTo, effect2) => {
|
|
1174
|
+
switch (content.type) {
|
|
1175
|
+
case "effect":
|
|
1176
|
+
return sendContent(
|
|
1177
|
+
remote,
|
|
1178
|
+
spaceId,
|
|
1179
|
+
chat,
|
|
1180
|
+
content.content,
|
|
1181
|
+
replyTo,
|
|
1182
|
+
content.effect
|
|
1183
|
+
);
|
|
1184
|
+
case "text": {
|
|
1185
|
+
const message = await remote.messages.sendText(
|
|
1186
|
+
chat,
|
|
1187
|
+
content.text,
|
|
1188
|
+
withReply(effectOption(effect2), replyTo)
|
|
1189
|
+
);
|
|
1190
|
+
return outboundMessage(spaceId, message, content);
|
|
1191
|
+
}
|
|
1192
|
+
case "richlink": {
|
|
1193
|
+
const message = await remote.messages.sendText(
|
|
1194
|
+
chat,
|
|
1195
|
+
content.url,
|
|
1196
|
+
withReply({ enableLinkPreview: true }, replyTo)
|
|
1197
|
+
);
|
|
1198
|
+
return outboundMessage(spaceId, message, content);
|
|
1199
|
+
}
|
|
1200
|
+
case "attachment": {
|
|
1201
|
+
const { guid } = await uploadAttachment(remote, content);
|
|
1202
|
+
const message = await remote.messages.sendAttachment(
|
|
1203
|
+
chat,
|
|
1204
|
+
guid,
|
|
1205
|
+
withReply(effectOption(effect2), replyTo)
|
|
1206
|
+
);
|
|
1207
|
+
return outboundMessage(spaceId, message, content);
|
|
1208
|
+
}
|
|
1209
|
+
case "contact": {
|
|
1210
|
+
const { guid } = await sendContactAttachment(remote, content);
|
|
1211
|
+
const message = await remote.messages.sendAttachment(
|
|
1212
|
+
chat,
|
|
1213
|
+
guid,
|
|
1214
|
+
replyOptions(replyTo)
|
|
1215
|
+
);
|
|
1216
|
+
return outboundMessage(spaceId, message, content);
|
|
1217
|
+
}
|
|
1218
|
+
case "voice": {
|
|
1219
|
+
const { guid } = await uploadVoice(remote, content);
|
|
1220
|
+
const message = await remote.messages.sendAttachment(chat, guid, {
|
|
1221
|
+
isAudioMessage: true,
|
|
1222
|
+
...replyOptions(replyTo)
|
|
1223
|
+
});
|
|
1224
|
+
return outboundMessage(spaceId, message, content);
|
|
1225
|
+
}
|
|
1226
|
+
case "poll":
|
|
1227
|
+
if (replyTo) {
|
|
1228
|
+
throw unsupportedRemoteContent(
|
|
1229
|
+
"poll",
|
|
1230
|
+
"polls cannot be sent as replies"
|
|
1231
|
+
);
|
|
1232
|
+
}
|
|
1233
|
+
return outboundPoll(
|
|
1234
|
+
spaceId,
|
|
1235
|
+
await remote.polls.create(
|
|
1236
|
+
chat,
|
|
1237
|
+
content.title,
|
|
1238
|
+
content.options.map((option) => option.title)
|
|
1239
|
+
),
|
|
1240
|
+
content
|
|
1241
|
+
);
|
|
1242
|
+
default:
|
|
1243
|
+
throw unsupportedRemoteContent(content.type);
|
|
1244
|
+
}
|
|
1245
|
+
};
|
|
1246
|
+
var validateGroupContent = (content) => {
|
|
1247
|
+
let textCount = 0;
|
|
1248
|
+
for (const sub of content.items) {
|
|
1249
|
+
const itemType = sub.content.type;
|
|
1250
|
+
if (!GROUP_ITEM_ALLOWED.has(itemType)) {
|
|
1251
|
+
throw unsupportedRemoteContent(
|
|
1252
|
+
"group",
|
|
1253
|
+
`"${itemType}" items are not supported inside a group`
|
|
1254
|
+
);
|
|
1255
|
+
}
|
|
1256
|
+
if (itemType === "text" && ++textCount > MAX_GROUP_TEXT_ITEMS) {
|
|
1257
|
+
throw unsupportedRemoteContent(
|
|
1258
|
+
"group",
|
|
1259
|
+
`groups can contain at most ${MAX_GROUP_TEXT_ITEMS} text item`
|
|
1260
|
+
);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
};
|
|
1264
|
+
var resolvePart = async (remote, content) => {
|
|
1265
|
+
switch (content.type) {
|
|
1266
|
+
case "text":
|
|
1267
|
+
return { text: content.text };
|
|
1268
|
+
case "attachment": {
|
|
1269
|
+
const { guid, name } = await uploadAttachment(remote, content);
|
|
1270
|
+
return { attachmentGuid: guid, attachmentName: name };
|
|
1271
|
+
}
|
|
1272
|
+
case "contact": {
|
|
1273
|
+
const { guid, name } = await sendContactAttachment(remote, content);
|
|
1274
|
+
return { attachmentGuid: guid, attachmentName: name };
|
|
1275
|
+
}
|
|
1276
|
+
case "voice": {
|
|
1277
|
+
const { guid, name } = await uploadVoice(remote, content);
|
|
1278
|
+
return { attachmentGuid: guid, attachmentName: name };
|
|
1279
|
+
}
|
|
1280
|
+
default:
|
|
1281
|
+
throw unsupportedRemoteContent(content.type);
|
|
1282
|
+
}
|
|
1283
|
+
};
|
|
1284
|
+
var send3 = async (remote, spaceId, content) => {
|
|
1285
|
+
const chat = toChatGuid(spaceId);
|
|
1286
|
+
if (content.type === "group") {
|
|
1287
|
+
validateGroupContent(content);
|
|
1288
|
+
const resolved = await Promise.all(
|
|
1289
|
+
content.items.map((sub) => resolvePart(remote, sub.content))
|
|
1290
|
+
);
|
|
1291
|
+
const message = await remote.messages.sendMultipart(
|
|
1292
|
+
chat,
|
|
1293
|
+
resolved.map((part, idx) => ({ ...part, bubbleIndex: idx }))
|
|
1294
|
+
);
|
|
1295
|
+
const parentGuid = message.guid;
|
|
1296
|
+
const timestamp = message.dateCreated;
|
|
1297
|
+
const items = content.items.map(
|
|
1298
|
+
(sub, idx) => outboundGroupItem(
|
|
1299
|
+
spaceId,
|
|
1300
|
+
formatChildId(idx, parentGuid),
|
|
1301
|
+
sub.content,
|
|
1302
|
+
timestamp,
|
|
1303
|
+
idx,
|
|
1304
|
+
parentGuid
|
|
1305
|
+
)
|
|
1306
|
+
);
|
|
1307
|
+
return outboundRecord(spaceId, parentGuid, providerGroup(items), timestamp);
|
|
1308
|
+
}
|
|
1309
|
+
return sendContent(remote, spaceId, chat, content);
|
|
1310
|
+
};
|
|
1311
|
+
var replyToMessage = async (remote, spaceId, msgId, content) => {
|
|
1312
|
+
const chat = toChatGuid(spaceId);
|
|
1313
|
+
return sendContent(remote, spaceId, chat, content, replyTargetFromId(msgId));
|
|
1314
|
+
};
|
|
1315
|
+
var editMessage = async (remote, spaceId, msgId, content) => {
|
|
1316
|
+
if (content.type !== "text") {
|
|
1317
|
+
throw unsupportedRemoteContent(
|
|
1318
|
+
content.type,
|
|
1319
|
+
"only text content can be edited"
|
|
1320
|
+
);
|
|
1321
|
+
}
|
|
1322
|
+
const childRef = parseChildId(msgId);
|
|
1323
|
+
await remote.messages.edit(
|
|
1324
|
+
toChatGuid(spaceId),
|
|
1325
|
+
toMessageGuid(childRef?.parentGuid ?? msgId),
|
|
1326
|
+
content.text,
|
|
1327
|
+
childRef ? { partIndex: childRef.partIndex } : void 0
|
|
1328
|
+
);
|
|
1329
|
+
};
|
|
1330
|
+
|
|
1331
|
+
// src/providers/imessage/remote/stream.ts
|
|
1332
|
+
import {
|
|
1333
|
+
AuthenticationError,
|
|
1334
|
+
IMessageError,
|
|
1335
|
+
NotFoundError as NotFoundError2,
|
|
1336
|
+
ValidationError
|
|
1337
|
+
} from "@photon-ai/advanced-imessage";
|
|
1338
|
+
|
|
1339
|
+
// src/utils/resumable-stream.ts
|
|
1340
|
+
var CATCH_UP_PAGE_SIZE = 100;
|
|
1341
|
+
var MAX_BUFFERED_LIVE_EVENTS = 1e3;
|
|
1342
|
+
var RECONNECT_INITIAL_DELAY_MS = 500;
|
|
1343
|
+
var RECONNECT_MAX_DELAY_MS = 3e4;
|
|
1344
|
+
var RetryableStreamError = class extends Error {
|
|
1345
|
+
constructor(message) {
|
|
1346
|
+
super(message);
|
|
1347
|
+
this.name = "RetryableStreamError";
|
|
1348
|
+
}
|
|
1349
|
+
};
|
|
1350
|
+
var LiveBufferOverflowError = class extends RetryableStreamError {
|
|
1351
|
+
constructor(limit) {
|
|
1352
|
+
super(`Live stream buffer exceeded ${limit} events during catch-up`);
|
|
1353
|
+
this.name = "LiveBufferOverflowError";
|
|
1354
|
+
}
|
|
1355
|
+
};
|
|
1356
|
+
var closeIterable = async (iterable) => {
|
|
1357
|
+
if (!iterable) {
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1360
|
+
await iterable.close?.();
|
|
1361
|
+
};
|
|
1362
|
+
var ignoreCleanupError = () => void 0;
|
|
1363
|
+
var jitterDelay = (delayMs) => Math.random() * delayMs;
|
|
1364
|
+
var numericCursor = (cursor) => {
|
|
1365
|
+
if (!cursor) {
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
const value = Number(cursor);
|
|
1369
|
+
return Number.isSafeInteger(value) && value >= 0 ? value : void 0;
|
|
1370
|
+
};
|
|
1371
|
+
var isCursorRegression = (next, current) => {
|
|
1372
|
+
const nextValue = numericCursor(next);
|
|
1373
|
+
const currentValue = numericCursor(current);
|
|
1374
|
+
return nextValue !== void 0 && currentValue !== void 0 && nextValue < currentValue;
|
|
1375
|
+
};
|
|
1376
|
+
var resumableOrderedStream = (options) => stream((emit, end) => {
|
|
1377
|
+
const catchUpPageSize = options.catchUpPageSize ?? CATCH_UP_PAGE_SIZE;
|
|
1378
|
+
const bufferLimit = options.bufferLimit ?? MAX_BUFFERED_LIVE_EVENTS;
|
|
1379
|
+
const initialRetryDelayMs = options.initialRetryDelayMs ?? RECONNECT_INITIAL_DELAY_MS;
|
|
1380
|
+
const maxRetryDelayMs = options.maxRetryDelayMs ?? RECONNECT_MAX_DELAY_MS;
|
|
1381
|
+
let activeLive;
|
|
1382
|
+
let closed = false;
|
|
1383
|
+
let lastCursor;
|
|
1384
|
+
let retryDelayMs = initialRetryDelayMs;
|
|
1385
|
+
let sleepTimer;
|
|
1386
|
+
let wakeSleep;
|
|
1387
|
+
const deliveredSinceCursor = /* @__PURE__ */ new Set();
|
|
1388
|
+
const resetRetryDelay = () => {
|
|
1389
|
+
retryDelayMs = initialRetryDelayMs;
|
|
1390
|
+
};
|
|
1391
|
+
const advanceCursor = (cursor, clearDelivered) => {
|
|
1392
|
+
if (!cursor || cursor === lastCursor || isCursorRegression(cursor, lastCursor)) {
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1395
|
+
lastCursor = cursor;
|
|
1396
|
+
if (clearDelivered) {
|
|
1397
|
+
deliveredSinceCursor.clear();
|
|
1398
|
+
}
|
|
1399
|
+
};
|
|
1400
|
+
const deliverItem = async (item, resetRetry, clearOnCursorAdvance) => {
|
|
1401
|
+
const alreadyDelivered = deliveredSinceCursor.has(item.id);
|
|
1402
|
+
if (!alreadyDelivered) {
|
|
1403
|
+
for (const value of item.values) {
|
|
1404
|
+
await emit(value);
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
advanceCursor(item.cursor, clearOnCursorAdvance);
|
|
1408
|
+
deliveredSinceCursor.add(item.id);
|
|
1409
|
+
if (resetRetry) {
|
|
1410
|
+
resetRetryDelay();
|
|
1411
|
+
}
|
|
1412
|
+
};
|
|
1413
|
+
const retryable = (error) => error instanceof RetryableStreamError || options.isRetryableError(error);
|
|
1414
|
+
const sleep2 = async (delayMs) => {
|
|
1415
|
+
if (delayMs <= 0 || closed) {
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
await new Promise((resolve) => {
|
|
1419
|
+
wakeSleep = resolve;
|
|
1420
|
+
sleepTimer = setTimeout(resolve, jitterDelay(delayMs));
|
|
1421
|
+
});
|
|
1422
|
+
sleepTimer = void 0;
|
|
1423
|
+
wakeSleep = void 0;
|
|
1424
|
+
};
|
|
1425
|
+
const cancelSleep = () => {
|
|
1426
|
+
if (sleepTimer) {
|
|
1427
|
+
clearTimeout(sleepTimer);
|
|
1428
|
+
sleepTimer = void 0;
|
|
1429
|
+
}
|
|
1430
|
+
wakeSleep?.();
|
|
1431
|
+
wakeSleep = void 0;
|
|
1432
|
+
};
|
|
1433
|
+
const nextRetryDelay = () => {
|
|
1434
|
+
const delay = retryDelayMs;
|
|
1435
|
+
retryDelayMs = Math.min(retryDelayMs * 2, maxRetryDelayMs);
|
|
1436
|
+
return delay;
|
|
1437
|
+
};
|
|
1438
|
+
const consumeLive = async () => {
|
|
1439
|
+
const live = options.subscribeLive(lastCursor);
|
|
1440
|
+
activeLive = live;
|
|
1441
|
+
try {
|
|
1442
|
+
for await (const event of live) {
|
|
1443
|
+
await deliverItem(await options.processLive(event), true, true);
|
|
1444
|
+
}
|
|
1445
|
+
throw new RetryableStreamError("Live stream ended");
|
|
1446
|
+
} finally {
|
|
1447
|
+
if (activeLive === live) {
|
|
1448
|
+
activeLive = void 0;
|
|
1449
|
+
}
|
|
1450
|
+
await closeIterable(live);
|
|
1451
|
+
}
|
|
1452
|
+
};
|
|
1453
|
+
const throwLiveError = (liveError) => {
|
|
1454
|
+
if (liveError) {
|
|
1455
|
+
throw liveError;
|
|
1456
|
+
}
|
|
1457
|
+
};
|
|
1458
|
+
const bufferLiveEvent = (buffer, event) => {
|
|
1459
|
+
if (buffer.length >= bufferLimit) {
|
|
1460
|
+
throw new LiveBufferOverflowError(bufferLimit);
|
|
1461
|
+
}
|
|
1462
|
+
buffer.push(event);
|
|
1463
|
+
};
|
|
1464
|
+
const startLivePump = (live, isBuffering, liveBuffer) => {
|
|
1465
|
+
let liveError;
|
|
1466
|
+
const pump2 = (async () => {
|
|
1467
|
+
try {
|
|
1468
|
+
for await (const event of live) {
|
|
1469
|
+
if (isBuffering()) {
|
|
1470
|
+
bufferLiveEvent(liveBuffer, event);
|
|
1471
|
+
continue;
|
|
1472
|
+
}
|
|
1473
|
+
await deliverItem(await options.processLive(event), true, true);
|
|
1474
|
+
}
|
|
1475
|
+
throw new RetryableStreamError("Live stream ended");
|
|
1476
|
+
} catch (error) {
|
|
1477
|
+
liveError = error;
|
|
1478
|
+
}
|
|
1479
|
+
})();
|
|
1480
|
+
return {
|
|
1481
|
+
getError: () => liveError,
|
|
1482
|
+
pump: pump2
|
|
1483
|
+
};
|
|
1484
|
+
};
|
|
1485
|
+
const replayMissed = async (cursor, getLiveError) => {
|
|
1486
|
+
for await (const event of options.fetchMissed(cursor, {
|
|
1487
|
+
limit: catchUpPageSize
|
|
1488
|
+
})) {
|
|
1489
|
+
throwLiveError(getLiveError());
|
|
1490
|
+
await deliverItem(await options.processMissed(event), false, false);
|
|
1491
|
+
}
|
|
1492
|
+
throwLiveError(getLiveError());
|
|
1493
|
+
};
|
|
1494
|
+
const flushLiveBuffer = async (liveBuffer, getLiveError) => {
|
|
1495
|
+
let index = 0;
|
|
1496
|
+
let lastFlushedId;
|
|
1497
|
+
while (index < liveBuffer.length) {
|
|
1498
|
+
throwLiveError(getLiveError());
|
|
1499
|
+
const event = liveBuffer[index];
|
|
1500
|
+
if (event === void 0) {
|
|
1501
|
+
throw new RetryableStreamError("Live stream buffer index missing");
|
|
1502
|
+
}
|
|
1503
|
+
const item = await options.processLive(event);
|
|
1504
|
+
await deliverItem(item, true, false);
|
|
1505
|
+
lastFlushedId = item.id;
|
|
1506
|
+
index += 1;
|
|
1507
|
+
}
|
|
1508
|
+
liveBuffer.length = 0;
|
|
1509
|
+
throwLiveError(getLiveError());
|
|
1510
|
+
return lastFlushedId;
|
|
1511
|
+
};
|
|
1512
|
+
const compactDeliveredIds = (lastId) => {
|
|
1513
|
+
if (!lastId) {
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1516
|
+
deliveredSinceCursor.clear();
|
|
1517
|
+
deliveredSinceCursor.add(lastId);
|
|
1518
|
+
};
|
|
1519
|
+
const catchUpThenConsumeLive = async (cursor) => {
|
|
1520
|
+
const live = options.subscribeLive(cursor);
|
|
1521
|
+
activeLive = live;
|
|
1522
|
+
let buffering = true;
|
|
1523
|
+
const liveBuffer = [];
|
|
1524
|
+
const livePump = startLivePump(live, () => buffering, liveBuffer);
|
|
1525
|
+
try {
|
|
1526
|
+
await replayMissed(cursor, livePump.getError);
|
|
1527
|
+
const lastFlushedId = await flushLiveBuffer(
|
|
1528
|
+
liveBuffer,
|
|
1529
|
+
livePump.getError
|
|
1530
|
+
);
|
|
1531
|
+
compactDeliveredIds(lastFlushedId);
|
|
1532
|
+
buffering = false;
|
|
1533
|
+
resetRetryDelay();
|
|
1534
|
+
await livePump.pump;
|
|
1535
|
+
throwLiveError(livePump.getError());
|
|
1536
|
+
} finally {
|
|
1537
|
+
buffering = false;
|
|
1538
|
+
if (activeLive === live) {
|
|
1539
|
+
activeLive = void 0;
|
|
1540
|
+
}
|
|
1541
|
+
await closeIterable(live);
|
|
1542
|
+
void livePump.pump.catch(ignoreCleanupError);
|
|
1543
|
+
}
|
|
1544
|
+
};
|
|
1545
|
+
const run = async () => {
|
|
1546
|
+
while (!closed) {
|
|
1547
|
+
try {
|
|
1548
|
+
if (lastCursor) {
|
|
1549
|
+
await catchUpThenConsumeLive(lastCursor);
|
|
1550
|
+
} else {
|
|
1551
|
+
await consumeLive();
|
|
1552
|
+
}
|
|
1553
|
+
} catch (error) {
|
|
1554
|
+
await closeIterable(activeLive);
|
|
1555
|
+
activeLive = void 0;
|
|
1556
|
+
if (closed) {
|
|
1557
|
+
break;
|
|
1558
|
+
}
|
|
1559
|
+
if (!retryable(error)) {
|
|
1560
|
+
end(error);
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
await sleep2(nextRetryDelay());
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
end();
|
|
1567
|
+
};
|
|
1568
|
+
const pump = run().catch((error) => {
|
|
1569
|
+
if (!closed) {
|
|
1570
|
+
end(error);
|
|
1571
|
+
}
|
|
1572
|
+
});
|
|
1573
|
+
return async () => {
|
|
1574
|
+
closed = true;
|
|
1575
|
+
cancelSleep();
|
|
1576
|
+
await closeIterable(activeLive);
|
|
1577
|
+
void pump.catch(ignoreCleanupError);
|
|
1578
|
+
};
|
|
1579
|
+
});
|
|
1580
|
+
|
|
1581
|
+
// src/providers/imessage/remote/polls.ts
|
|
1582
|
+
var isVotedPollEvent = (event) => event.delta.type === "voted";
|
|
1583
|
+
var isUnvotedPollEvent = (event) => event.delta.type === "unvoted";
|
|
1584
|
+
var toCachedPoll = (input) => {
|
|
1585
|
+
const poll = asPoll({
|
|
1586
|
+
title: input.title,
|
|
1587
|
+
options: input.options.map((optionInfo) => ({
|
|
1588
|
+
title: optionInfo.text
|
|
1589
|
+
}))
|
|
1590
|
+
});
|
|
1591
|
+
const optionsByIdentifier = /* @__PURE__ */ new Map();
|
|
1592
|
+
for (const [index, optionInfo] of input.options.entries()) {
|
|
1593
|
+
const option = poll.options[index];
|
|
1594
|
+
if (option && optionInfo.optionIdentifier) {
|
|
1595
|
+
optionsByIdentifier.set(optionInfo.optionIdentifier, option);
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
return { poll, optionsByIdentifier };
|
|
1599
|
+
};
|
|
1600
|
+
var cachePollInfo = (cache, info) => {
|
|
1601
|
+
const cached = toCachedPoll(info);
|
|
1602
|
+
cache.set(info.pollMessageGuid, cached);
|
|
1603
|
+
return cached;
|
|
1604
|
+
};
|
|
1605
|
+
var cachePollEvent = (cache, event) => {
|
|
1606
|
+
if (event.delta.type === "created" || event.delta.type === "optionAdded") {
|
|
1607
|
+
try {
|
|
1608
|
+
const cached = toCachedPoll({
|
|
1609
|
+
title: event.delta.title,
|
|
1610
|
+
options: event.delta.options
|
|
1611
|
+
});
|
|
1612
|
+
cache.set(event.pollMessageGuid, cached);
|
|
1613
|
+
return cached;
|
|
1614
|
+
} catch (e) {
|
|
1615
|
+
console.error("[spectrum-ts][imessage][poll] failed to cache poll", e);
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
};
|
|
1619
|
+
var fetchPollInfo = async (client, cache, event) => {
|
|
1620
|
+
try {
|
|
1621
|
+
const info = await client.polls.get(event.pollMessageGuid);
|
|
1622
|
+
cachePollInfo(cache, info);
|
|
1623
|
+
return info;
|
|
1624
|
+
} catch (e) {
|
|
1625
|
+
console.error("[spectrum-ts][imessage][poll] failed to fetch poll", e);
|
|
1626
|
+
return;
|
|
1627
|
+
}
|
|
1628
|
+
};
|
|
1629
|
+
var resolvePoll = async (client, cache, event) => {
|
|
1630
|
+
const pollId = event.pollMessageGuid;
|
|
1631
|
+
const cached = cache.get(pollId);
|
|
1632
|
+
if (cached) {
|
|
1633
|
+
return cached;
|
|
1634
|
+
}
|
|
1635
|
+
try {
|
|
1636
|
+
const info = await client.polls.get(event.pollMessageGuid);
|
|
1637
|
+
return cachePollInfo(cache, info);
|
|
1638
|
+
} catch (e) {
|
|
1639
|
+
console.error("[spectrum-ts][imessage][poll] failed to resolve poll", e);
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1642
|
+
};
|
|
1643
|
+
var buildPollOptionMessage = (input) => {
|
|
1644
|
+
const option = input.cached.optionsByIdentifier.get(input.optionId);
|
|
1645
|
+
if (!option) {
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
const action = input.selected ? "selected" : "deselected";
|
|
1649
|
+
const eventTime = input.event.occurredAt.getTime();
|
|
1650
|
+
return {
|
|
1651
|
+
id: `${input.event.pollMessageGuid}:${input.senderAddress}:${input.optionId}:${action}:${eventTime}`,
|
|
1652
|
+
sender: { id: input.senderAddress },
|
|
1653
|
+
space: {
|
|
1654
|
+
id: input.chatGuid,
|
|
1655
|
+
type: input.chatGuid.includes(";+;") ? "group" : "dm",
|
|
1656
|
+
phone: input.phone
|
|
1657
|
+
},
|
|
1658
|
+
timestamp: input.event.occurredAt,
|
|
1659
|
+
content: asPollOption({
|
|
1660
|
+
option,
|
|
1661
|
+
poll: input.cached.poll,
|
|
1662
|
+
selected: input.selected
|
|
1663
|
+
})
|
|
1664
|
+
};
|
|
1665
|
+
};
|
|
1666
|
+
var buildPollOptionMessages = (input) => {
|
|
1667
|
+
const messages5 = [];
|
|
1668
|
+
for (const delta of input.deltas) {
|
|
1669
|
+
const message = buildPollOptionMessage({
|
|
1670
|
+
cached: input.cached,
|
|
1671
|
+
chatGuid: input.chatGuid,
|
|
1672
|
+
event: input.event,
|
|
1673
|
+
optionId: delta.optionId,
|
|
1674
|
+
phone: input.phone,
|
|
1675
|
+
selected: delta.selected,
|
|
1676
|
+
senderAddress: input.senderAddress
|
|
1677
|
+
});
|
|
1678
|
+
if (message) {
|
|
1679
|
+
messages5.push(message);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
return messages5;
|
|
1683
|
+
};
|
|
1684
|
+
var allOptionIdsKnown = (cached, optionIds) => optionIds.every((optionId) => cached.optionsByIdentifier.has(optionId));
|
|
1685
|
+
var refreshPollMetadata = async (client, pollCache, event) => {
|
|
1686
|
+
const info = await fetchPollInfo(client, pollCache, event);
|
|
1687
|
+
if (!info) {
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1690
|
+
return pollCache.get(info.pollMessageGuid);
|
|
1691
|
+
};
|
|
1692
|
+
var toPollVoteMessages = async (client, pollCache, event, phone) => {
|
|
1693
|
+
const senderAddress = event.actor?.address;
|
|
1694
|
+
if (!senderAddress) {
|
|
1695
|
+
return [];
|
|
1696
|
+
}
|
|
1697
|
+
const pollId = event.pollMessageGuid;
|
|
1698
|
+
if (pollCache.isStaleActorSelectionEvent(
|
|
1699
|
+
pollId,
|
|
1700
|
+
senderAddress,
|
|
1701
|
+
event.occurredAt
|
|
1702
|
+
)) {
|
|
1703
|
+
return [];
|
|
1704
|
+
}
|
|
1705
|
+
const cached = await resolvePoll(client, pollCache, event);
|
|
1706
|
+
if (!cached) {
|
|
1707
|
+
return [];
|
|
1708
|
+
}
|
|
1709
|
+
const chatGuidStr = event.chatGuid;
|
|
1710
|
+
const currentOptionIds = [event.delta.optionIdentifier];
|
|
1711
|
+
let resolvedPoll = cached;
|
|
1712
|
+
if (currentOptionIds.some(
|
|
1713
|
+
(optionId) => !resolvedPoll.optionsByIdentifier.has(optionId)
|
|
1714
|
+
)) {
|
|
1715
|
+
const snapshot = await refreshPollMetadata(client, pollCache, event);
|
|
1716
|
+
if (snapshot) {
|
|
1717
|
+
resolvedPoll = snapshot;
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
if (!allOptionIdsKnown(resolvedPoll, currentOptionIds)) {
|
|
1721
|
+
return [];
|
|
1722
|
+
}
|
|
1723
|
+
const deltas = pollCache.actorSelectionDeltas(
|
|
1724
|
+
pollId,
|
|
1725
|
+
senderAddress,
|
|
1726
|
+
currentOptionIds
|
|
1727
|
+
);
|
|
1728
|
+
const messages5 = buildPollOptionMessages({
|
|
1729
|
+
cached: resolvedPoll,
|
|
1730
|
+
chatGuid: chatGuidStr,
|
|
1731
|
+
deltas,
|
|
1732
|
+
event,
|
|
1733
|
+
phone,
|
|
1734
|
+
senderAddress
|
|
1735
|
+
});
|
|
1736
|
+
pollCache.commitActorSelection(
|
|
1737
|
+
pollId,
|
|
1738
|
+
senderAddress,
|
|
1739
|
+
currentOptionIds,
|
|
1740
|
+
event.occurredAt
|
|
1741
|
+
);
|
|
1742
|
+
return messages5;
|
|
1743
|
+
};
|
|
1744
|
+
var toPollUnvoteMessages = async (client, pollCache, event, phone) => {
|
|
1745
|
+
const senderAddress = event.actor?.address;
|
|
1746
|
+
if (!senderAddress) {
|
|
1747
|
+
return [];
|
|
1748
|
+
}
|
|
1749
|
+
const pollId = event.pollMessageGuid;
|
|
1750
|
+
if (pollCache.isStaleActorSelectionEvent(
|
|
1751
|
+
pollId,
|
|
1752
|
+
senderAddress,
|
|
1753
|
+
event.occurredAt
|
|
1754
|
+
)) {
|
|
1755
|
+
return [];
|
|
1756
|
+
}
|
|
1757
|
+
const cached = await resolvePoll(client, pollCache, event);
|
|
1758
|
+
if (!cached) {
|
|
1759
|
+
return [];
|
|
1760
|
+
}
|
|
1761
|
+
const chatGuidStr = event.chatGuid;
|
|
1762
|
+
const deltas = pollCache.clearedActorSelectionDeltas(pollId, senderAddress);
|
|
1763
|
+
const messages5 = buildPollOptionMessages({
|
|
1764
|
+
cached,
|
|
1765
|
+
chatGuid: chatGuidStr,
|
|
1766
|
+
deltas,
|
|
1767
|
+
event,
|
|
1768
|
+
phone,
|
|
1769
|
+
senderAddress
|
|
1770
|
+
});
|
|
1771
|
+
pollCache.commitActorSelection(pollId, senderAddress, [], event.occurredAt);
|
|
1772
|
+
return messages5;
|
|
1773
|
+
};
|
|
1774
|
+
var toPollDeltaMessages = async (client, pollCache, event, phone) => {
|
|
1775
|
+
if (isVotedPollEvent(event)) {
|
|
1776
|
+
return toPollVoteMessages(client, pollCache, event, phone);
|
|
1777
|
+
}
|
|
1778
|
+
if (isUnvotedPollEvent(event)) {
|
|
1779
|
+
return toPollUnvoteMessages(client, pollCache, event, phone);
|
|
1780
|
+
}
|
|
1781
|
+
return [];
|
|
1782
|
+
};
|
|
1783
|
+
|
|
1784
|
+
// src/providers/imessage/remote/stream.ts
|
|
1785
|
+
var isRetryableIMessageStreamError = (error) => {
|
|
1786
|
+
if (error instanceof AuthenticationError || error instanceof NotFoundError2 || error instanceof ValidationError) {
|
|
1787
|
+
return false;
|
|
1788
|
+
}
|
|
1789
|
+
if (error instanceof IMessageError) {
|
|
1790
|
+
return true;
|
|
1791
|
+
}
|
|
1792
|
+
return false;
|
|
1793
|
+
};
|
|
1794
|
+
var isEventFromCurrentAccount = (event, phone) => phone !== SHARED_PHONE && event.actor?.address !== void 0 && event.actor.address === phone;
|
|
1795
|
+
var toMessageItem = async (client, event, phone, cursor) => {
|
|
1796
|
+
if (event.type === "message.received") {
|
|
1797
|
+
if (event.message.isFromMe) {
|
|
1798
|
+
return { cursor, id: event.message.guid, values: [] };
|
|
1799
|
+
}
|
|
1800
|
+
const cache = getMessageCache(client);
|
|
1801
|
+
return {
|
|
1802
|
+
cursor,
|
|
1803
|
+
id: event.message.guid,
|
|
1804
|
+
values: await toInboundMessages(client, cache, event, phone)
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
if (event.type === "message.reactionAdded") {
|
|
1808
|
+
if (isEventFromCurrentAccount(event, phone)) {
|
|
1809
|
+
return {
|
|
1810
|
+
cursor,
|
|
1811
|
+
id: `${event.messageGuid}:reaction:${event.sequence}`,
|
|
1812
|
+
values: []
|
|
1813
|
+
};
|
|
1814
|
+
}
|
|
1815
|
+
const cache = getMessageCache(client);
|
|
1816
|
+
return {
|
|
1817
|
+
cursor,
|
|
1818
|
+
id: `${event.messageGuid}:reaction:${event.sequence}`,
|
|
1819
|
+
values: await toReactionMessages(client, cache, event, phone)
|
|
1820
|
+
};
|
|
1821
|
+
}
|
|
1822
|
+
return {
|
|
1823
|
+
cursor,
|
|
1824
|
+
id: `${event.type}:${"messageGuid" in event ? event.messageGuid : "unknown"}:${event.sequence}`,
|
|
1825
|
+
values: []
|
|
1826
|
+
};
|
|
1827
|
+
};
|
|
1828
|
+
var toPollItem = async (client, pollCache, event, phone, cursor) => {
|
|
1829
|
+
cachePollEvent(pollCache, event);
|
|
1830
|
+
if (isEventFromCurrentAccount(event, phone)) {
|
|
1831
|
+
return {
|
|
1832
|
+
cursor,
|
|
1833
|
+
id: `${event.pollMessageGuid}:poll:${event.sequence}`,
|
|
1834
|
+
values: []
|
|
1835
|
+
};
|
|
1836
|
+
}
|
|
1837
|
+
return {
|
|
1838
|
+
cursor,
|
|
1839
|
+
id: `${event.pollMessageGuid}:poll:${event.sequence}`,
|
|
1840
|
+
values: await toPollDeltaMessages(client, pollCache, event, phone)
|
|
1841
|
+
};
|
|
1842
|
+
};
|
|
1843
|
+
var toCatchUpCompleteItem = (event) => ({
|
|
1844
|
+
cursor: String(event.headSequence),
|
|
1845
|
+
id: `${event.type}:${event.headSequence}`,
|
|
1846
|
+
values: []
|
|
1847
|
+
});
|
|
1848
|
+
var isMessageEvent = (event) => event.type.startsWith("message.");
|
|
1849
|
+
var isPollEvent = (event) => event.type === "poll.changed";
|
|
1850
|
+
async function* catchUpEvents(client, cursor, isWanted) {
|
|
1851
|
+
const since = toResumeAfter(cursor);
|
|
1852
|
+
if (since === void 0) {
|
|
1853
|
+
return;
|
|
1854
|
+
}
|
|
1855
|
+
for await (const event of client.events.catchUp(since)) {
|
|
1856
|
+
if (event.type === "catchup.complete") {
|
|
1857
|
+
yield event;
|
|
1858
|
+
return;
|
|
1859
|
+
}
|
|
1860
|
+
if (isWanted(event)) {
|
|
1861
|
+
yield event;
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
var toResumeAfter = (cursor) => {
|
|
1866
|
+
if (!cursor) {
|
|
1867
|
+
return void 0;
|
|
1868
|
+
}
|
|
1869
|
+
const sequence = Number(cursor);
|
|
1870
|
+
return Number.isSafeInteger(sequence) && sequence >= 0 ? sequence : void 0;
|
|
1871
|
+
};
|
|
1872
|
+
async function* afterCursor(stream2, cursor) {
|
|
1873
|
+
const resumeAfter = toResumeAfter(cursor);
|
|
1874
|
+
try {
|
|
1875
|
+
for await (const event of stream2) {
|
|
1876
|
+
if (resumeAfter !== void 0 && event.sequence <= resumeAfter) {
|
|
1877
|
+
continue;
|
|
1878
|
+
}
|
|
1879
|
+
yield event;
|
|
1880
|
+
}
|
|
1881
|
+
} finally {
|
|
1882
|
+
await stream2.close?.();
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
var withClose = (source, cursor) => Object.assign(afterCursor(source, cursor), {
|
|
1886
|
+
close: async () => {
|
|
1887
|
+
await source.close?.();
|
|
1888
|
+
}
|
|
1889
|
+
});
|
|
1890
|
+
var messageStream = (client, phone) => resumableOrderedStream({
|
|
1891
|
+
fetchMissed: (cursor) => catchUpEvents(client, cursor, isMessageEvent),
|
|
1892
|
+
isRetryableError: isRetryableIMessageStreamError,
|
|
1893
|
+
processLive: (event) => toMessageItem(client, event, phone, String(event.sequence)),
|
|
1894
|
+
processMissed: (event) => event.type === "catchup.complete" ? Promise.resolve(toCatchUpCompleteItem(event)) : toMessageItem(client, event, phone, String(event.sequence)),
|
|
1895
|
+
subscribeLive: (cursor) => withClose(client.messages.subscribeEvents(), cursor)
|
|
1896
|
+
});
|
|
1897
|
+
var pollStream = (client, pollCache, phone) => resumableOrderedStream({
|
|
1898
|
+
fetchMissed: (cursor) => catchUpEvents(client, cursor, isPollEvent),
|
|
1899
|
+
isRetryableError: isRetryableIMessageStreamError,
|
|
1900
|
+
processLive: (event) => toPollItem(client, pollCache, event, phone, String(event.sequence)),
|
|
1901
|
+
processMissed: (event) => event.type === "catchup.complete" ? Promise.resolve(toCatchUpCompleteItem(event)) : toPollItem(client, pollCache, event, phone, String(event.sequence)),
|
|
1902
|
+
subscribeLive: (cursor) => withClose(client.polls.subscribeEvents(), cursor)
|
|
1903
|
+
});
|
|
1904
|
+
var clientStream = (client, pollCache, phone) => mergeStreams([
|
|
1905
|
+
messageStream(client, phone),
|
|
1906
|
+
pollStream(client, pollCache, phone)
|
|
1907
|
+
]);
|
|
1908
|
+
var messages3 = (clients) => {
|
|
1909
|
+
const pollCache = getPollCache(clients);
|
|
1910
|
+
return mergeStreams(
|
|
1911
|
+
clients.map((entry) => clientStream(entry.client, pollCache, entry.phone))
|
|
1912
|
+
);
|
|
1913
|
+
};
|
|
1914
|
+
|
|
1915
|
+
// src/providers/imessage/remote/typing.ts
|
|
1916
|
+
var startTyping = async (remote, spaceId) => {
|
|
1917
|
+
await remote.chats.setTyping(toChatGuid(spaceId), true);
|
|
1918
|
+
};
|
|
1919
|
+
var stopTyping = async (remote, spaceId) => {
|
|
1920
|
+
await remote.chats.setTyping(toChatGuid(spaceId), false);
|
|
1921
|
+
};
|
|
1922
|
+
|
|
1923
|
+
// src/providers/imessage/remote/api.ts
|
|
1924
|
+
var messages4 = (clients) => messages3(clients);
|
|
1925
|
+
var startTyping2 = async (remote, spaceId) => {
|
|
1926
|
+
await startTyping(remote, spaceId);
|
|
1927
|
+
};
|
|
1928
|
+
var stopTyping2 = async (remote, spaceId) => {
|
|
1929
|
+
await stopTyping(remote, spaceId);
|
|
1930
|
+
};
|
|
1931
|
+
var send4 = async (remote, spaceId, content) => send3(remote, spaceId, content);
|
|
1932
|
+
var replyToMessage2 = async (remote, spaceId, msgId, content) => replyToMessage(remote, spaceId, msgId, content);
|
|
1933
|
+
var editMessage2 = async (remote, spaceId, msgId, content) => editMessage(remote, spaceId, msgId, content);
|
|
1934
|
+
var reactToMessage2 = async (remote, spaceId, target, reaction) => {
|
|
1935
|
+
await reactToMessage(remote, spaceId, target, reaction);
|
|
1936
|
+
};
|
|
1937
|
+
var getMessage4 = async (remote, spaceId, msgId, phone) => getMessage3(remote, spaceId, msgId, phone);
|
|
1938
|
+
|
|
1939
|
+
// src/providers/imessage/remote/client.ts
|
|
1940
|
+
var isSharedMode = (clients) => clients.length === 1 && clients[0]?.phone === SHARED_PHONE;
|
|
1941
|
+
var availablePhones = (clients) => clients.map((c) => c.phone);
|
|
1942
|
+
var clientForPhone = (clients, phone) => {
|
|
1943
|
+
if (isSharedMode(clients)) {
|
|
1944
|
+
const entry2 = clients[0];
|
|
1945
|
+
if (!entry2) {
|
|
1946
|
+
throw new Error("No iMessage clients configured");
|
|
1947
|
+
}
|
|
1948
|
+
return entry2.client;
|
|
1949
|
+
}
|
|
1950
|
+
const entry = clients.find((c) => c.phone === phone);
|
|
1951
|
+
if (!entry) {
|
|
1952
|
+
const list = availablePhones(clients).join(", ") || "<none>";
|
|
1953
|
+
throw new Error(
|
|
1954
|
+
`No iMessage client serves phone ${phone}. Available: ${list}`
|
|
1955
|
+
);
|
|
1956
|
+
}
|
|
1957
|
+
return entry.client;
|
|
1958
|
+
};
|
|
1959
|
+
var randomPhone = (clients) => {
|
|
1960
|
+
if (clients.length === 0) {
|
|
1961
|
+
throw new Error("No iMessage phones configured for this account");
|
|
1962
|
+
}
|
|
1963
|
+
if (isSharedMode(clients)) {
|
|
1964
|
+
return SHARED_PHONE;
|
|
1965
|
+
}
|
|
1966
|
+
const entry = clients[Math.floor(Math.random() * clients.length)];
|
|
1967
|
+
if (!entry) {
|
|
1968
|
+
throw new Error("No iMessage phones configured for this account");
|
|
1969
|
+
}
|
|
1970
|
+
return entry.phone;
|
|
1971
|
+
};
|
|
1972
|
+
|
|
1973
|
+
// src/providers/imessage/index.ts
|
|
1974
|
+
var isPollContent = (content) => content.type === "poll" || content.type === "poll_option";
|
|
1975
|
+
var imessage = definePlatform("iMessage", {
|
|
1976
|
+
config: configSchema,
|
|
1977
|
+
static: {
|
|
1978
|
+
effect: {
|
|
1979
|
+
message: MessageEffect2
|
|
1980
|
+
}
|
|
1981
|
+
},
|
|
1982
|
+
lifecycle: {
|
|
1983
|
+
createClient: async ({
|
|
1984
|
+
config,
|
|
1985
|
+
projectId,
|
|
1986
|
+
projectSecret
|
|
1987
|
+
}) => {
|
|
1988
|
+
if (config.local) {
|
|
1989
|
+
return new IMessageSDK2();
|
|
1990
|
+
}
|
|
1991
|
+
if (config.clients) {
|
|
1992
|
+
const entries = Array.isArray(config.clients) ? config.clients : [config.clients];
|
|
1993
|
+
return entries.map((e) => ({
|
|
1994
|
+
phone: e.phone,
|
|
1995
|
+
client: createClient2({
|
|
1996
|
+
address: e.address,
|
|
1997
|
+
tls: true,
|
|
1998
|
+
token: e.token
|
|
1999
|
+
})
|
|
2000
|
+
}));
|
|
2001
|
+
}
|
|
2002
|
+
if (!(projectId && projectSecret)) {
|
|
2003
|
+
throw new Error(
|
|
2004
|
+
"iMessage requires projectId and projectSecret. Either pass credentials to Spectrum(), use local mode: imessage.config({ local: true }), or provide explicit client config: imessage.config({ clients: [...] })"
|
|
2005
|
+
);
|
|
2006
|
+
}
|
|
2007
|
+
return await createCloudClients(projectId, projectSecret);
|
|
2008
|
+
},
|
|
2009
|
+
destroyClient: async ({ client }) => {
|
|
2010
|
+
if (isLocal(client)) {
|
|
2011
|
+
await client.close();
|
|
2012
|
+
return;
|
|
2013
|
+
}
|
|
2014
|
+
await disposeCloudAuth(client);
|
|
2015
|
+
await Promise.all(client.map((entry) => entry.client.close()));
|
|
2016
|
+
}
|
|
2017
|
+
},
|
|
2018
|
+
user: {
|
|
2019
|
+
resolve: async ({ input }) => ({ id: input.userID })
|
|
2020
|
+
},
|
|
2021
|
+
space: {
|
|
2022
|
+
schema: spaceSchema,
|
|
2023
|
+
params: spaceParamsSchema,
|
|
2024
|
+
resolve: async ({ input, client }) => {
|
|
2025
|
+
if (isLocal(client)) {
|
|
2026
|
+
throw UnsupportedError.action(
|
|
2027
|
+
"createSpace",
|
|
2028
|
+
"iMessage (local mode)",
|
|
2029
|
+
"local mode only supports replying to existing messages"
|
|
2030
|
+
);
|
|
2031
|
+
}
|
|
2032
|
+
if (input.users.length === 0) {
|
|
2033
|
+
throw new Error("iMessage space creation requires at least one user");
|
|
2034
|
+
}
|
|
2035
|
+
if (client.length === 0) {
|
|
2036
|
+
throw new Error("No iMessage clients configured");
|
|
2037
|
+
}
|
|
2038
|
+
const phone = isSharedMode(client) ? SHARED_PHONE : input.params?.phone ?? randomPhone(client);
|
|
2039
|
+
const remote = clientForPhone(client, phone);
|
|
2040
|
+
const addresses = input.users.map((u) => u.id);
|
|
2041
|
+
if (input.users.length === 1) {
|
|
2042
|
+
return {
|
|
2043
|
+
id: dmChatGuid(addresses[0] ?? ""),
|
|
2044
|
+
type: "dm",
|
|
2045
|
+
phone
|
|
2046
|
+
};
|
|
2047
|
+
}
|
|
2048
|
+
const { chat } = await remote.chats.create(addresses);
|
|
2049
|
+
return { id: chat.guid, type: "group", phone };
|
|
2050
|
+
}
|
|
2051
|
+
},
|
|
2052
|
+
message: {
|
|
2053
|
+
schema: messageSchema
|
|
2054
|
+
},
|
|
2055
|
+
events: {
|
|
2056
|
+
messages: ({ client }) => isLocal(client) ? messages2(client) : messages4(client)
|
|
2057
|
+
},
|
|
2058
|
+
actions: {
|
|
2059
|
+
send: async ({ space, content, client }) => {
|
|
2060
|
+
if (isLocal(client)) {
|
|
2061
|
+
return await send2(client, space.id, content);
|
|
2062
|
+
}
|
|
2063
|
+
const remote = clientForPhone(client, space.phone);
|
|
2064
|
+
return await send4(remote, space.id, content);
|
|
2065
|
+
},
|
|
2066
|
+
startTyping: async ({ space, client }) => {
|
|
2067
|
+
if (isLocal(client)) {
|
|
2068
|
+
return;
|
|
2069
|
+
}
|
|
2070
|
+
const remote = clientForPhone(client, space.phone);
|
|
2071
|
+
await startTyping2(remote, space.id);
|
|
2072
|
+
},
|
|
2073
|
+
stopTyping: async ({ space, client }) => {
|
|
2074
|
+
if (isLocal(client)) {
|
|
2075
|
+
return;
|
|
2076
|
+
}
|
|
2077
|
+
const remote = clientForPhone(client, space.phone);
|
|
2078
|
+
await stopTyping2(remote, space.id);
|
|
2079
|
+
},
|
|
2080
|
+
reactToMessage: async ({ space, target, reaction, client }) => {
|
|
2081
|
+
if (isLocal(client)) {
|
|
2082
|
+
throw UnsupportedError.action("react", "iMessage (local mode)");
|
|
2083
|
+
}
|
|
2084
|
+
if (isPollContent(target.content)) {
|
|
2085
|
+
throw UnsupportedError.action(
|
|
2086
|
+
"react",
|
|
2087
|
+
"iMessage",
|
|
2088
|
+
"iMessage polls do not support reactions"
|
|
2089
|
+
);
|
|
2090
|
+
}
|
|
2091
|
+
const remote = clientForPhone(client, space.phone);
|
|
2092
|
+
await reactToMessage2(
|
|
2093
|
+
remote,
|
|
2094
|
+
space.id,
|
|
2095
|
+
target,
|
|
2096
|
+
reaction
|
|
2097
|
+
);
|
|
2098
|
+
},
|
|
2099
|
+
replyToMessage: async ({ space, messageId, target, content, client }) => {
|
|
2100
|
+
if (isLocal(client)) {
|
|
2101
|
+
throw UnsupportedError.action("reply", "iMessage (local mode)");
|
|
2102
|
+
}
|
|
2103
|
+
if (isPollContent(target.content)) {
|
|
2104
|
+
throw UnsupportedError.action(
|
|
2105
|
+
"reply",
|
|
2106
|
+
"iMessage",
|
|
2107
|
+
"iMessage polls do not support replies"
|
|
2108
|
+
);
|
|
2109
|
+
}
|
|
2110
|
+
const remote = clientForPhone(client, space.phone);
|
|
2111
|
+
return await replyToMessage2(remote, space.id, messageId, content);
|
|
2112
|
+
},
|
|
2113
|
+
editMessage: async ({ space, messageId, content, client }) => {
|
|
2114
|
+
if (isLocal(client)) {
|
|
2115
|
+
throw UnsupportedError.action("edit", "iMessage (local mode)");
|
|
2116
|
+
}
|
|
2117
|
+
const remote = clientForPhone(client, space.phone);
|
|
2118
|
+
await editMessage2(remote, space.id, messageId, content);
|
|
2119
|
+
},
|
|
2120
|
+
getMessage: async ({ space, messageId, client }) => {
|
|
2121
|
+
if (isLocal(client)) {
|
|
2122
|
+
return getMessage2(client, messageId);
|
|
2123
|
+
}
|
|
2124
|
+
const remote = clientForPhone(client, space.phone);
|
|
2125
|
+
return getMessage4(remote, space.id, messageId, space.phone);
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
});
|
|
2129
|
+
|
|
2130
|
+
export {
|
|
2131
|
+
effect,
|
|
2132
|
+
imessage
|
|
2133
|
+
};
|