@vercel/board 0.16.0 → 0.18.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 +27 -27
- package/dist/{a2-C41hD8SF.mjs → a2-ChRLRupa.mjs} +969 -25
- package/dist/access.d.mts +1 -1
- package/dist/access.mjs +3 -3
- package/dist/{actors-DjMeZpnU.mjs → actors-BOamkEuz.mjs} +3 -73
- package/dist/attempts-CLG840Oj.mjs +20 -0
- package/dist/{auth-COzc09sn.mjs → auth-Drg9FiC2.mjs} +225 -2
- package/dist/backend-B-SJ7p6p.d.mts +680 -0
- package/dist/backend-a2/contract.d.mts +873 -2
- package/dist/backend-a2/contract.mjs +2 -2
- package/dist/backend-a2/scheduler-local.d.mts +1 -1
- package/dist/backend-a2/scheduler-local.mjs +1 -1
- package/dist/backend-a2/scheduler-qstash.d.mts +78 -1
- package/dist/backend-a2/scheduler-qstash.mjs +521 -1
- package/dist/backend-a2/scheduler-vercel.d.mts +37 -1
- package/dist/backend-a2/scheduler-vercel.mjs +203 -1
- package/dist/backend-a2/store-memory.d.mts +2 -2
- package/dist/backend-a2/store-memory.mjs +3 -3
- package/dist/backend-a2/store-postgres.d.mts +1 -1
- package/dist/backend-a2/store-postgres.mjs +2850 -2
- package/dist/backend-a2/store-redis-http.d.mts +1 -1
- package/dist/backend-a2/store-redis-http.mjs +490 -2
- package/dist/backend-a2/store-redis.d.mts +10 -2
- package/dist/backend-a2/store-redis.mjs +51 -2
- package/dist/backend-a2/store-sqlite.d.mts +1 -1
- package/dist/backend-a2/store-sqlite.mjs +3 -3
- package/dist/backend-a2.d.mts +2706 -3
- package/dist/backend-a2.mjs +1 -1
- package/dist/{backend-http-Bid7RIpC.d.mts → backend-http-7cNb8nJI.d.mts} +10 -3
- package/dist/{backend-http-0roEmjl0.mjs → backend-http-CMlL3yq3.mjs} +2 -2
- package/dist/{backend-http-BVRno-ca.mjs → backend-http-Ddzd5kTS.mjs} +32 -3
- package/dist/backend-http.d.mts +1 -1
- package/dist/backend-http.mjs +1 -1
- package/dist/backend-http.node.d.mts +1 -1
- package/dist/backend-http.node.mjs +1 -1
- package/dist/{chat-provider-XCnQlxq6.d.mts → chat-provider-DNJQyajS.d.mts} +16 -4
- package/dist/{chat-provider-jFSeyNmB.mjs → chat-provider-YHy-WbrB.mjs} +15 -9
- package/dist/cli.mjs +30 -10
- package/dist/{client-DGDPaN7U.mjs → client-BA-J7usH.mjs} +10 -2
- package/dist/{conformance-DRR23YNy.mjs → conformance-CT5EeHeC.mjs} +5 -2
- package/dist/conformance.d.mts +1 -1
- package/dist/conformance.mjs +1 -1
- package/dist/{engine-Bir5s8ts.mjs → engine-D0oBUBEK.mjs} +249 -163
- package/dist/events-CewD4kw7.mjs +75 -0
- package/dist/{exports-Dvc1o52I.mjs → exports-CgjfMrQ-.mjs} +35 -32
- package/dist/exports-ZsgjzEnM.d.mts +4 -0
- package/dist/external-BV_cUncs.mjs +151 -0
- package/dist/folds-CRYY14kc.d.mts +534 -0
- package/dist/{reducers-mUPFuFJV.mjs → folds-Dyjput8M.mjs} +6 -73
- package/dist/ids-C6AYo1bu.mjs +31 -0
- package/dist/index.d.mts +4 -4
- package/dist/{index.default-Bkg77Pxn.mjs → index.default-C5Cpz3sa.mjs} +1 -1
- package/dist/index.mjs +5 -5
- package/dist/index.node.d.mts +4 -4
- package/dist/index.node.mjs +3 -3
- package/dist/inspect/app.js +31 -31
- package/dist/inspect/engine.js +5 -5
- package/dist/internal-CvsrprnU-CRY89I-b.mjs +203 -0
- package/dist/{local-Cr_dSjvs.mjs → local-DhkrAqPP.mjs} +1 -1
- package/dist/mentions-CB6ShY4R.mjs +72 -0
- package/dist/{targets-yM5JBUgr.mjs → middleware-CqaCwucP.mjs} +1 -33
- package/dist/platform-B4TnJtWu-CwcTyKg2.mjs +32 -0
- package/dist/{project-CFiArraX.mjs → project-DMDST9kl.mjs} +1 -1
- package/dist/{provider-BGjz-mDa.d.mts → provider-B0b9Qxw4.d.mts} +170 -40
- package/dist/provider-B3DpYSDq.mjs +220 -0
- package/dist/redis-tables-DN9PfIDi.mjs +3897 -0
- package/dist/reducer-CYPHsSmd-BQIfozGi.d.mts +170 -0
- package/dist/reducer-CjmvZSAb-D_OSISmj.mjs +74 -0
- package/dist/{contract-CuJmlrhw.mjs → reducers-BFmIXilu.mjs} +260 -287
- package/dist/retryable-lazy-DZWmHpii-Bye-W8q7.mjs +19 -0
- package/dist/scheduler-task-X1SToDjP-BVoWuInI.mjs +69 -0
- package/dist/{serve-BsOOc-6_.mjs → serve-BGYw0uNL.mjs} +5 -5
- package/dist/{server-jwPY7_wi.mjs → server-DPxBSogx.mjs} +4 -4
- package/dist/server-DtFrWxi3-Boa6gdWO.mjs +3602 -0
- package/dist/server-qgez0Xny-D5mLfqFQ.d.mts +385 -0
- package/dist/{serving-BNbzc96L.mjs → serving-CNkgUPW1.mjs} +1 -1
- package/dist/session-clear-wHd5GCpZ-BnKSxReK.mjs +51 -0
- package/dist/{sql-DbiEnqM0.mjs → sql-U9WTxrwJ.mjs} +2 -2
- package/dist/{sqlite-db-Cbj03vmp.mjs → sqlite-db-Ds4majtT.mjs} +1 -1
- package/dist/store-D0MhBvg6-4Z2L0Kqd.d.mts +530 -0
- package/dist/store-N8PXxDAS-BWBgzBHp.mjs +5 -0
- package/dist/store-codec-DwrWlgQx-CYxGW0aD.mjs +61 -0
- package/dist/store-memory-Bas9Uq5C.mjs +872 -0
- package/dist/store-memory-ZLiZbH3R.mjs +2 -0
- package/dist/store-polling-BZt2Rf3D-BiEM6_HB.mjs +148 -0
- package/dist/store-presence-polling-JuAy2Ck2-CYpuK75X.mjs +109 -0
- package/dist/store-sqlite-BmwL3pUR.mjs +2 -0
- package/dist/store-sqlite-CxxPCcCT.mjs +1053 -0
- package/dist/stream-buffer-DrTyiIuo-BKy-vI14.mjs +53 -0
- package/dist/sync-github.d.mts +24 -17
- package/dist/sync-github.mjs +89 -22
- package/dist/sync-kapso.d.mts +15 -13
- package/dist/sync-kapso.mjs +17 -14
- package/dist/sync-linear.d.mts +219 -0
- package/dist/sync-linear.mjs +311 -0
- package/dist/sync-slack.d.mts +12 -17
- package/dist/sync-slack.mjs +54 -42
- package/dist/sync.chat.d.mts +1 -1
- package/dist/sync.chat.mjs +1 -1
- package/dist/sync.d.mts +3 -3
- package/dist/sync.mjs +5 -4
- package/dist/targets-C-wdR5wP.mjs +33 -0
- package/dist/{tokens-DKLn5P9B.mjs → tokens-DhuzKzAl.mjs} +1 -1
- package/dist/{types-BHCjTZdB.d.mts → types-8yq2VnKI.d.mts} +2 -2
- package/dist/{users-Dls7_ANV.mjs → users-yryHKFMo.mjs} +2 -2
- package/dist/{watch-spec-W0c7ypHJ.mjs → watchers-hash-RLtnSF-t.mjs} +233 -28
- package/docs/01-board/01-mounting.md +1 -1
- package/docs/02-sync/01-overview.md +72 -48
- package/docs/02-sync/02-functions.md +398 -0
- package/docs/02-sync/03-slack.md +30 -25
- package/docs/02-sync/04-github.md +62 -26
- package/docs/02-sync/05-whatsapp.md +23 -19
- package/docs/02-sync/06-providers.md +60 -27
- package/docs/02-sync/07-linear.md +129 -0
- package/docs/03-guides/04-triage.md +1 -1
- package/docs/04-reference/01-cli.md +26 -6
- package/docs/04-reference/02-board.md +18 -5
- package/docs/04-reference/03-http.md +5 -2
- package/docs/04-reference/05-conformance.md +1 -1
- package/docs/index.md +5 -10
- package/package.json +31 -6
- package/dist/a2-D24cvwpN.d.mts +0 -3359
- package/dist/access-CJU2-vaa.mjs +0 -226
- package/dist/attempts-Bxp9KgPf.mjs +0 -919
- package/dist/exports-DLiE7fLY.d.mts +0 -4
- package/dist/provider-CuewcDNZ.mjs +0 -205
- package/dist/redis-tables-B35vv4CZ.mjs +0 -842
- package/dist/reducers-B_vUGQRp.d.mts +0 -1172
- package/dist/watchers-hash-CLjsa6od.mjs +0 -225
- package/docs/02-sync/02-callbacks.md +0 -265
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { a as typeOf, i as fileSize, r as IMPORT_BYTES_MAX, t as ATTACHMENT_BYTES_MAX } from "./files-SniSmeEr.mjs";
|
|
2
2
|
import { C as toStorable, c as ChannelPath, m as MESSAGE_TEXT_MAX } from "./schemas-Be08nyfW.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { c as externalMessageKey, l as isThreadTarget, s as channelOf } from "./external-BV_cUncs.mjs";
|
|
4
4
|
import { t as canonicalEmoji } from "./emoji-CTLpvH-A.mjs";
|
|
5
|
+
import { n as derivedId } from "./ids-C6AYo1bu.mjs";
|
|
5
6
|
import { r as badRequest, s as notFound, t as HttpError } from "./errors-DX3hpwWt.mjs";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
7
|
+
import { n as replaceMentions } from "./mentions-CB6ShY4R.mjs";
|
|
8
|
+
import { t as DroppedError } from "./middleware-CqaCwucP.mjs";
|
|
9
|
+
import { n as waitUntil, t as afterRetry } from "./attempts-CLG840Oj.mjs";
|
|
10
|
+
import { a as channelMissing, i as UnreadableError, n as ResolvedTarget, o as routesFor, t as ReceivedEvent } from "./provider-B3DpYSDq.mjs";
|
|
11
|
+
import { a as immutable, n as SyncOverrides } from "./events-CewD4kw7.mjs";
|
|
11
12
|
import { t as displayText } from "./bots-CGkO76qH.mjs";
|
|
12
13
|
//#region src/sync/engine.ts
|
|
13
14
|
/** `files`: a message with files may have no text. */
|
|
@@ -169,38 +170,43 @@ function createSyncEngine(entries) {
|
|
|
169
170
|
return state;
|
|
170
171
|
}
|
|
171
172
|
const linkedTo = (actors, person) => Object.entries(actors.links).filter(([, to]) => to === person).map(([actor]) => actor);
|
|
172
|
-
/** How long a
|
|
173
|
+
/** How long a name that didn't resolve (say, a Slack channel that doesn't exist yet) waits before another try. */
|
|
173
174
|
const RETRY_MS = 6e4;
|
|
174
|
-
const
|
|
175
|
+
const resolutions = /* @__PURE__ */ new Map();
|
|
175
176
|
/**
|
|
176
|
-
*
|
|
177
|
-
* resolves
|
|
177
|
+
* A platform channel's ID from its name, on entry `key`: what sync functions route by. Each
|
|
178
|
+
* resolves once. One the platform says doesn't exist is logged and
|
|
179
|
+
* tried again a minute later; one it couldn't check is asked again on the next call, so a
|
|
180
|
+
* retried event asks again.
|
|
178
181
|
*/
|
|
179
|
-
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const pairs = Object.entries(platform.mappings);
|
|
182
|
+
function resolve(key, name) {
|
|
183
|
+
const at = `${key}:${name}`;
|
|
184
|
+
const cached = resolutions.get(at);
|
|
185
|
+
if (cached && (!cached.failed || Date.now() - cached.at < RETRY_MS)) return cached.id;
|
|
184
186
|
const entry = {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
187
|
+
id: platformOf(key).resolveChannel(name),
|
|
188
|
+
failed: false,
|
|
189
|
+
at: Date.now()
|
|
188
190
|
};
|
|
189
|
-
entry.
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
else {
|
|
195
|
-
entry.complete = false;
|
|
196
|
-
console.warn(`[board] sync ${key}: skipping ${raw}: ${result.reason.message}`);
|
|
197
|
-
}
|
|
198
|
-
});
|
|
199
|
-
return map;
|
|
191
|
+
entry.id.catch((error) => {
|
|
192
|
+
entry.failed = true;
|
|
193
|
+
const missing = channelMissing(error);
|
|
194
|
+
if (!missing && resolutions.get(at) === entry) resolutions.delete(at);
|
|
195
|
+
console.warn(`[board] sync ${key}: ${missing ? "skipping" : "couldn't resolve"} ${name}: ${error.message}`);
|
|
200
196
|
});
|
|
201
|
-
|
|
202
|
-
return entry.
|
|
203
|
-
}
|
|
197
|
+
resolutions.set(at, entry);
|
|
198
|
+
return entry.id;
|
|
199
|
+
}
|
|
200
|
+
/** The platform channel a target is in, as the platform names it; null when its provider can't tell. */
|
|
201
|
+
function channelIn(key, target) {
|
|
202
|
+
if (!isThreadTarget(target)) return native(target);
|
|
203
|
+
try {
|
|
204
|
+
const channel = platformOf(key).inboxChannel(key, target);
|
|
205
|
+
return channel === null ? null : native(channel);
|
|
206
|
+
} catch {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
204
210
|
/** A synced platform's user with this email, for `@[ada@vercel.com]`. */
|
|
205
211
|
async function findByEmail(email) {
|
|
206
212
|
for (const platform of Object.values(entries)) {
|
|
@@ -215,18 +221,6 @@ function createSyncEngine(entries) {
|
|
|
215
221
|
}
|
|
216
222
|
return null;
|
|
217
223
|
}
|
|
218
|
-
/** External channels a new root in this board channel starts threads in. */
|
|
219
|
-
async function configTargets(channel) {
|
|
220
|
-
const targets = [];
|
|
221
|
-
for (const [key, platform] of Object.entries(entries)) {
|
|
222
|
-
if (!platform.canStartThreads) continue;
|
|
223
|
-
for (const [external, target] of await mappings(key)) if (target.channel === channel) targets.push({
|
|
224
|
-
target: external,
|
|
225
|
-
kind: "channel"
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
return targets;
|
|
229
|
-
}
|
|
230
224
|
/** Validates a requested target and resolves names (`slack:#prs`) to IDs. */
|
|
231
225
|
async function normalizeTarget(target) {
|
|
232
226
|
const at = target.indexOf(":");
|
|
@@ -254,7 +248,8 @@ function createSyncEngine(entries) {
|
|
|
254
248
|
event.thread,
|
|
255
249
|
"subthread" in event ? event.subthread : null
|
|
256
250
|
]) if (location !== null && !location.startsWith(`${key}:`)) throw badRequest(`provider locations must belong to sync entry ${key}`);
|
|
257
|
-
|
|
251
|
+
const routes = routesFor(platformOf(key));
|
|
252
|
+
if (routes && !await routes(native(event.channel), (name) => resolve(key, name)) && !await syncsThread(event.thread)) return;
|
|
258
253
|
await call("inboxPush", {
|
|
259
254
|
key,
|
|
260
255
|
events: [{
|
|
@@ -458,11 +453,10 @@ function createSyncEngine(entries) {
|
|
|
458
453
|
notes
|
|
459
454
|
};
|
|
460
455
|
}
|
|
461
|
-
/** One platform event from the inbox,
|
|
456
|
+
/** One platform event from the inbox, through its entry's sync function. `id` is its ID there, which its writes derive theirs from. */
|
|
462
457
|
async function inbound(key, item, meta) {
|
|
463
458
|
const { event, raw } = item;
|
|
464
459
|
const platform = platformOf(key);
|
|
465
|
-
const mapping = (await mappings(key)).get(event.channel);
|
|
466
460
|
const linked = (await find({ thread: event.thread })).thread ?? void 0;
|
|
467
461
|
if (event.kind === "message") {
|
|
468
462
|
const copied = (await find({
|
|
@@ -483,56 +477,68 @@ function createSyncEngine(entries) {
|
|
|
483
477
|
avatar: null,
|
|
484
478
|
isBot: true
|
|
485
479
|
};
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
key,
|
|
489
|
-
...originOf(key, event)
|
|
490
|
-
}, meta.id);
|
|
491
|
-
let applied;
|
|
492
|
-
let active = true;
|
|
493
|
-
const context = {
|
|
480
|
+
let typed;
|
|
481
|
+
const typedEvent = () => typed ??= immutable(platform.event({
|
|
494
482
|
id: meta.id,
|
|
495
483
|
key,
|
|
496
484
|
event,
|
|
497
|
-
raw
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
485
|
+
raw
|
|
486
|
+
}));
|
|
487
|
+
let imported;
|
|
488
|
+
let link;
|
|
489
|
+
let active = true;
|
|
490
|
+
const sync = {
|
|
491
|
+
direction: "in",
|
|
492
|
+
key,
|
|
493
|
+
channel: native(event.channel),
|
|
494
|
+
get event() {
|
|
495
|
+
return typedEvent();
|
|
496
|
+
},
|
|
497
|
+
board: host().client({
|
|
498
|
+
author,
|
|
499
|
+
key,
|
|
500
|
+
...originOf(key, event)
|
|
501
|
+
}, meta.id),
|
|
502
|
+
import(channel, overrides = {}) {
|
|
503
|
+
if (!active) throw badRequest("the sync function has finished");
|
|
504
|
+
imported ??= importEvent(key, item, meta, linked, channel, overrides);
|
|
505
|
+
return imported;
|
|
506
|
+
},
|
|
507
|
+
permalink() {
|
|
508
|
+
const promise = link ?? platform.permalink(typedEvent());
|
|
509
|
+
link = promise;
|
|
510
|
+
promise.catch(() => {
|
|
511
|
+
if (link === promise) link = void 0;
|
|
509
512
|
});
|
|
510
|
-
return
|
|
513
|
+
return promise;
|
|
511
514
|
},
|
|
512
|
-
|
|
513
|
-
async function sync(first, second, overrides) {
|
|
514
|
-
if (!active) throw badRequest("the sync callback has finished");
|
|
515
|
-
if (typeof second === "string") return board.sync(first, second);
|
|
516
|
-
if (typeof first !== "string" || second !== publicEvent) throw badRequest("sync only accepts the current verified event");
|
|
517
|
-
return platform.sync(context, first, overrides);
|
|
518
|
-
}
|
|
519
|
-
return {
|
|
520
|
-
...board,
|
|
521
|
-
sync
|
|
522
|
-
};
|
|
523
|
-
}
|
|
515
|
+
resolve: (name) => resolve(key, name)
|
|
524
516
|
};
|
|
525
517
|
try {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
return;
|
|
529
|
-
}
|
|
530
|
-
const channel = mapping?.channel ?? (linked ? (await syncOf(linked))?.channel : void 0);
|
|
531
|
-
if (channel) await platform.sync(context, channel);
|
|
518
|
+
await platform.sync(sync);
|
|
519
|
+
await imported;
|
|
532
520
|
} finally {
|
|
533
521
|
active = false;
|
|
534
522
|
}
|
|
535
523
|
}
|
|
524
|
+
/** The import `sync.import` makes: into the event's board thread, or a new root in `channel`. */
|
|
525
|
+
async function importEvent(key, item, meta, linked, channel, overrides) {
|
|
526
|
+
const where = channel ?? (linked ? (await syncOf(linked))?.channel : void 0);
|
|
527
|
+
if (where === void 0) return {
|
|
528
|
+
root: null,
|
|
529
|
+
message: null
|
|
530
|
+
};
|
|
531
|
+
try {
|
|
532
|
+
return await applyIncoming(key, item, meta, ChannelPath.parse(where), SyncOverrides.parse(overrides));
|
|
533
|
+
} catch (error) {
|
|
534
|
+
if (!(error instanceof DroppedError)) throw error;
|
|
535
|
+
const thread = (await find({ thread: item.event.thread })).thread;
|
|
536
|
+
return {
|
|
537
|
+
root: thread === null ? null : (await syncOf(thread))?.root ?? null,
|
|
538
|
+
message: null
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
}
|
|
536
542
|
/**
|
|
537
543
|
* Where a platform event's writes come from, for the middleware, and how it answers
|
|
538
544
|
* there: a reply in the event's own platform thread.
|
|
@@ -652,14 +658,6 @@ function createSyncEngine(entries) {
|
|
|
652
658
|
throw new DroppedError(answer.dropped);
|
|
653
659
|
};
|
|
654
660
|
const post = async ({ attachments = [], ...input }) => {
|
|
655
|
-
const where = input.channel ?? synced?.channel ?? channel;
|
|
656
|
-
const caller = input.re === void 0 ? {
|
|
657
|
-
...me,
|
|
658
|
-
origin: {
|
|
659
|
-
...me.origin,
|
|
660
|
-
config: await configTargets(where)
|
|
661
|
-
}
|
|
662
|
-
} : me;
|
|
663
661
|
return {
|
|
664
662
|
id: await screened(await call("syncImport", {
|
|
665
663
|
op: "post",
|
|
@@ -668,7 +666,7 @@ function createSyncEngine(entries) {
|
|
|
668
666
|
...input,
|
|
669
667
|
origin: input.origin ?? null,
|
|
670
668
|
...attachments.length ? { attachments: attachments.map((file) => file.id) } : {}
|
|
671
|
-
}
|
|
669
|
+
})),
|
|
672
670
|
re: input.re ?? null
|
|
673
671
|
};
|
|
674
672
|
};
|
|
@@ -883,6 +881,24 @@ function createSyncEngine(entries) {
|
|
|
883
881
|
const set = Object.fromEntries(Object.entries(desired).filter(([key, value]) => root?.labels[key] !== value));
|
|
884
882
|
const unset = Object.keys(root?.labels ?? {}).filter((key) => !(key in desired) && (event.unset.includes(key) || overrides.unset?.includes(key) || owned(key)));
|
|
885
883
|
if (root && !root.deleted && (Object.keys(set).length || unset.length)) await setLabels(event.author, set, unset);
|
|
884
|
+
if (event.retitled && root && !root.deleted && root.from === key && root.origin?.thread === event.thread) {
|
|
885
|
+
const text = clampText(overrides.text ?? rootDraft({
|
|
886
|
+
text: event.url ? `${event.title}\n${event.url}` : event.title,
|
|
887
|
+
labels: root.labels,
|
|
888
|
+
author: event.author,
|
|
889
|
+
raw
|
|
890
|
+
}).text);
|
|
891
|
+
if (root.text !== text) await screened(await call("syncImport", {
|
|
892
|
+
op: "edit",
|
|
893
|
+
author: event.author,
|
|
894
|
+
from: key,
|
|
895
|
+
origin: {
|
|
896
|
+
thread: root.origin.thread,
|
|
897
|
+
message: root.origin.message
|
|
898
|
+
},
|
|
899
|
+
text
|
|
900
|
+
}));
|
|
901
|
+
}
|
|
886
902
|
if (event.reply) return result(await post({
|
|
887
903
|
author: event.author,
|
|
888
904
|
re: rootId,
|
|
@@ -1001,14 +1017,14 @@ function createSyncEngine(entries) {
|
|
|
1001
1017
|
return null;
|
|
1002
1018
|
}
|
|
1003
1019
|
}
|
|
1004
|
-
/** A board message as markdown for a platform, which converts it to its own format. */
|
|
1005
|
-
function render(key, target, message, actors) {
|
|
1020
|
+
/** A board message as markdown for a platform, which converts it to its own format: by `renderer`, or the provider's own. */
|
|
1021
|
+
function render(key, target, message, actors, renderer) {
|
|
1006
1022
|
const actor = actors.byId[message.by.actor];
|
|
1007
1023
|
let name = actor?.name ?? message.by.actor;
|
|
1008
1024
|
if (message.by.profile && actor?.handle.includes("/")) name = `${actor.name} via ${actor.handle.split("/")[0]}`;
|
|
1009
1025
|
if (message.from) name = `${name} (${entries[message.from]?.platform ?? message.by.actor.slice(0, message.by.actor.indexOf(":"))})`;
|
|
1010
1026
|
const platform = platformOf(key);
|
|
1011
|
-
const body = platform.render(immutable({
|
|
1027
|
+
const body = (renderer ?? ((input) => platform.render(input)))(immutable({
|
|
1012
1028
|
message: {
|
|
1013
1029
|
...structuredClone(message),
|
|
1014
1030
|
text: displayText(message.text, message.by.actor)
|
|
@@ -1045,19 +1061,19 @@ function createSyncEngine(entries) {
|
|
|
1045
1061
|
* `render`, with a line per file where the files won't follow: on a platform
|
|
1046
1062
|
* that takes no uploads, or one that refused them (`noted`).
|
|
1047
1063
|
*/
|
|
1048
|
-
const renderFor = (target, message, actors, noted = false) => {
|
|
1064
|
+
const renderFor = (target, message, actors, noted = false, renderer) => {
|
|
1049
1065
|
const key = keyOf(target);
|
|
1050
|
-
const { markdown, as } = render(key, target, message, actors);
|
|
1066
|
+
const { markdown, as } = render(key, target, message, actors, renderer);
|
|
1051
1067
|
return {
|
|
1052
1068
|
markdown: message.attachments.length > 0 && (noted || !platformOf(key).uploadsFiles) ? `${markdown}\n${attachmentNotes(message)}` : markdown,
|
|
1053
1069
|
...as ? { as } : {}
|
|
1054
1070
|
};
|
|
1055
1071
|
};
|
|
1056
|
-
async function postReply(message, target, externalThread, actors) {
|
|
1072
|
+
async function postReply(message, target, externalThread, actors, renderer) {
|
|
1057
1073
|
const replyCopy = message.replyTo === null ? void 0 : (await copiesOf([message.replyTo]))[message.replyTo]?.[target];
|
|
1058
1074
|
const place = replyCopy !== void 0 && replyCopy.thread !== externalThread ? replyCopy.thread : externalThread;
|
|
1059
1075
|
const platform = platformOf(keyOf(target));
|
|
1060
|
-
let body = renderFor(target, message, actors);
|
|
1076
|
+
let body = renderFor(target, message, actors, false, renderer);
|
|
1061
1077
|
let files;
|
|
1062
1078
|
let sentId;
|
|
1063
1079
|
if (!body.markdown.trim() && message.attachments.length && platform.uploadsFiles) try {
|
|
@@ -1066,7 +1082,7 @@ function createSyncEngine(entries) {
|
|
|
1066
1082
|
files = sent.files;
|
|
1067
1083
|
} catch (error) {
|
|
1068
1084
|
if (!platform.refusesFiles(error) && !keepsNoFiles(error)) throw error;
|
|
1069
|
-
body = renderFor(target, message, actors, true);
|
|
1085
|
+
body = renderFor(target, message, actors, true, renderer);
|
|
1070
1086
|
sentId = (await platform.post(native(place), "thread", body)).message;
|
|
1071
1087
|
files = null;
|
|
1072
1088
|
}
|
|
@@ -1124,7 +1140,7 @@ function createSyncEngine(entries) {
|
|
|
1124
1140
|
* receipt comes first, so a retry sends only what's missing. If the platform
|
|
1125
1141
|
* refuses files, the text gets a line for each; any other failure retries.
|
|
1126
1142
|
*/
|
|
1127
|
-
async function deliverFiles(message, target, copy, actors) {
|
|
1143
|
+
async function deliverFiles(message, target, copy, actors, renderer) {
|
|
1128
1144
|
const platform = platformOf(keyOf(target));
|
|
1129
1145
|
if (!platform.uploadsFiles || message.attachments.length === 0 || copy.files !== void 0) return;
|
|
1130
1146
|
let ids;
|
|
@@ -1137,7 +1153,7 @@ function createSyncEngine(entries) {
|
|
|
1137
1153
|
});
|
|
1138
1154
|
console.warn(`[board] ${target} won't take #${message.id}'s files (${describe(error)}); noting them in its text`);
|
|
1139
1155
|
ids = null;
|
|
1140
|
-
await platform.edit(native(copy.thread), copy.message, renderFor(target, message, actors, true)).catch((edit) => console.warn(`[board] couldn't note #${message.id}'s files on ${target}:`, describe(edit)));
|
|
1156
|
+
await platform.edit(native(copy.thread), copy.message, renderFor(target, message, actors, true, renderer)).catch((edit) => console.warn(`[board] couldn't note #${message.id}'s files on ${target}:`, describe(edit)));
|
|
1141
1157
|
}
|
|
1142
1158
|
await call("syncReceipt", {
|
|
1143
1159
|
kind: "files",
|
|
@@ -1146,12 +1162,20 @@ function createSyncEngine(entries) {
|
|
|
1146
1162
|
files: ids
|
|
1147
1163
|
});
|
|
1148
1164
|
}
|
|
1165
|
+
/** Records a failure once: a delivery's own and the sync function that passed it on are one. */
|
|
1166
|
+
const failed = (failures, id, target, error) => {
|
|
1167
|
+
if (!failures.some((failure) => failure.error === error)) failures.push({
|
|
1168
|
+
id,
|
|
1169
|
+
target,
|
|
1170
|
+
error
|
|
1171
|
+
});
|
|
1172
|
+
};
|
|
1149
1173
|
async function settle(attempt, failures) {
|
|
1150
1174
|
if (failures.length === 0) return;
|
|
1151
1175
|
if (attempt < DELIVERY_ATTEMPTS || afterRetry(attempt)) throw failures[0].error;
|
|
1152
1176
|
for (const { id, target, error } of failures) {
|
|
1153
|
-
console.error(`[board] gave up delivering #${id} to ${target}:`, describe(error));
|
|
1154
|
-
await call("syncReceipt", {
|
|
1177
|
+
console.error(`[board] gave up ${target === null ? `syncing #${id}` : `delivering #${id} to ${target}`}:`, describe(error));
|
|
1178
|
+
if (target !== null) await call("syncReceipt", {
|
|
1155
1179
|
kind: "failed",
|
|
1156
1180
|
message: id,
|
|
1157
1181
|
target,
|
|
@@ -1160,20 +1184,66 @@ function createSyncEngine(entries) {
|
|
|
1160
1184
|
}
|
|
1161
1185
|
}
|
|
1162
1186
|
/**
|
|
1163
|
-
*
|
|
1164
|
-
*
|
|
1187
|
+
* Hands one board event, for one target, to its entry's sync function: `send` is what its
|
|
1188
|
+
* `deliver` does, at most once. A throw, the function's or the delivery's, comes back here.
|
|
1165
1189
|
*/
|
|
1166
|
-
async function
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1190
|
+
async function offer(key, event, message, target, send) {
|
|
1191
|
+
let sent;
|
|
1192
|
+
let active = true;
|
|
1193
|
+
const finished = () => badRequest("the sync function has finished");
|
|
1194
|
+
const sync = {
|
|
1195
|
+
direction: "out",
|
|
1196
|
+
key,
|
|
1197
|
+
channel: target === null ? null : channelIn(key, target),
|
|
1198
|
+
event: immutable(structuredClone(event)),
|
|
1199
|
+
message: immutable(structuredClone(message)),
|
|
1200
|
+
target,
|
|
1201
|
+
deliver(options = {}) {
|
|
1202
|
+
if (!active) throw finished();
|
|
1203
|
+
sent ??= target === null ? Promise.resolve() : send(options.render);
|
|
1204
|
+
return sent;
|
|
1205
|
+
},
|
|
1206
|
+
start(channel) {
|
|
1207
|
+
if (!active) throw finished();
|
|
1208
|
+
return startThread(key, message, channel);
|
|
1209
|
+
},
|
|
1210
|
+
resolve: (name) => resolve(key, name)
|
|
1211
|
+
};
|
|
1212
|
+
try {
|
|
1213
|
+
await platformOf(key).sync(sync);
|
|
1214
|
+
await sent;
|
|
1215
|
+
} finally {
|
|
1216
|
+
active = false;
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
/** Syncs a message's thread with a platform channel, as a sync function asked: never the channel it came from. */
|
|
1220
|
+
async function startThread(key, message, channel) {
|
|
1221
|
+
const { target, kind } = await normalizeTarget(`${key}:${channel}`);
|
|
1222
|
+
const root = message.re === null ? message : await messageOf(message.re);
|
|
1223
|
+
if (!root || root.deleted || root.noSync) return;
|
|
1224
|
+
if (target === (root.origin ? root.origin.channel ?? channelOf(root.origin.thread) : null)) return;
|
|
1225
|
+
await call("syncReceipt", {
|
|
1170
1226
|
kind: "thread",
|
|
1171
|
-
message:
|
|
1172
|
-
target
|
|
1173
|
-
targetKind:
|
|
1227
|
+
message: root.id,
|
|
1228
|
+
target,
|
|
1229
|
+
targetKind: kind,
|
|
1174
1230
|
cause: "config"
|
|
1175
1231
|
});
|
|
1176
1232
|
}
|
|
1233
|
+
/** A new root, offered to each platform that can start threads: its sync function may `start` one there. */
|
|
1234
|
+
async function deliverRoot(event, attempt) {
|
|
1235
|
+
const starters = Object.keys(entries).filter((key) => entries[key].canStartThreads);
|
|
1236
|
+
if (event.payload.noSync || starters.length === 0) return;
|
|
1237
|
+
const root = await messageOf(event.message.id);
|
|
1238
|
+
if (!root || root.deleted) return;
|
|
1239
|
+
const failures = [];
|
|
1240
|
+
for (const key of starters) try {
|
|
1241
|
+
await offer(key, event, root, null, async () => {});
|
|
1242
|
+
} catch (error) {
|
|
1243
|
+
failed(failures, root.id, null, error);
|
|
1244
|
+
}
|
|
1245
|
+
await settle(attempt, failures);
|
|
1246
|
+
}
|
|
1177
1247
|
async function deliverThread(event, attempt) {
|
|
1178
1248
|
const { thread, target } = event.payload;
|
|
1179
1249
|
if (!ours(target)) return;
|
|
@@ -1181,12 +1251,33 @@ function createSyncEngine(entries) {
|
|
|
1181
1251
|
const synced = await syncOf(thread);
|
|
1182
1252
|
const [root, ...replies] = synced ? await threadOf(synced.root) : [];
|
|
1183
1253
|
if (!synced || !root || root.id !== synced.root || root.deleted) return;
|
|
1254
|
+
const failures = [];
|
|
1255
|
+
try {
|
|
1256
|
+
await offer(keyOf(target), event, root, target, (renderer) => backfill(synced, root, replies, target, failures, renderer));
|
|
1257
|
+
} catch (error) {
|
|
1258
|
+
failed(failures, root.id, target, error);
|
|
1259
|
+
}
|
|
1260
|
+
await settle(attempt, failures);
|
|
1261
|
+
}
|
|
1262
|
+
/**
|
|
1263
|
+
* A thread's root into a platform channel, then what's been said in it, each once. A reply
|
|
1264
|
+
* waits for its root; every reply is tried, and the first failure is thrown.
|
|
1265
|
+
*/
|
|
1266
|
+
async function backfill(synced, root, replies, target, failures, renderer) {
|
|
1184
1267
|
const actors = await actorsNow();
|
|
1185
1268
|
const copies = await copiesOf([root.id, ...replies.map((reply) => reply.id)]);
|
|
1269
|
+
const rootFailed = (error) => {
|
|
1270
|
+
failures.push({
|
|
1271
|
+
id: root.id,
|
|
1272
|
+
target,
|
|
1273
|
+
error
|
|
1274
|
+
});
|
|
1275
|
+
return error;
|
|
1276
|
+
};
|
|
1186
1277
|
let externalThread = synced.external[target];
|
|
1187
1278
|
if (!externalThread) try {
|
|
1188
1279
|
const platform = platformOf(keyOf(target));
|
|
1189
|
-
const body = renderFor(target, root, actors);
|
|
1280
|
+
const body = renderFor(target, root, actors, false, renderer);
|
|
1190
1281
|
const mediaOnly = !body.markdown.trim() && root.attachments.length > 0 && platform.uploadsFiles;
|
|
1191
1282
|
if (!body.markdown.trim() && !mediaOnly) throw badRequest("a renderer must return text unless the message has media");
|
|
1192
1283
|
let uploaded;
|
|
@@ -1210,7 +1301,7 @@ function createSyncEngine(entries) {
|
|
|
1210
1301
|
} catch (error) {
|
|
1211
1302
|
if (!platform.refusesFiles(error) && !keepsNoFiles(error)) throw error;
|
|
1212
1303
|
uploaded = null;
|
|
1213
|
-
return platform.post(native(target), "channel", renderFor(target, root, actors, true));
|
|
1304
|
+
return platform.post(native(target), "channel", renderFor(target, root, actors, true, renderer));
|
|
1214
1305
|
}
|
|
1215
1306
|
};
|
|
1216
1307
|
const sent = await post();
|
|
@@ -1222,46 +1313,39 @@ function createSyncEngine(entries) {
|
|
|
1222
1313
|
...postedAsPerson ? { asUser: true } : {}
|
|
1223
1314
|
};
|
|
1224
1315
|
await recordDelivery(root, target, external);
|
|
1225
|
-
await deliverFiles(root, target, external, actors);
|
|
1316
|
+
await deliverFiles(root, target, external, actors, renderer);
|
|
1226
1317
|
} catch (error) {
|
|
1227
|
-
|
|
1228
|
-
id: root.id,
|
|
1229
|
-
target,
|
|
1230
|
-
error
|
|
1231
|
-
}]);
|
|
1318
|
+
throw rootFailed(error);
|
|
1232
1319
|
}
|
|
1233
1320
|
else {
|
|
1234
1321
|
const copy = copies[root.id]?.[target];
|
|
1235
1322
|
if (copy) try {
|
|
1236
|
-
await deliverFiles(root, target, copy, actors);
|
|
1323
|
+
await deliverFiles(root, target, copy, actors, renderer);
|
|
1237
1324
|
} catch (error) {
|
|
1238
|
-
|
|
1239
|
-
id: root.id,
|
|
1240
|
-
target,
|
|
1241
|
-
error
|
|
1242
|
-
}]);
|
|
1325
|
+
throw rootFailed(error);
|
|
1243
1326
|
}
|
|
1244
1327
|
}
|
|
1245
|
-
const
|
|
1328
|
+
const mine = [];
|
|
1246
1329
|
for (const reply of replies) {
|
|
1247
1330
|
if (reply.deleted || reply.noSync || cameFrom(reply, target)) continue;
|
|
1248
1331
|
try {
|
|
1249
|
-
await deliverFiles(reply, target, copies[reply.id]?.[target] ?? await postReply(reply, target, externalThread, actors), actors);
|
|
1332
|
+
await deliverFiles(reply, target, copies[reply.id]?.[target] ?? await postReply(reply, target, externalThread, actors, renderer), actors, renderer);
|
|
1250
1333
|
} catch (error) {
|
|
1251
|
-
|
|
1334
|
+
mine.push({
|
|
1252
1335
|
id: reply.id,
|
|
1253
1336
|
target,
|
|
1254
1337
|
error
|
|
1255
1338
|
});
|
|
1256
1339
|
}
|
|
1257
1340
|
}
|
|
1258
|
-
|
|
1341
|
+
failures.push(...mine);
|
|
1342
|
+
if (mine.length) throw mine[0].error;
|
|
1259
1343
|
}
|
|
1260
1344
|
/** Never deliver a message back to the platform it came from. */
|
|
1261
1345
|
const cameFrom = (message, target) => message.origin?.thread === target || message.from !== null && keyOf(target) === message.from;
|
|
1262
1346
|
async function deliverPost(event, attempt) {
|
|
1263
1347
|
if (!event.message) return;
|
|
1264
|
-
if (event.message.re === null) return
|
|
1348
|
+
if (event.message.re === null) return deliverRoot(event, attempt);
|
|
1265
1349
|
if (event.payload.noSync) return;
|
|
1266
1350
|
const synced = await syncOf(event.payload.thread);
|
|
1267
1351
|
if (!synced || synced.targets.length === 0) return;
|
|
@@ -1274,13 +1358,12 @@ function createSyncEngine(entries) {
|
|
|
1274
1358
|
if (!ours(target) || !externalThread) continue;
|
|
1275
1359
|
if (cameFrom(message, target)) continue;
|
|
1276
1360
|
try {
|
|
1277
|
-
await
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
id: message.id,
|
|
1281
|
-
target,
|
|
1282
|
-
error
|
|
1361
|
+
await offer(keyOf(target), event, message, target, async (renderer) => {
|
|
1362
|
+
const copy = (await copiesOf([message.id]))[message.id]?.[target] ?? await postReply(message, target, externalThread, actors, renderer);
|
|
1363
|
+
await deliverFiles(message, target, copy, actors, renderer);
|
|
1283
1364
|
});
|
|
1365
|
+
} catch (error) {
|
|
1366
|
+
failed(failures, message.id, target, error);
|
|
1284
1367
|
}
|
|
1285
1368
|
}
|
|
1286
1369
|
await settle(attempt, failures);
|
|
@@ -1289,28 +1372,30 @@ function createSyncEngine(entries) {
|
|
|
1289
1372
|
async function deliverChange(event) {
|
|
1290
1373
|
const copies = (await copiesOf([event.payload.id]))[event.payload.id] ?? {};
|
|
1291
1374
|
if (Object.keys(copies).length === 0) return;
|
|
1292
|
-
const message = event.type === "message.edited" ? await messageOf(event.payload.id) : event.message;
|
|
1293
|
-
if (event.type === "message.edited" &&
|
|
1375
|
+
const message = event.type === "message.edited" ? await messageOf(event.payload.id) : event.message ?? await messageOf(event.payload.id);
|
|
1376
|
+
if (!message || event.type === "message.edited" && message.deleted) return;
|
|
1294
1377
|
const actors = await actorsNow();
|
|
1295
1378
|
for (const [target, copy] of Object.entries(copies)) {
|
|
1296
1379
|
if (!ours(target) || event.payload.from !== null && keyOf(target) === event.payload.from) continue;
|
|
1297
1380
|
try {
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
if (
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1381
|
+
await offer(keyOf(target), event, message, target, async (renderer) => {
|
|
1382
|
+
const platform = platformOf(keyOf(target));
|
|
1383
|
+
const thread = native(copy.thread);
|
|
1384
|
+
const theirs = copy.asUser && (event.type === "message.edited" || event.type === "message.deleted");
|
|
1385
|
+
const token = theirs ? await personToken(platform, message, false) : null;
|
|
1386
|
+
if (theirs && !token) {
|
|
1387
|
+
console.warn(`[board] skipped ${event.type} on ${target}: #${message.id} was posted as its author, and their token is gone`);
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
if (event.type === "message.edited") if (token) await platform.editAs(token, thread, copy.message, renderAsPerson(target, message, actors));
|
|
1391
|
+
else await platform.edit(thread, copy.message, renderFor(target, message, actors, copy.files === null, renderer));
|
|
1392
|
+
else if (event.type === "message.deleted") {
|
|
1393
|
+
if (token) await platform.deleteAs(token, thread, copy.message);
|
|
1394
|
+
else await platform.delete(thread, copy.message);
|
|
1395
|
+
if (copy.files?.length) await platform.deleteFiles(copy.files);
|
|
1396
|
+
} else if (event.type === "reaction.added") await platform.react(thread, copy.message, canonicalEmoji(event.payload.emoji), true);
|
|
1397
|
+
else await platform.react(thread, copy.message, canonicalEmoji(event.payload.emoji), false);
|
|
1398
|
+
});
|
|
1314
1399
|
} catch (error) {
|
|
1315
1400
|
console.warn(`[board] skipped ${event.type} on ${target}:`, error.message);
|
|
1316
1401
|
}
|
|
@@ -1339,9 +1424,11 @@ function createSyncEngine(entries) {
|
|
|
1339
1424
|
for (const [target, copy] of Object.entries(copies)) {
|
|
1340
1425
|
if (!ours(target)) continue;
|
|
1341
1426
|
try {
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1427
|
+
await offer(keyOf(target), event, message, target, async (renderer) => {
|
|
1428
|
+
const body = renderFor(target, message, actors, copy.files === null, renderer);
|
|
1429
|
+
if (body.markdown === renderFor(target, previous, actors, copy.files === null, renderer).markdown) return;
|
|
1430
|
+
await platformOf(keyOf(target)).edit(native(copy.thread), copy.message, body);
|
|
1431
|
+
});
|
|
1345
1432
|
} catch (error) {
|
|
1346
1433
|
console.warn(`[board] skipped labels.changed on ${target}:`, describe(error));
|
|
1347
1434
|
}
|
|
@@ -1432,7 +1519,6 @@ function createSyncEngine(entries) {
|
|
|
1432
1519
|
deliver,
|
|
1433
1520
|
receive: inbound,
|
|
1434
1521
|
pull,
|
|
1435
|
-
configTargets,
|
|
1436
1522
|
normalizeTarget,
|
|
1437
1523
|
findByEmail,
|
|
1438
1524
|
handleWebhook,
|