spectrum-ts 1.18.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -1
- package/dist/{attachment-DfWSZS5L.d.ts → attachment-WePAHfcH.d.ts} +1 -1
- package/dist/{authoring-C9uDdZ2F.d.ts → authoring-DDh3muGT.d.ts} +61 -26
- package/dist/authoring.d.ts +3 -3
- package/dist/authoring.js +8 -5
- package/dist/chunk-34FQGGD7.js +34 -0
- package/dist/chunk-3GEJYGZK.js +84 -0
- package/dist/{chunk-MC6ZKFSG.js → chunk-5XEFJBN2.js} +25 -103
- package/dist/{chunk-QGJFZMD5.js → chunk-6UZFVXQF.js} +17 -101
- package/dist/{chunk-NNY6LMSC.js → chunk-77U6SH5A.js} +1 -1
- package/dist/{chunk-YN6WOTBF.js → chunk-AYCMTRVC.js} +622 -79
- package/dist/{chunk-JQN6CRSC.js → chunk-CHY5YLLV.js} +11 -40
- package/dist/{chunk-5BKZJMZV.js → chunk-EZ5SNNFS.js} +79 -38
- package/dist/{chunk-3OTECDNH.js → chunk-FULEQIRQ.js} +31 -23
- package/dist/{chunk-2ILTJC35.js → chunk-LQMDV75O.js} +205 -11
- package/dist/{chunk-IPOFBAIM.js → chunk-LX437ZTY.js} +439 -154
- package/dist/chunk-MHGCPC2V.js +35 -0
- package/dist/chunk-NZ5WCMTY.js +91 -0
- package/dist/chunk-TXRWKSNH.js +927 -0
- package/dist/{chunk-5TIF3FIE.js → chunk-UXJ5OO6P.js} +16 -14
- package/dist/index.d.ts +125 -129
- package/dist/index.js +180 -73
- package/dist/manifest.json +6 -0
- package/dist/providers/imessage/index.d.ts +6 -14
- package/dist/providers/imessage/index.js +9 -6
- package/dist/providers/index.d.ts +5 -2
- package/dist/providers/index.js +18 -10
- package/dist/providers/slack/index.d.ts +1 -2
- package/dist/providers/slack/index.js +5 -4
- package/dist/providers/telegram/index.d.ts +45 -0
- package/dist/providers/telegram/index.js +13 -0
- package/dist/providers/terminal/index.d.ts +18 -422
- package/dist/providers/terminal/index.js +7 -5
- package/dist/providers/whatsapp-business/index.d.ts +1 -1
- package/dist/providers/whatsapp-business/index.js +7 -5
- package/dist/types-BujGKBin.d.ts +82 -0
- package/dist/{types-DcQ5a7PK.d.ts → types-YqCNUDIt.d.ts} +204 -26
- package/package.json +3 -1
|
@@ -1,29 +1,36 @@
|
|
|
1
1
|
import { createRequire as __spectrumCreateRequire } from "node:module"; const require = __spectrumCreateRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
asRichlink
|
|
4
|
+
} from "./chunk-CHY5YLLV.js";
|
|
2
5
|
import {
|
|
3
6
|
asGroup,
|
|
4
|
-
asRichlink,
|
|
5
7
|
groupSchema
|
|
6
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-MHGCPC2V.js";
|
|
7
9
|
import {
|
|
8
10
|
asPoll,
|
|
9
11
|
asPollOption
|
|
10
12
|
} from "./chunk-2D27WW5B.js";
|
|
11
13
|
import {
|
|
12
|
-
cloud
|
|
14
|
+
cloud
|
|
15
|
+
} from "./chunk-3GEJYGZK.js";
|
|
16
|
+
import {
|
|
17
|
+
asContact
|
|
18
|
+
} from "./chunk-NZ5WCMTY.js";
|
|
19
|
+
import {
|
|
13
20
|
mergeStreams,
|
|
14
21
|
stream
|
|
15
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-5XEFJBN2.js";
|
|
16
23
|
import {
|
|
17
|
-
asContact,
|
|
18
24
|
fromVCard,
|
|
19
25
|
toVCard
|
|
20
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-6UZFVXQF.js";
|
|
21
27
|
import {
|
|
22
28
|
UnsupportedError,
|
|
23
29
|
buildPhotoAction,
|
|
24
30
|
definePlatform,
|
|
31
|
+
markdownSchema,
|
|
25
32
|
photoActionSchema
|
|
26
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-LX437ZTY.js";
|
|
27
34
|
import {
|
|
28
35
|
asAttachment,
|
|
29
36
|
asCustom,
|
|
@@ -32,7 +39,7 @@ import {
|
|
|
32
39
|
reactionSchema,
|
|
33
40
|
text,
|
|
34
41
|
textSchema
|
|
35
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-LQMDV75O.js";
|
|
36
43
|
|
|
37
44
|
// src/providers/imessage/index.ts
|
|
38
45
|
import { createClient as createClient2, MessageEffect as MessageEffect2 } from "@photon-ai/advanced-imessage";
|
|
@@ -127,6 +134,7 @@ import {
|
|
|
127
134
|
import z3 from "zod";
|
|
128
135
|
var effectInnerSchema = z3.discriminatedUnion("type", [
|
|
129
136
|
textSchema,
|
|
137
|
+
markdownSchema,
|
|
130
138
|
attachmentSchema
|
|
131
139
|
]);
|
|
132
140
|
var messageEffectSchema = z3.object({
|
|
@@ -147,9 +155,9 @@ function effect(input, messageEffect) {
|
|
|
147
155
|
);
|
|
148
156
|
}
|
|
149
157
|
const inner = await resolveContent(input);
|
|
150
|
-
if (inner.type !== "text" && inner.type !== "attachment") {
|
|
158
|
+
if (inner.type !== "text" && inner.type !== "markdown" && inner.type !== "attachment") {
|
|
151
159
|
throw new Error(
|
|
152
|
-
`imessage effect() only supports text and attachment content, got "${inner.type}"`
|
|
160
|
+
`imessage effect() only supports text, markdown, and attachment content, got "${inner.type}"`
|
|
153
161
|
);
|
|
154
162
|
}
|
|
155
163
|
return messageEffectSchema.parse({
|
|
@@ -565,6 +573,7 @@ var getMessage2 = async (client, id) => getMessage(client, id);
|
|
|
565
573
|
// src/providers/imessage/remote/ids.ts
|
|
566
574
|
var PART_PREFIX = /^p:(\d+)\//;
|
|
567
575
|
var dmChatGuid = (address) => `any;-;${address}`;
|
|
576
|
+
var chatTypeFromGuid = (guid) => guid.includes(";+;") ? "group" : "dm";
|
|
568
577
|
var toChatGuid = (value) => value;
|
|
569
578
|
var toMessageGuid = (value) => value;
|
|
570
579
|
var formatChildId = (partIndex, parentGuid) => `p:${partIndex}/${parentGuid}`;
|
|
@@ -799,7 +808,7 @@ var buildMessageBase = (message, chatGuidHint, timestamp, phone) => {
|
|
|
799
808
|
sender: { id: resolveSenderId(message) },
|
|
800
809
|
space: {
|
|
801
810
|
id: chat,
|
|
802
|
-
type: chat
|
|
811
|
+
type: chatTypeFromGuid(chat),
|
|
803
812
|
phone
|
|
804
813
|
},
|
|
805
814
|
timestamp
|
|
@@ -1084,7 +1093,7 @@ var toReactionMessages = async (client, cache, event, phone) => {
|
|
|
1084
1093
|
sender: { id: senderAddress },
|
|
1085
1094
|
space: {
|
|
1086
1095
|
id: event.chatGuid,
|
|
1087
|
-
type: event.chatGuid
|
|
1096
|
+
type: chatTypeFromGuid(event.chatGuid),
|
|
1088
1097
|
phone
|
|
1089
1098
|
},
|
|
1090
1099
|
timestamp: event.occurredAt,
|
|
@@ -1093,23 +1102,39 @@ var toReactionMessages = async (client, cache, event, phone) => {
|
|
|
1093
1102
|
}
|
|
1094
1103
|
];
|
|
1095
1104
|
};
|
|
1105
|
+
var toSettableReaction = (emoji) => {
|
|
1106
|
+
const native = EMOJI_TO_TAPBACK[emoji];
|
|
1107
|
+
return native ? { kind: native } : { kind: "emoji", emoji };
|
|
1108
|
+
};
|
|
1109
|
+
var tapbackTarget = (target) => ({
|
|
1110
|
+
guid: toMessageGuid(target.parentId ?? target.id),
|
|
1111
|
+
opts: typeof target.partIndex === "number" ? { partIndex: target.partIndex } : void 0
|
|
1112
|
+
});
|
|
1096
1113
|
var reactToMessage = async (remote, spaceId, target, reaction) => {
|
|
1097
|
-
const
|
|
1098
|
-
const
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1114
|
+
const { guid, opts } = tapbackTarget(target);
|
|
1115
|
+
const sent = await remote.messages.setReaction(
|
|
1116
|
+
toChatGuid(spaceId),
|
|
1117
|
+
guid,
|
|
1118
|
+
toSettableReaction(reaction),
|
|
1119
|
+
true,
|
|
1120
|
+
opts
|
|
1121
|
+
);
|
|
1122
|
+
return {
|
|
1123
|
+
id: sent.guid,
|
|
1124
|
+
content: asProviderReaction(reaction, target),
|
|
1125
|
+
space: { id: spaceId },
|
|
1126
|
+
timestamp: sent.dateCreated
|
|
1127
|
+
};
|
|
1128
|
+
};
|
|
1129
|
+
var unsendReaction = async (remote, spaceId, target, reaction) => {
|
|
1130
|
+
const { guid, opts } = tapbackTarget(target);
|
|
1131
|
+
await remote.messages.setReaction(
|
|
1132
|
+
toChatGuid(spaceId),
|
|
1133
|
+
guid,
|
|
1134
|
+
toSettableReaction(reaction),
|
|
1135
|
+
false,
|
|
1136
|
+
opts
|
|
1137
|
+
);
|
|
1113
1138
|
};
|
|
1114
1139
|
|
|
1115
1140
|
// src/providers/imessage/remote/read.ts
|
|
@@ -1245,13 +1270,314 @@ var ensureM4a = async (buffer, mimeType) => {
|
|
|
1245
1270
|
return transcodeToM4a(buffer);
|
|
1246
1271
|
};
|
|
1247
1272
|
|
|
1273
|
+
// src/providers/imessage/remote/markdown.ts
|
|
1274
|
+
import { Marked } from "marked";
|
|
1275
|
+
var markdownLexer = new Marked();
|
|
1276
|
+
var BULLET = "\u2022 ";
|
|
1277
|
+
var HR_LINE = "\u2014\u2014\u2014";
|
|
1278
|
+
var NESTED_LIST_INDENT = " ";
|
|
1279
|
+
var BLOCK_SEPARATOR = "\n\n";
|
|
1280
|
+
var TABLE_CELL_SEPARATOR = " | ";
|
|
1281
|
+
var DEFAULT_LIST_START = 1;
|
|
1282
|
+
var LEADING_WHITESPACE = /^\s+/;
|
|
1283
|
+
var TRAILING_WHITESPACE = /\s+$/;
|
|
1284
|
+
var MONOSPACE_UPPER_A = 120432;
|
|
1285
|
+
var MONOSPACE_LOWER_A = 120458;
|
|
1286
|
+
var MONOSPACE_DIGIT_ZERO = 120822;
|
|
1287
|
+
var UPPER_A = 65;
|
|
1288
|
+
var UPPER_Z = 90;
|
|
1289
|
+
var LOWER_A = 97;
|
|
1290
|
+
var LOWER_Z = 122;
|
|
1291
|
+
var DIGIT_ZERO = 48;
|
|
1292
|
+
var DIGIT_NINE = 57;
|
|
1293
|
+
var monospaceCodePoint = (codePoint) => {
|
|
1294
|
+
if (codePoint >= UPPER_A && codePoint <= UPPER_Z) {
|
|
1295
|
+
return MONOSPACE_UPPER_A + (codePoint - UPPER_A);
|
|
1296
|
+
}
|
|
1297
|
+
if (codePoint >= LOWER_A && codePoint <= LOWER_Z) {
|
|
1298
|
+
return MONOSPACE_LOWER_A + (codePoint - LOWER_A);
|
|
1299
|
+
}
|
|
1300
|
+
if (codePoint >= DIGIT_ZERO && codePoint <= DIGIT_NINE) {
|
|
1301
|
+
return MONOSPACE_DIGIT_ZERO + (codePoint - DIGIT_ZERO);
|
|
1302
|
+
}
|
|
1303
|
+
return codePoint;
|
|
1304
|
+
};
|
|
1305
|
+
var toMonospace = (text2) => {
|
|
1306
|
+
let out = "";
|
|
1307
|
+
for (const char of text2) {
|
|
1308
|
+
const codePoint = char.codePointAt(0);
|
|
1309
|
+
out += codePoint === void 0 ? char : String.fromCodePoint(monospaceCodePoint(codePoint));
|
|
1310
|
+
}
|
|
1311
|
+
return out;
|
|
1312
|
+
};
|
|
1313
|
+
var STYLE_ORDER = ["bold", "italic", "strikethrough"];
|
|
1314
|
+
var plain = (text2) => ({ text: text2, styles: [] });
|
|
1315
|
+
var withStyle = (spans, style) => spans.map(
|
|
1316
|
+
(span) => span.styles.includes(style) ? span : { ...span, styles: [...span.styles, style] }
|
|
1317
|
+
);
|
|
1318
|
+
var asLink = (spans) => spans.map((span) => ({ ...span, link: true }));
|
|
1319
|
+
var spanText = (spans) => {
|
|
1320
|
+
let out = "";
|
|
1321
|
+
for (const span of spans) {
|
|
1322
|
+
out += span.text;
|
|
1323
|
+
}
|
|
1324
|
+
return out;
|
|
1325
|
+
};
|
|
1326
|
+
var joinSpans = (blocks, separator) => {
|
|
1327
|
+
const out = [];
|
|
1328
|
+
for (const [index, block] of blocks.entries()) {
|
|
1329
|
+
if (index > 0) {
|
|
1330
|
+
out.push(plain(separator));
|
|
1331
|
+
}
|
|
1332
|
+
out.push(...block);
|
|
1333
|
+
}
|
|
1334
|
+
return out;
|
|
1335
|
+
};
|
|
1336
|
+
var splitSpanLines = (spans) => {
|
|
1337
|
+
let current = [];
|
|
1338
|
+
const lines = [current];
|
|
1339
|
+
for (const span of spans) {
|
|
1340
|
+
const parts = span.text.split("\n");
|
|
1341
|
+
for (const [index, part] of parts.entries()) {
|
|
1342
|
+
if (index > 0) {
|
|
1343
|
+
current = [];
|
|
1344
|
+
lines.push(current);
|
|
1345
|
+
}
|
|
1346
|
+
if (part) {
|
|
1347
|
+
current.push({ ...span, text: part });
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
return lines;
|
|
1352
|
+
};
|
|
1353
|
+
var asMarkedToken = (token) => token;
|
|
1354
|
+
var checkboxPrefix = (item) => {
|
|
1355
|
+
if (!item.task) {
|
|
1356
|
+
return "";
|
|
1357
|
+
}
|
|
1358
|
+
return item.checked ? "[x] " : "[ ] ";
|
|
1359
|
+
};
|
|
1360
|
+
var listMarker = (list, index) => {
|
|
1361
|
+
if (!list.ordered) {
|
|
1362
|
+
return BULLET;
|
|
1363
|
+
}
|
|
1364
|
+
const start = list.start === "" ? DEFAULT_LIST_START : list.start;
|
|
1365
|
+
return `${start + index}. `;
|
|
1366
|
+
};
|
|
1367
|
+
var renderLink = (token) => {
|
|
1368
|
+
if (token.text === token.href) {
|
|
1369
|
+
return [{ text: token.href, styles: [], link: true }];
|
|
1370
|
+
}
|
|
1371
|
+
return [
|
|
1372
|
+
...asLink(renderInlineTokens(token.tokens)),
|
|
1373
|
+
{ text: ` (${token.href})`, styles: [], link: true }
|
|
1374
|
+
];
|
|
1375
|
+
};
|
|
1376
|
+
var renderImage = (token) => [
|
|
1377
|
+
{
|
|
1378
|
+
text: token.text ? `${token.text} (${token.href})` : token.href,
|
|
1379
|
+
styles: [],
|
|
1380
|
+
link: true
|
|
1381
|
+
}
|
|
1382
|
+
];
|
|
1383
|
+
var renderInlineToken = (token) => {
|
|
1384
|
+
switch (token.type) {
|
|
1385
|
+
case "strong":
|
|
1386
|
+
return withStyle(renderInlineTokens(token.tokens), "bold");
|
|
1387
|
+
case "em":
|
|
1388
|
+
return withStyle(renderInlineTokens(token.tokens), "italic");
|
|
1389
|
+
case "del":
|
|
1390
|
+
return withStyle(renderInlineTokens(token.tokens), "strikethrough");
|
|
1391
|
+
case "codespan":
|
|
1392
|
+
return [plain(toMonospace(token.text))];
|
|
1393
|
+
case "br":
|
|
1394
|
+
return [plain("\n")];
|
|
1395
|
+
case "link":
|
|
1396
|
+
return renderLink(token);
|
|
1397
|
+
case "image":
|
|
1398
|
+
return renderImage(token);
|
|
1399
|
+
case "escape":
|
|
1400
|
+
return [plain(token.text)];
|
|
1401
|
+
case "text":
|
|
1402
|
+
return token.tokens ? renderInlineTokens(token.tokens) : [plain(token.text)];
|
|
1403
|
+
// Raw HTML in markdown source stays literal — styled text has no markup.
|
|
1404
|
+
case "html":
|
|
1405
|
+
return [plain(token.text)];
|
|
1406
|
+
// Task-item checkboxes are rendered from `ListItem.task`/`checked`.
|
|
1407
|
+
case "checkbox":
|
|
1408
|
+
return [];
|
|
1409
|
+
default:
|
|
1410
|
+
return "raw" in token ? [plain(String(token.raw))] : [];
|
|
1411
|
+
}
|
|
1412
|
+
};
|
|
1413
|
+
var renderInlineTokens = (tokens) => {
|
|
1414
|
+
const out = [];
|
|
1415
|
+
for (const token of tokens) {
|
|
1416
|
+
out.push(...renderInlineToken(asMarkedToken(token)));
|
|
1417
|
+
}
|
|
1418
|
+
return out;
|
|
1419
|
+
};
|
|
1420
|
+
var renderBlockquote = (quote) => {
|
|
1421
|
+
const lines = splitSpanLines(renderBlockTokens(quote.tokens));
|
|
1422
|
+
const out = [];
|
|
1423
|
+
for (const [index, line] of lines.entries()) {
|
|
1424
|
+
if (index > 0) {
|
|
1425
|
+
out.push(plain("\n"));
|
|
1426
|
+
}
|
|
1427
|
+
out.push(plain(line.length > 0 ? "> " : ">"), ...line);
|
|
1428
|
+
}
|
|
1429
|
+
return out;
|
|
1430
|
+
};
|
|
1431
|
+
var renderList = (list) => {
|
|
1432
|
+
const out = [];
|
|
1433
|
+
for (const [index, item] of list.items.entries()) {
|
|
1434
|
+
const prefix = `${listMarker(list, index)}${checkboxPrefix(item)}`;
|
|
1435
|
+
const blocks = [];
|
|
1436
|
+
for (const token of item.tokens) {
|
|
1437
|
+
const rendered = renderBlockToken(asMarkedToken(token));
|
|
1438
|
+
if (spanText(rendered)) {
|
|
1439
|
+
blocks.push(rendered);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
const [first = [], ...rest] = splitSpanLines(joinSpans(blocks, "\n"));
|
|
1443
|
+
if (out.length > 0) {
|
|
1444
|
+
out.push(plain("\n"));
|
|
1445
|
+
}
|
|
1446
|
+
out.push(plain(prefix), ...first);
|
|
1447
|
+
for (const line of rest) {
|
|
1448
|
+
out.push(plain(`
|
|
1449
|
+
${NESTED_LIST_INDENT}`), ...line);
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
return out;
|
|
1453
|
+
};
|
|
1454
|
+
var renderTable = (table) => {
|
|
1455
|
+
const out = [];
|
|
1456
|
+
const pushRow = (cells, rowIndex) => {
|
|
1457
|
+
if (rowIndex > 0) {
|
|
1458
|
+
out.push(plain("\n"));
|
|
1459
|
+
}
|
|
1460
|
+
for (const [cellIndex, cell] of cells.entries()) {
|
|
1461
|
+
if (cellIndex > 0) {
|
|
1462
|
+
out.push(plain(TABLE_CELL_SEPARATOR));
|
|
1463
|
+
}
|
|
1464
|
+
out.push(...renderInlineTokens(cell.tokens));
|
|
1465
|
+
}
|
|
1466
|
+
};
|
|
1467
|
+
pushRow(table.header, 0);
|
|
1468
|
+
for (const [index, row] of table.rows.entries()) {
|
|
1469
|
+
pushRow(row, index + 1);
|
|
1470
|
+
}
|
|
1471
|
+
return out;
|
|
1472
|
+
};
|
|
1473
|
+
var renderBlockToken = (token) => {
|
|
1474
|
+
switch (token.type) {
|
|
1475
|
+
// iMessage formatting has no heading sizes; bold is the conventional
|
|
1476
|
+
// stand-in (Telegram precedent).
|
|
1477
|
+
case "heading":
|
|
1478
|
+
return withStyle(renderInlineTokens(token.tokens), "bold");
|
|
1479
|
+
case "paragraph":
|
|
1480
|
+
return renderInlineTokens(token.tokens);
|
|
1481
|
+
case "code":
|
|
1482
|
+
return [plain(toMonospace(token.text))];
|
|
1483
|
+
case "blockquote":
|
|
1484
|
+
return renderBlockquote(token);
|
|
1485
|
+
case "list":
|
|
1486
|
+
return renderList(token);
|
|
1487
|
+
case "table":
|
|
1488
|
+
return renderTable(token);
|
|
1489
|
+
case "hr":
|
|
1490
|
+
return [plain(HR_LINE)];
|
|
1491
|
+
case "space":
|
|
1492
|
+
case "def":
|
|
1493
|
+
return [];
|
|
1494
|
+
default:
|
|
1495
|
+
return renderInlineToken(token);
|
|
1496
|
+
}
|
|
1497
|
+
};
|
|
1498
|
+
var renderBlockTokens = (tokens) => {
|
|
1499
|
+
const blocks = [];
|
|
1500
|
+
for (const token of tokens) {
|
|
1501
|
+
const rendered = renderBlockToken(asMarkedToken(token));
|
|
1502
|
+
if (spanText(rendered)) {
|
|
1503
|
+
blocks.push(rendered);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
return joinSpans(blocks, BLOCK_SEPARATOR);
|
|
1507
|
+
};
|
|
1508
|
+
var trimSpans = (spans) => {
|
|
1509
|
+
const trimmed = [...spans];
|
|
1510
|
+
while (trimmed.length > 0) {
|
|
1511
|
+
const first = trimmed.at(0);
|
|
1512
|
+
const text2 = first?.text.replace(LEADING_WHITESPACE, "");
|
|
1513
|
+
if (first && text2) {
|
|
1514
|
+
trimmed[0] = { ...first, text: text2 };
|
|
1515
|
+
break;
|
|
1516
|
+
}
|
|
1517
|
+
trimmed.shift();
|
|
1518
|
+
}
|
|
1519
|
+
while (trimmed.length > 0) {
|
|
1520
|
+
const last = trimmed.at(-1);
|
|
1521
|
+
const text2 = last?.text.replace(TRAILING_WHITESPACE, "");
|
|
1522
|
+
if (last && text2) {
|
|
1523
|
+
trimmed[trimmed.length - 1] = { ...last, text: text2 };
|
|
1524
|
+
break;
|
|
1525
|
+
}
|
|
1526
|
+
trimmed.pop();
|
|
1527
|
+
}
|
|
1528
|
+
return trimmed;
|
|
1529
|
+
};
|
|
1530
|
+
var finalize = (spans) => {
|
|
1531
|
+
let text2 = "";
|
|
1532
|
+
let hasLinks = false;
|
|
1533
|
+
const open = /* @__PURE__ */ new Map();
|
|
1534
|
+
const ranges = [];
|
|
1535
|
+
const close = (style, end) => {
|
|
1536
|
+
const start = open.get(style);
|
|
1537
|
+
open.delete(style);
|
|
1538
|
+
if (start !== void 0 && end > start) {
|
|
1539
|
+
ranges.push({ type: style, start, length: end - start });
|
|
1540
|
+
}
|
|
1541
|
+
};
|
|
1542
|
+
for (const span of spans) {
|
|
1543
|
+
if (!span.text) {
|
|
1544
|
+
continue;
|
|
1545
|
+
}
|
|
1546
|
+
hasLinks ||= span.link === true;
|
|
1547
|
+
const offset = text2.length;
|
|
1548
|
+
for (const style of STYLE_ORDER) {
|
|
1549
|
+
if (span.styles.includes(style)) {
|
|
1550
|
+
if (!open.has(style)) {
|
|
1551
|
+
open.set(style, offset);
|
|
1552
|
+
}
|
|
1553
|
+
} else {
|
|
1554
|
+
close(style, offset);
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
text2 += span.text;
|
|
1558
|
+
}
|
|
1559
|
+
for (const style of STYLE_ORDER) {
|
|
1560
|
+
close(style, text2.length);
|
|
1561
|
+
}
|
|
1562
|
+
ranges.sort(
|
|
1563
|
+
(a, b) => a.start - b.start || STYLE_ORDER.indexOf(a.type) - STYLE_ORDER.indexOf(b.type)
|
|
1564
|
+
);
|
|
1565
|
+
return { text: text2, formatting: ranges, hasLinks };
|
|
1566
|
+
};
|
|
1567
|
+
var markdownToIMessageText = (markdown) => finalize(trimSpans(renderBlockTokens(markdownLexer.lexer(markdown))));
|
|
1568
|
+
|
|
1248
1569
|
// src/providers/imessage/remote/send.ts
|
|
1249
1570
|
var GROUP_ITEM_ALLOWED = /* @__PURE__ */ new Set([
|
|
1250
1571
|
"text",
|
|
1572
|
+
"markdown",
|
|
1251
1573
|
"attachment",
|
|
1252
1574
|
"contact",
|
|
1253
1575
|
"voice"
|
|
1254
1576
|
]);
|
|
1577
|
+
var GROUP_TEXT_TYPES = /* @__PURE__ */ new Set([
|
|
1578
|
+
"text",
|
|
1579
|
+
"markdown"
|
|
1580
|
+
]);
|
|
1255
1581
|
var MAX_GROUP_TEXT_ITEMS = 1;
|
|
1256
1582
|
var outboundRecord = (spaceId, id, content, timestamp, extras) => ({
|
|
1257
1583
|
id,
|
|
@@ -1268,6 +1594,18 @@ var providerGroup = (items) => asGroup({ items });
|
|
|
1268
1594
|
var withReply = (options, replyTo) => replyTo ? { ...options, replyTo } : options;
|
|
1269
1595
|
var replyOptions = (replyTo) => replyTo ? { replyTo } : void 0;
|
|
1270
1596
|
var effectOption = (effect2) => effect2 ? { effect: effect2 } : {};
|
|
1597
|
+
var formattingOption = (formatting) => formatting.length > 0 ? { formatting } : {};
|
|
1598
|
+
var dataDetectionOption = (hasLinks) => hasLinks ? { enableDataDetection: true } : {};
|
|
1599
|
+
var renderMarkdown = (markdown) => {
|
|
1600
|
+
const rendered = markdownToIMessageText(markdown);
|
|
1601
|
+
if (!rendered.text) {
|
|
1602
|
+
throw unsupportedRemoteContent(
|
|
1603
|
+
"markdown",
|
|
1604
|
+
"renders to empty text \u2014 nothing to send"
|
|
1605
|
+
);
|
|
1606
|
+
}
|
|
1607
|
+
return rendered;
|
|
1608
|
+
};
|
|
1271
1609
|
var replyTargetFromId = (messageId) => {
|
|
1272
1610
|
const childRef = parseChildId(messageId);
|
|
1273
1611
|
if (childRef) {
|
|
@@ -1325,6 +1663,22 @@ var sendContent = async (remote, spaceId, chat, content, replyTo, effect2) => {
|
|
|
1325
1663
|
);
|
|
1326
1664
|
return outboundMessage(spaceId, message, content);
|
|
1327
1665
|
}
|
|
1666
|
+
case "markdown": {
|
|
1667
|
+
const rendered = renderMarkdown(content.markdown);
|
|
1668
|
+
const message = await remote.messages.sendText(
|
|
1669
|
+
chat,
|
|
1670
|
+
rendered.text,
|
|
1671
|
+
withReply(
|
|
1672
|
+
{
|
|
1673
|
+
...effectOption(effect2),
|
|
1674
|
+
...formattingOption(rendered.formatting),
|
|
1675
|
+
...dataDetectionOption(rendered.hasLinks)
|
|
1676
|
+
},
|
|
1677
|
+
replyTo
|
|
1678
|
+
)
|
|
1679
|
+
);
|
|
1680
|
+
return outboundMessage(spaceId, message, content);
|
|
1681
|
+
}
|
|
1328
1682
|
case "richlink": {
|
|
1329
1683
|
const message = await remote.messages.sendText(
|
|
1330
1684
|
chat,
|
|
@@ -1389,7 +1743,7 @@ var validateGroupContent = (content) => {
|
|
|
1389
1743
|
`"${itemType}" items are not supported inside a group`
|
|
1390
1744
|
);
|
|
1391
1745
|
}
|
|
1392
|
-
if (itemType
|
|
1746
|
+
if (GROUP_TEXT_TYPES.has(itemType) && ++textCount > MAX_GROUP_TEXT_ITEMS) {
|
|
1393
1747
|
throw unsupportedRemoteContent(
|
|
1394
1748
|
"group",
|
|
1395
1749
|
`groups can contain at most ${MAX_GROUP_TEXT_ITEMS} text item`
|
|
@@ -1401,6 +1755,10 @@ var resolvePart = async (remote, content) => {
|
|
|
1401
1755
|
switch (content.type) {
|
|
1402
1756
|
case "text":
|
|
1403
1757
|
return { text: content.text };
|
|
1758
|
+
case "markdown": {
|
|
1759
|
+
const rendered = renderMarkdown(content.markdown);
|
|
1760
|
+
return { text: rendered.text, ...formattingOption(rendered.formatting) };
|
|
1761
|
+
}
|
|
1404
1762
|
case "attachment": {
|
|
1405
1763
|
const { guid, name } = await uploadAttachment(remote, content);
|
|
1406
1764
|
return { attachmentGuid: guid, attachmentName: name };
|
|
@@ -1463,20 +1821,29 @@ var editMessage = async (remote, spaceId, msgId, content) => {
|
|
|
1463
1821
|
childRef ? { partIndex: childRef.partIndex } : void 0
|
|
1464
1822
|
);
|
|
1465
1823
|
};
|
|
1824
|
+
var unsendMessage = async (remote, spaceId, msgId) => {
|
|
1825
|
+
const childRef = parseChildId(msgId);
|
|
1826
|
+
await remote.messages.unsend(
|
|
1827
|
+
toChatGuid(spaceId),
|
|
1828
|
+
toMessageGuid(childRef?.parentGuid ?? msgId),
|
|
1829
|
+
childRef ? { partIndex: childRef.partIndex } : void 0
|
|
1830
|
+
);
|
|
1831
|
+
};
|
|
1466
1832
|
|
|
1467
1833
|
// src/providers/imessage/remote/stream.ts
|
|
1468
1834
|
import {
|
|
1469
|
-
AuthenticationError,
|
|
1470
|
-
IMessageError,
|
|
1471
|
-
NotFoundError as NotFoundError3,
|
|
1472
1835
|
ValidationError
|
|
1473
1836
|
} from "@photon-ai/advanced-imessage";
|
|
1837
|
+
import { sanitizePhone } from "@photon-ai/otel";
|
|
1474
1838
|
|
|
1475
1839
|
// src/utils/resumable-stream.ts
|
|
1840
|
+
import { createLogger } from "@photon-ai/otel";
|
|
1476
1841
|
var CATCH_UP_PAGE_SIZE = 100;
|
|
1477
1842
|
var MAX_BUFFERED_LIVE_EVENTS = 1e3;
|
|
1478
1843
|
var RECONNECT_INITIAL_DELAY_MS = 500;
|
|
1479
1844
|
var RECONNECT_MAX_DELAY_MS = 3e4;
|
|
1845
|
+
var PERSISTENT_FAILURE_ERROR_THRESHOLD = 5;
|
|
1846
|
+
var log = createLogger("spectrum.stream");
|
|
1480
1847
|
var RetryableStreamError = class extends Error {
|
|
1481
1848
|
constructor(message) {
|
|
1482
1849
|
super(message);
|
|
@@ -1489,6 +1856,12 @@ var LiveBufferOverflowError = class extends RetryableStreamError {
|
|
|
1489
1856
|
this.name = "LiveBufferOverflowError";
|
|
1490
1857
|
}
|
|
1491
1858
|
};
|
|
1859
|
+
var CursorRejectedError = class extends Error {
|
|
1860
|
+
constructor(cause) {
|
|
1861
|
+
super("Server rejected resume cursor", { cause });
|
|
1862
|
+
this.name = "CursorRejectedError";
|
|
1863
|
+
}
|
|
1864
|
+
};
|
|
1492
1865
|
var closeIterable = async (iterable) => {
|
|
1493
1866
|
if (!iterable) {
|
|
1494
1867
|
return;
|
|
@@ -1496,7 +1869,15 @@ var closeIterable = async (iterable) => {
|
|
|
1496
1869
|
await iterable.close?.();
|
|
1497
1870
|
};
|
|
1498
1871
|
var ignoreCleanupError = () => void 0;
|
|
1499
|
-
var jitterDelay = (delayMs) => Math.random() *
|
|
1872
|
+
var jitterDelay = (delayMs) => delayMs * (0.5 + Math.random() * 0.5);
|
|
1873
|
+
var errorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
1874
|
+
async function* throwOnCursorRejection(source, isCursorRejected) {
|
|
1875
|
+
try {
|
|
1876
|
+
yield* source;
|
|
1877
|
+
} catch (error) {
|
|
1878
|
+
throw isCursorRejected(error) ? new CursorRejectedError(error) : error;
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1500
1881
|
var numericCursor = (cursor) => {
|
|
1501
1882
|
if (!cursor) {
|
|
1502
1883
|
return;
|
|
@@ -1514,15 +1895,23 @@ var resumableOrderedStream = (options) => stream((emit, end) => {
|
|
|
1514
1895
|
const bufferLimit = options.bufferLimit ?? MAX_BUFFERED_LIVE_EVENTS;
|
|
1515
1896
|
const initialRetryDelayMs = options.initialRetryDelayMs ?? RECONNECT_INITIAL_DELAY_MS;
|
|
1516
1897
|
const maxRetryDelayMs = options.maxRetryDelayMs ?? RECONNECT_MAX_DELAY_MS;
|
|
1898
|
+
const jitter = options.jitter ?? jitterDelay;
|
|
1899
|
+
const label = options.label;
|
|
1517
1900
|
let activeLive;
|
|
1518
1901
|
let closed = false;
|
|
1902
|
+
let failedAttempts = 0;
|
|
1519
1903
|
let lastCursor;
|
|
1520
1904
|
let retryDelayMs = initialRetryDelayMs;
|
|
1521
1905
|
let sleepTimer;
|
|
1522
1906
|
let wakeSleep;
|
|
1523
1907
|
const deliveredSinceCursor = /* @__PURE__ */ new Set();
|
|
1524
|
-
const
|
|
1908
|
+
const noteRecovery = () => {
|
|
1525
1909
|
retryDelayMs = initialRetryDelayMs;
|
|
1910
|
+
if (failedAttempts === 0) {
|
|
1911
|
+
return;
|
|
1912
|
+
}
|
|
1913
|
+
log.info("stream recovered", { attempts: failedAttempts, label });
|
|
1914
|
+
failedAttempts = 0;
|
|
1526
1915
|
};
|
|
1527
1916
|
const advanceCursor = (cursor, clearDelivered) => {
|
|
1528
1917
|
if (!cursor || cursor === lastCursor || isCursorRegression(cursor, lastCursor)) {
|
|
@@ -1543,17 +1932,17 @@ var resumableOrderedStream = (options) => stream((emit, end) => {
|
|
|
1543
1932
|
advanceCursor(item.cursor, clearOnCursorAdvance);
|
|
1544
1933
|
deliveredSinceCursor.add(item.id);
|
|
1545
1934
|
if (resetRetry) {
|
|
1546
|
-
|
|
1935
|
+
noteRecovery();
|
|
1547
1936
|
}
|
|
1548
1937
|
};
|
|
1549
|
-
const
|
|
1938
|
+
const isCursorRejected = (error) => options.isCursorRejectedError?.(error) === true;
|
|
1550
1939
|
const sleep2 = async (delayMs) => {
|
|
1551
1940
|
if (delayMs <= 0 || closed) {
|
|
1552
1941
|
return;
|
|
1553
1942
|
}
|
|
1554
1943
|
await new Promise((resolve) => {
|
|
1555
1944
|
wakeSleep = resolve;
|
|
1556
|
-
sleepTimer = setTimeout(resolve,
|
|
1945
|
+
sleepTimer = setTimeout(resolve, jitter(delayMs));
|
|
1557
1946
|
});
|
|
1558
1947
|
sleepTimer = void 0;
|
|
1559
1948
|
wakeSleep = void 0;
|
|
@@ -1571,6 +1960,37 @@ var resumableOrderedStream = (options) => stream((emit, end) => {
|
|
|
1571
1960
|
retryDelayMs = Math.min(retryDelayMs * 2, maxRetryDelayMs);
|
|
1572
1961
|
return delay;
|
|
1573
1962
|
};
|
|
1963
|
+
const handleFailure = (error) => {
|
|
1964
|
+
failedAttempts += 1;
|
|
1965
|
+
const delayMs = nextRetryDelay();
|
|
1966
|
+
if (error instanceof CursorRejectedError) {
|
|
1967
|
+
lastCursor = void 0;
|
|
1968
|
+
deliveredSinceCursor.clear();
|
|
1969
|
+
log.warn(
|
|
1970
|
+
"resume cursor rejected; accepting event gap and resuming live",
|
|
1971
|
+
{
|
|
1972
|
+
attempt: failedAttempts,
|
|
1973
|
+
delayMs,
|
|
1974
|
+
error: errorMessage(error.cause),
|
|
1975
|
+
label
|
|
1976
|
+
}
|
|
1977
|
+
);
|
|
1978
|
+
return delayMs;
|
|
1979
|
+
}
|
|
1980
|
+
const attrs = {
|
|
1981
|
+
attempt: failedAttempts,
|
|
1982
|
+
delayMs,
|
|
1983
|
+
error: errorMessage(error),
|
|
1984
|
+
hasCursor: lastCursor !== void 0,
|
|
1985
|
+
label
|
|
1986
|
+
};
|
|
1987
|
+
if (failedAttempts >= PERSISTENT_FAILURE_ERROR_THRESHOLD) {
|
|
1988
|
+
log.error("stream persistently failing; still retrying", attrs, error);
|
|
1989
|
+
return delayMs;
|
|
1990
|
+
}
|
|
1991
|
+
log.warn("stream interrupted; reconnecting", attrs);
|
|
1992
|
+
return delayMs;
|
|
1993
|
+
};
|
|
1574
1994
|
const consumeLive = async () => {
|
|
1575
1995
|
const live = options.subscribeLive(lastCursor);
|
|
1576
1996
|
activeLive = live;
|
|
@@ -1619,15 +2039,17 @@ var resumableOrderedStream = (options) => stream((emit, end) => {
|
|
|
1619
2039
|
};
|
|
1620
2040
|
};
|
|
1621
2041
|
const replayMissed = async (cursor, getLiveError) => {
|
|
1622
|
-
|
|
1623
|
-
limit: catchUpPageSize
|
|
1624
|
-
|
|
2042
|
+
const missed = throwOnCursorRejection(
|
|
2043
|
+
options.fetchMissed(cursor, { limit: catchUpPageSize }),
|
|
2044
|
+
isCursorRejected
|
|
2045
|
+
);
|
|
2046
|
+
for await (const event of missed) {
|
|
1625
2047
|
throwLiveError(getLiveError());
|
|
1626
2048
|
await deliverItem(await options.processMissed(event), false, false);
|
|
1627
2049
|
}
|
|
1628
2050
|
throwLiveError(getLiveError());
|
|
1629
2051
|
};
|
|
1630
|
-
const flushLiveBuffer = async (liveBuffer, getLiveError) => {
|
|
2052
|
+
const flushLiveBuffer = async (liveBuffer, getLiveError, stopBuffering) => {
|
|
1631
2053
|
let index = 0;
|
|
1632
2054
|
let lastFlushedId;
|
|
1633
2055
|
while (index < liveBuffer.length) {
|
|
@@ -1643,7 +2065,8 @@ var resumableOrderedStream = (options) => stream((emit, end) => {
|
|
|
1643
2065
|
}
|
|
1644
2066
|
liveBuffer.length = 0;
|
|
1645
2067
|
throwLiveError(getLiveError());
|
|
1646
|
-
|
|
2068
|
+
compactDeliveredIds(lastFlushedId);
|
|
2069
|
+
stopBuffering();
|
|
1647
2070
|
};
|
|
1648
2071
|
const compactDeliveredIds = (lastId) => {
|
|
1649
2072
|
if (!lastId) {
|
|
@@ -1660,13 +2083,10 @@ var resumableOrderedStream = (options) => stream((emit, end) => {
|
|
|
1660
2083
|
const livePump = startLivePump(live, () => buffering, liveBuffer);
|
|
1661
2084
|
try {
|
|
1662
2085
|
await replayMissed(cursor, livePump.getError);
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
);
|
|
1667
|
-
compactDeliveredIds(lastFlushedId);
|
|
1668
|
-
buffering = false;
|
|
1669
|
-
resetRetryDelay();
|
|
2086
|
+
await flushLiveBuffer(liveBuffer, livePump.getError, () => {
|
|
2087
|
+
buffering = false;
|
|
2088
|
+
});
|
|
2089
|
+
noteRecovery();
|
|
1670
2090
|
await livePump.pump;
|
|
1671
2091
|
throwLiveError(livePump.getError());
|
|
1672
2092
|
} finally {
|
|
@@ -1687,21 +2107,18 @@ var resumableOrderedStream = (options) => stream((emit, end) => {
|
|
|
1687
2107
|
await consumeLive();
|
|
1688
2108
|
}
|
|
1689
2109
|
} catch (error) {
|
|
1690
|
-
await closeIterable(activeLive);
|
|
2110
|
+
await closeIterable(activeLive).catch(ignoreCleanupError);
|
|
1691
2111
|
activeLive = void 0;
|
|
1692
2112
|
if (closed) {
|
|
1693
2113
|
break;
|
|
1694
2114
|
}
|
|
1695
|
-
|
|
1696
|
-
end(error);
|
|
1697
|
-
return;
|
|
1698
|
-
}
|
|
1699
|
-
await sleep2(nextRetryDelay());
|
|
2115
|
+
await sleep2(handleFailure(error));
|
|
1700
2116
|
}
|
|
1701
2117
|
}
|
|
1702
2118
|
end();
|
|
1703
2119
|
};
|
|
1704
2120
|
const pump = run().catch((error) => {
|
|
2121
|
+
log.error("resumable stream loop crashed", { label }, error);
|
|
1705
2122
|
if (!closed) {
|
|
1706
2123
|
end(error);
|
|
1707
2124
|
}
|
|
@@ -1834,7 +2251,7 @@ var buildPollOptionMessage = (input) => {
|
|
|
1834
2251
|
sender: { id: input.senderAddress },
|
|
1835
2252
|
space: {
|
|
1836
2253
|
id: input.chatGuid,
|
|
1837
|
-
type: input.chatGuid
|
|
2254
|
+
type: chatTypeFromGuid(input.chatGuid),
|
|
1838
2255
|
phone: input.phone
|
|
1839
2256
|
},
|
|
1840
2257
|
timestamp: input.event.occurredAt,
|
|
@@ -1890,15 +2307,8 @@ var toPollDeltaMessages = async (client, pollCache, event, phone) => {
|
|
|
1890
2307
|
};
|
|
1891
2308
|
|
|
1892
2309
|
// src/providers/imessage/remote/stream.ts
|
|
1893
|
-
var
|
|
1894
|
-
|
|
1895
|
-
return false;
|
|
1896
|
-
}
|
|
1897
|
-
if (error instanceof IMessageError) {
|
|
1898
|
-
return true;
|
|
1899
|
-
}
|
|
1900
|
-
return false;
|
|
1901
|
-
};
|
|
2310
|
+
var isCursorRejectedIMessageError = (error) => error instanceof ValidationError;
|
|
2311
|
+
var streamLabel = (kind, phone) => `imessage.${kind}:${phone === SHARED_PHONE ? phone : sanitizePhone(phone)}`;
|
|
1902
2312
|
var isEventFromCurrentAccount = (event, phone) => event.isFromMe || phone !== SHARED_PHONE && event.actor?.address !== void 0 && event.actor.address === phone;
|
|
1903
2313
|
var toMessageItem = async (client, event, phone, cursor, onInbound) => {
|
|
1904
2314
|
if (event.type === "message.received") {
|
|
@@ -2001,7 +2411,8 @@ var withClose = (source, cursor) => Object.assign(afterCursor(source, cursor), {
|
|
|
2001
2411
|
});
|
|
2002
2412
|
var messageStream = (client, phone, onInbound) => resumableOrderedStream({
|
|
2003
2413
|
fetchMissed: (cursor) => catchUpEvents(client, cursor, isMessageEvent),
|
|
2004
|
-
|
|
2414
|
+
isCursorRejectedError: isCursorRejectedIMessageError,
|
|
2415
|
+
label: streamLabel("messages", phone),
|
|
2005
2416
|
processLive: (event) => toMessageItem(client, event, phone, String(event.sequence), onInbound),
|
|
2006
2417
|
processMissed: (event) => event.type === "catchup.complete" ? Promise.resolve(toCatchUpCompleteItem(event)) : toMessageItem(
|
|
2007
2418
|
client,
|
|
@@ -2014,7 +2425,8 @@ var messageStream = (client, phone, onInbound) => resumableOrderedStream({
|
|
|
2014
2425
|
});
|
|
2015
2426
|
var pollStream = (client, pollCache, phone) => resumableOrderedStream({
|
|
2016
2427
|
fetchMissed: (cursor) => catchUpEvents(client, cursor, isPollEvent),
|
|
2017
|
-
|
|
2428
|
+
isCursorRejectedError: isCursorRejectedIMessageError,
|
|
2429
|
+
label: streamLabel("polls", phone),
|
|
2018
2430
|
processLive: (event) => toPollItem(client, pollCache, event, phone, String(event.sequence)),
|
|
2019
2431
|
processMissed: (event) => event.type === "catchup.complete" ? Promise.resolve(toCatchUpCompleteItem(event)) : toPollItem(client, pollCache, event, phone, String(event.sequence)),
|
|
2020
2432
|
subscribeLive: (cursor) => withClose(client.polls.subscribeEvents(), cursor)
|
|
@@ -2039,6 +2451,61 @@ var messages3 = (clients, projectConfig) => {
|
|
|
2039
2451
|
);
|
|
2040
2452
|
};
|
|
2041
2453
|
|
|
2454
|
+
// src/providers/imessage/remote/stream-text.ts
|
|
2455
|
+
var INITIAL_THROTTLE_MS = 1e3;
|
|
2456
|
+
var BACKOFF_FACTOR = 2;
|
|
2457
|
+
var MAX_EDITS = 5;
|
|
2458
|
+
var sendStreamText = async (remote, spaceId, content) => {
|
|
2459
|
+
if (content.format === "markdown") {
|
|
2460
|
+
throw unsupportedRemoteContent(
|
|
2461
|
+
"streamText",
|
|
2462
|
+
"markdown-formatted streams have no native iMessage delivery"
|
|
2463
|
+
);
|
|
2464
|
+
}
|
|
2465
|
+
const chat = toChatGuid(spaceId);
|
|
2466
|
+
let sent;
|
|
2467
|
+
let full = "";
|
|
2468
|
+
let lastSentText = "";
|
|
2469
|
+
let lastEditAt = 0;
|
|
2470
|
+
let editCount = 0;
|
|
2471
|
+
const flushEdit = async (text2) => {
|
|
2472
|
+
if (!sent || text2 === lastSentText) {
|
|
2473
|
+
return;
|
|
2474
|
+
}
|
|
2475
|
+
await remote.messages.edit(chat, toMessageGuid(sent.guid), text2);
|
|
2476
|
+
lastSentText = text2;
|
|
2477
|
+
lastEditAt = Date.now();
|
|
2478
|
+
editCount += 1;
|
|
2479
|
+
};
|
|
2480
|
+
for await (const delta of content.stream()) {
|
|
2481
|
+
full += delta;
|
|
2482
|
+
if (!sent) {
|
|
2483
|
+
sent = await remote.messages.sendText(chat, full);
|
|
2484
|
+
lastSentText = full;
|
|
2485
|
+
lastEditAt = Date.now();
|
|
2486
|
+
continue;
|
|
2487
|
+
}
|
|
2488
|
+
const hasBudgetForInterimEdit = editCount < MAX_EDITS - 1;
|
|
2489
|
+
const requiredGap = INITIAL_THROTTLE_MS * BACKOFF_FACTOR ** editCount;
|
|
2490
|
+
if (hasBudgetForInterimEdit && Date.now() - lastEditAt >= requiredGap) {
|
|
2491
|
+
await flushEdit(full);
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
if (!sent) {
|
|
2495
|
+
throw unsupportedRemoteContent(
|
|
2496
|
+
"streamText",
|
|
2497
|
+
"stream produced no text \u2014 nothing to send"
|
|
2498
|
+
);
|
|
2499
|
+
}
|
|
2500
|
+
await flushEdit(full);
|
|
2501
|
+
return {
|
|
2502
|
+
id: sent.guid,
|
|
2503
|
+
content: asText(full),
|
|
2504
|
+
space: { id: spaceId },
|
|
2505
|
+
timestamp: sent.dateCreated
|
|
2506
|
+
};
|
|
2507
|
+
};
|
|
2508
|
+
|
|
2042
2509
|
// src/providers/imessage/remote/typing.ts
|
|
2043
2510
|
var startTyping = async (remote, spaceId) => {
|
|
2044
2511
|
await remote.chats.setTyping(toChatGuid(spaceId), true);
|
|
@@ -2063,11 +2530,12 @@ var stopTyping2 = async (remote, spaceId) => {
|
|
|
2063
2530
|
await stopTyping(remote, spaceId);
|
|
2064
2531
|
};
|
|
2065
2532
|
var send4 = async (remote, spaceId, content) => send3(remote, spaceId, content);
|
|
2533
|
+
var sendStreamText2 = async (remote, spaceId, content) => sendStreamText(remote, spaceId, content);
|
|
2066
2534
|
var replyToMessage2 = async (remote, spaceId, msgId, content) => replyToMessage(remote, spaceId, msgId, content);
|
|
2067
2535
|
var editMessage2 = async (remote, spaceId, msgId, content) => editMessage(remote, spaceId, msgId, content);
|
|
2068
|
-
var reactToMessage2 = async (remote, spaceId, target, reaction) =>
|
|
2069
|
-
|
|
2070
|
-
|
|
2536
|
+
var reactToMessage2 = async (remote, spaceId, target, reaction) => reactToMessage(remote, spaceId, target, reaction);
|
|
2537
|
+
var unsendMessage2 = async (remote, spaceId, msgId) => unsendMessage(remote, spaceId, msgId);
|
|
2538
|
+
var unsendReaction2 = async (remote, spaceId, target, reaction) => unsendReaction(remote, spaceId, target, reaction);
|
|
2071
2539
|
var getMessage4 = async (remote, spaceId, msgId, phone) => getMessage3(remote, spaceId, msgId, phone);
|
|
2072
2540
|
|
|
2073
2541
|
// src/providers/imessage/remote/client.ts
|
|
@@ -2120,6 +2588,41 @@ var handleEdit = async (client, space, content) => {
|
|
|
2120
2588
|
const remote = clientForPhone(client, space.phone);
|
|
2121
2589
|
await editMessage2(remote, space.id, content.target.id, content.content);
|
|
2122
2590
|
};
|
|
2591
|
+
var handleUnsend = async (client, space, content) => {
|
|
2592
|
+
if (isLocal(client)) {
|
|
2593
|
+
throw UnsupportedError.action("unsend", "iMessage (local mode)");
|
|
2594
|
+
}
|
|
2595
|
+
if (isPollContent(content.target.content)) {
|
|
2596
|
+
throw UnsupportedError.action(
|
|
2597
|
+
"unsend",
|
|
2598
|
+
"iMessage",
|
|
2599
|
+
"iMessage polls cannot be unsent"
|
|
2600
|
+
);
|
|
2601
|
+
}
|
|
2602
|
+
const remote = clientForPhone(client, space.phone);
|
|
2603
|
+
const targetContent = content.target.content;
|
|
2604
|
+
if (targetContent.type === "reaction") {
|
|
2605
|
+
await unsendReaction2(
|
|
2606
|
+
remote,
|
|
2607
|
+
space.id,
|
|
2608
|
+
targetContent.target,
|
|
2609
|
+
targetContent.emoji
|
|
2610
|
+
);
|
|
2611
|
+
return;
|
|
2612
|
+
}
|
|
2613
|
+
await unsendMessage2(remote, space.id, content.target.id);
|
|
2614
|
+
};
|
|
2615
|
+
var handleStreamText = async (client, space, content) => {
|
|
2616
|
+
if (isLocal(client)) {
|
|
2617
|
+
throw UnsupportedError.action(
|
|
2618
|
+
"streamText",
|
|
2619
|
+
"iMessage (local mode)",
|
|
2620
|
+
"streaming text responses require remote iMessage"
|
|
2621
|
+
);
|
|
2622
|
+
}
|
|
2623
|
+
const remote = clientForPhone(client, space.phone);
|
|
2624
|
+
return await sendStreamText2(remote, space.id, content);
|
|
2625
|
+
};
|
|
2123
2626
|
var handleBackground = async (client, space, content) => {
|
|
2124
2627
|
if (isLocal(client)) {
|
|
2125
2628
|
throw UnsupportedError.action(
|
|
@@ -2249,10 +2752,10 @@ var imessage = definePlatform("iMessage", {
|
|
|
2249
2752
|
space: {
|
|
2250
2753
|
schema: spaceSchema,
|
|
2251
2754
|
params: spaceParamsSchema,
|
|
2252
|
-
|
|
2755
|
+
create: async ({ input, client }) => {
|
|
2253
2756
|
if (isLocal(client)) {
|
|
2254
2757
|
throw UnsupportedError.action(
|
|
2255
|
-
"
|
|
2758
|
+
"space.create",
|
|
2256
2759
|
"iMessage (local mode)",
|
|
2257
2760
|
"local mode only supports replying to existing messages"
|
|
2258
2761
|
);
|
|
@@ -2263,18 +2766,52 @@ var imessage = definePlatform("iMessage", {
|
|
|
2263
2766
|
if (client.length === 0) {
|
|
2264
2767
|
throw new Error("No iMessage clients configured");
|
|
2265
2768
|
}
|
|
2266
|
-
const phone = isSharedMode(client) ? SHARED_PHONE : input.params?.phone ?? randomPhone(client);
|
|
2267
|
-
const remote = clientForPhone(client, phone);
|
|
2268
2769
|
const addresses = input.users.map((u) => u.id);
|
|
2269
|
-
if (
|
|
2770
|
+
if (isSharedMode(client)) {
|
|
2771
|
+
if (addresses.length > 1) {
|
|
2772
|
+
throw UnsupportedError.action(
|
|
2773
|
+
"space.create",
|
|
2774
|
+
"iMessage (shared mode)",
|
|
2775
|
+
"shared mode cannot create group chats \u2014 use a dedicated number, or space.get(chatGuid) for an existing group"
|
|
2776
|
+
);
|
|
2777
|
+
}
|
|
2270
2778
|
return {
|
|
2271
2779
|
id: dmChatGuid(addresses[0] ?? ""),
|
|
2272
2780
|
type: "dm",
|
|
2273
|
-
phone
|
|
2781
|
+
phone: SHARED_PHONE
|
|
2274
2782
|
};
|
|
2275
2783
|
}
|
|
2784
|
+
const phone = input.params?.phone ?? randomPhone(client);
|
|
2785
|
+
const remote = clientForPhone(client, phone);
|
|
2276
2786
|
const { chat } = await remote.chats.create(addresses);
|
|
2277
|
-
return {
|
|
2787
|
+
return {
|
|
2788
|
+
id: chat.guid,
|
|
2789
|
+
type: chat.isGroup ? "group" : "dm",
|
|
2790
|
+
phone
|
|
2791
|
+
};
|
|
2792
|
+
},
|
|
2793
|
+
get: async ({ input, client }) => {
|
|
2794
|
+
if (isLocal(client)) {
|
|
2795
|
+
throw UnsupportedError.action(
|
|
2796
|
+
"space.get",
|
|
2797
|
+
"iMessage (local mode)",
|
|
2798
|
+
"local mode only supports replying to existing messages"
|
|
2799
|
+
);
|
|
2800
|
+
}
|
|
2801
|
+
if (client.length === 0) {
|
|
2802
|
+
throw new Error("No iMessage clients configured");
|
|
2803
|
+
}
|
|
2804
|
+
const phone = isSharedMode(client) ? SHARED_PHONE : input.params?.phone ?? (client.length === 1 ? client[0]?.phone : void 0);
|
|
2805
|
+
if (!phone) {
|
|
2806
|
+
throw new Error(
|
|
2807
|
+
`iMessage space.get requires params.phone when multiple clients are configured. Available: ${availablePhones(client).join(", ")}`
|
|
2808
|
+
);
|
|
2809
|
+
}
|
|
2810
|
+
return {
|
|
2811
|
+
id: input.id,
|
|
2812
|
+
type: chatTypeFromGuid(input.id),
|
|
2813
|
+
phone
|
|
2814
|
+
};
|
|
2278
2815
|
},
|
|
2279
2816
|
actions: {
|
|
2280
2817
|
// Sugar: `space.background(input, opts?)` →
|
|
@@ -2334,13 +2871,12 @@ var imessage = definePlatform("iMessage", {
|
|
|
2334
2871
|
);
|
|
2335
2872
|
}
|
|
2336
2873
|
const remote2 = clientForPhone(client, space.phone);
|
|
2337
|
-
await reactToMessage2(
|
|
2874
|
+
return await reactToMessage2(
|
|
2338
2875
|
remote2,
|
|
2339
2876
|
space.id,
|
|
2340
2877
|
content.target,
|
|
2341
2878
|
content.emoji
|
|
2342
2879
|
);
|
|
2343
|
-
return;
|
|
2344
2880
|
}
|
|
2345
2881
|
if (content.type === "typing") {
|
|
2346
2882
|
await handleTyping(client, space, content.state);
|
|
@@ -2350,6 +2886,13 @@ var imessage = definePlatform("iMessage", {
|
|
|
2350
2886
|
await handleEdit(client, space, content);
|
|
2351
2887
|
return;
|
|
2352
2888
|
}
|
|
2889
|
+
if (content.type === "unsend") {
|
|
2890
|
+
await handleUnsend(client, space, content);
|
|
2891
|
+
return;
|
|
2892
|
+
}
|
|
2893
|
+
if (content.type === "streamText") {
|
|
2894
|
+
return await handleStreamText(client, space, content);
|
|
2895
|
+
}
|
|
2353
2896
|
if (content.type === "rename") {
|
|
2354
2897
|
await handleRename(client, space, content);
|
|
2355
2898
|
return;
|