fedipod 1.45.0 → 1.48.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 -16
- package/architecture.md +6 -3
- package/gateway.md +51 -23
- package/gui.md +3 -2
- package/lib/client/c2s.mjs +2 -1
- package/lib/client/masto/index.mjs +5 -4
- package/lib/client/masto/render.mjs +4 -1
- package/lib/core/intake/activities.mjs +8 -9
- package/lib/core/intake/counts.mjs +143 -0
- package/lib/core/intake/index.mjs +4 -0
- package/lib/core/publisher/notes.mjs +4 -1
- package/lib/core/social.mjs +2 -0
- package/lib/core/storage.mjs +54 -5
- package/lib/core/wire.mjs +16 -5
- package/lib/gateway/account-agent.mjs +20 -4
- package/lib/gateway/copy.mjs +95 -5
- package/lib/gateway/front-core.mjs +4 -4
- package/lib/gateway/held-mail.mjs +11 -4
- package/lib/gateway/keeper.mjs +9 -6
- package/lib/gateway/masto-gateway.mjs +140 -40
- package/lib/gateway/private-read.mjs +10 -3
- package/lib/gateway/quiet.mjs +4 -3
- package/lib/gateway/state-api.mjs +73 -18
- package/lib/pod/notes.mjs +25 -0
- package/lib/server/embed.mjs +1 -1
- package/lib/session/README.md +2 -135
- package/lib/surface/routes/owner.mjs +2 -2
- package/lib/surface/surface.mjs +4 -3
- package/package.json +5 -3
- package/scripts/check-netlify-bundles.mjs +0 -4
- package/scripts/check-pod-calls.mjs +5 -108
- package/scripts/check-sizes.mjs +6 -3
- package/scripts/dependents.mjs +76 -0
- package/scripts/stage-site.mjs +10 -8
- package/scripts/start.mjs +2 -2
- package/scripts/test-dependents.mjs +37 -0
- package/web/admin/gateway.js +8 -24
- package/web/admin/index.html +0 -16
- package/web/admin/record.js +1 -2
- package/web/admin/upkeep.js +1 -5
- package/web/app/admin-facade.mjs +1 -1
- package/web/app/agent.mjs +27 -10
- package/web/app/copy-mode.mjs +98 -13
- package/web/app/dist/boot.js +68 -57
- package/web/app/dist/boot.js.map +4 -4
- package/web/app/dist/sw.js +583 -283
- package/web/app/dist/sw.js.map +4 -4
- package/web/app/forum-support.mjs +2 -2
- package/web/app/oidc-session.mjs +2 -2
- package/lib/session/demo.html +0 -94
- package/lib/session/fedi-account.mjs +0 -417
- package/lib/session/fedi-login.mjs +0 -123
- package/lib/session/oidc-session.mjs +0 -233
- package/lib/session/package.json +0 -49
package/lib/core/wire.mjs
CHANGED
|
@@ -70,13 +70,13 @@ export function jrd({ handle, host, actor, aliases = [], page = null }) {
|
|
|
70
70
|
// `kind: 'group'` publishes a Group rather than a Person, which is what makes
|
|
71
71
|
// Mastodon and Lemmy treat the actor as a community you join.
|
|
72
72
|
// Whether a follow will be held for the operator instead of accepted on
|
|
73
|
-
// arrival. A person's follows
|
|
74
|
-
//
|
|
75
|
-
//
|
|
73
|
+
// arrival. A person's follows are accepted unless `autoAcceptFollows: false`
|
|
74
|
+
// asks to see them; a group is open unless its operator asked to see joins.
|
|
75
|
+
// The actor document has to say
|
|
76
76
|
// the same thing, or a remote server shows "Following" for a follow that is
|
|
77
77
|
// in fact still waiting, and nothing ever tells it otherwise.
|
|
78
78
|
export function followsNeedApproval(config = {}) {
|
|
79
|
-
return !!config.approveJoins || (config.kind !== 'group' &&
|
|
79
|
+
return !!config.approveJoins || (config.kind !== 'group' && config.autoAcceptFollows === false);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
export const assertionKeyId = (urls) => urls.actor + '#ed25519-key';
|
|
@@ -654,6 +654,16 @@ export function quotePolicy(visibility) {
|
|
|
654
654
|
return { canQuote: { automaticApproval: open ? [PUBLIC] : [], manualApproval: [] } };
|
|
655
655
|
}
|
|
656
656
|
|
|
657
|
+
// How many liked a post and how many boosted it (§5.7, §5.8): the totals,
|
|
658
|
+
// with nobody named. Both sections let a collection be filtered for whoever
|
|
659
|
+
// asks, and a total with no names is how Mastodon serves them too.
|
|
660
|
+
export function reactionCounts(likes = 0, shares = 0) {
|
|
661
|
+
return {
|
|
662
|
+
likes: { type: 'Collection', totalItems: Math.max(0, Number(likes) || 0) },
|
|
663
|
+
shares: { type: 'Collection', totalItems: Math.max(0, Number(shares) || 0) },
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
|
|
657
667
|
// `also`: actors addressed without being mentioned — the parent's author on
|
|
658
668
|
// a reply. In cc but not in tag, so their server receives the post and does
|
|
659
669
|
// not notify them. A direct post names its audience in the text alone.
|
|
@@ -661,7 +671,7 @@ export function quotePolicy(visibility) {
|
|
|
661
671
|
// authorization is the quoted author's QuoteAuthorization, once granted.
|
|
662
672
|
export function noteDoc({ urls, slug, content, published, inReplyTo, attachments, mentions = [],
|
|
663
673
|
visibility = 'public', summary = null, sensitive = false, updated = null, container = null, also = [],
|
|
664
|
-
quote = null }) {
|
|
674
|
+
quote = null, likes = 0, shares = 0 }) {
|
|
665
675
|
const id = (container || urls.notes) + slug;
|
|
666
676
|
const who = mentions.map(m => m.actor);
|
|
667
677
|
const extra = visibility === 'direct' ? [] : also.filter(a => a && !who.includes(a));
|
|
@@ -676,6 +686,7 @@ export function noteDoc({ urls, slug, content, published, inReplyTo, attachments
|
|
|
676
686
|
to: addressed.to,
|
|
677
687
|
cc: addressed.cc,
|
|
678
688
|
replies: repliesId(id),
|
|
689
|
+
...reactionCounts(likes, shares),
|
|
679
690
|
interactionPolicy: quotePolicy(visibility),
|
|
680
691
|
};
|
|
681
692
|
if (quote?.id) {
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
//
|
|
5
5
|
// It works with the gateway's own pod identity and nothing else, which the
|
|
6
6
|
// owner's app named in the rules on the account's folder. The account's state
|
|
7
|
-
// is its copy at the gateway when there is one (copy.mjs),
|
|
8
|
-
//
|
|
9
|
-
// kept, never made.
|
|
7
|
+
// is its copy at the gateway when there is one (copy.mjs), made from the pod
|
|
8
|
+
// while the hold is on, and the pod when the hold is off. The signing key is
|
|
9
|
+
// read from the pod when it is needed, never kept, never made.
|
|
10
10
|
import crypto from 'node:crypto';
|
|
11
11
|
import { PodTransport } from '../pod/transport.mjs';
|
|
12
12
|
import { podBaseOfWebId } from '../pod/urls.mjs';
|
|
@@ -19,7 +19,8 @@ import { Publisher } from '../core/publisher/index.mjs';
|
|
|
19
19
|
import { Intake } from '../core/intake/index.mjs';
|
|
20
20
|
import { C2S } from '../client/c2s.mjs';
|
|
21
21
|
import { keeperSession } from './keeper-session.mjs';
|
|
22
|
-
import { CopyStorage, copyMeta, copyLease, keptBefore, GATEWAY_HOLDER } from './copy.mjs';
|
|
22
|
+
import { CopyStorage, copyMeta, copyLease, keptBefore, holdOn, GATEWAY_HOLDER } from './copy.mjs';
|
|
23
|
+
import { makeCopy } from './state-api.mjs';
|
|
23
24
|
|
|
24
25
|
const RSA_ALG = { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' };
|
|
25
26
|
|
|
@@ -58,6 +59,21 @@ export async function reachAccount(ctx, handle, rec, { log = console.log, sessio
|
|
|
58
59
|
return { remote, podFetch: (u, i) => remote.fetch(u, i), pod, root, urls: apUrls(pod, root), inCopy };
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Where this piece of work reads and writes the account, decided by a caller
|
|
64
|
+
* holding lockCopy, so the round cannot delete the copy in between: a copy
|
|
65
|
+
* that is here is used; none, while the hold is on, is made from the pod; with
|
|
66
|
+
* the hold off, the pod. Sets `at.inCopy`; returns { ok } or { ok: false,
|
|
67
|
+
* status, why }.
|
|
68
|
+
*/
|
|
69
|
+
export async function settle(ctx, handle, rec, at, { log = console.log } = {}) {
|
|
70
|
+
at.inCopy = !!(ctx.copyKv && await copyMeta(ctx.copyKv, handle));
|
|
71
|
+
if (at.inCopy || !ctx.copyKv || !holdOn(ctx)) return { ok: true };
|
|
72
|
+
const made = await makeCopy(ctx, handle, rec, log);
|
|
73
|
+
at.inCopy = made.ok;
|
|
74
|
+
return made;
|
|
75
|
+
}
|
|
76
|
+
|
|
61
77
|
/**
|
|
62
78
|
* The account's state store and lease, as `holder`: over the copy when there
|
|
63
79
|
* is one, else over the pod.
|
package/lib/gateway/copy.mjs
CHANGED
|
@@ -3,11 +3,17 @@
|
|
|
3
3
|
// While the gateway keeps an account running, the account's state documents
|
|
4
4
|
// (everything under ap-state/) live here, and every agent works from this copy:
|
|
5
5
|
// the owner's browser (through the state API, state-api.mjs), the keeper and
|
|
6
|
-
// the gateway's Mastodon API (in process).
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
6
|
+
// the gateway's Mastodon API (in process). What only the pod may hold never
|
|
7
|
+
// comes here: the signing key, the pod's own lease document, and the passwords
|
|
8
|
+
// and tokens of accounts on other servers.
|
|
9
|
+
//
|
|
10
|
+
// The hold: a person's copy is made from the pod when something needs it, and
|
|
11
|
+
// every fifteen minutes, whatever is happening, it is written to the pod and
|
|
12
|
+
// deleted (holdOver); the next use makes it again. So nothing private stays
|
|
13
|
+
// here longer than that. A gateway's admin may turn the hold off
|
|
14
|
+
// (FEDIPOD_HOLD=off): then no copy is made at all, and everything works on the
|
|
15
|
+
// pod directly. A forum's copy is kept all the time, and written to its pod
|
|
16
|
+
// every fifteen minutes (flushCopy), as before.
|
|
11
17
|
//
|
|
12
18
|
// The lease that decides which agent acts lives in the copy too, and the copy
|
|
13
19
|
// is what enforces it: a state document is written only by the lease's holder.
|
|
@@ -89,6 +95,30 @@ const readJson = async (kv, key) => {
|
|
|
89
95
|
export const keptNow = (ctx, rec) => !!(rec?.keeper && ctx.keeperWebId && (rec.keeper.webId || ctx.keeperWebId) === ctx.keeperWebId);
|
|
90
96
|
export const keptBefore = (ctx, rec) => !!(rec?.keeper?.webId && ctx.keeperWebId && rec.keeper.webId !== ctx.keeperWebId);
|
|
91
97
|
|
|
98
|
+
// A person's account, as against a forum or one of its categories.
|
|
99
|
+
export const isPersonal = (rec) => !!rec && (rec.kind || 'person') === 'person' && !rec.forum;
|
|
100
|
+
|
|
101
|
+
// The admin's setting: the hold is on unless FEDIPOD_HOLD says off.
|
|
102
|
+
export const holdFrom = (v) => !/^(off|no|false|0)$/iu.test(String(v ?? '').trim());
|
|
103
|
+
export const holdOn = (ctx) => ctx?.hold !== false;
|
|
104
|
+
|
|
105
|
+
// While the round writes a copy to the pod and deletes it: the state API asks
|
|
106
|
+
// its callers to try again in a moment, and the gateway's readers wait.
|
|
107
|
+
const CLOSING_MS = 2 * 60_000;
|
|
108
|
+
export async function closing(kv, h) {
|
|
109
|
+
const at = Number((await kv.get(`${account(h)}/closing`))?.text || 0);
|
|
110
|
+
return !!at && Date.now() - at < CLOSING_MS;
|
|
111
|
+
}
|
|
112
|
+
/** Wait, up to `waitMs`, for a copy being written and deleted to be gone. */
|
|
113
|
+
export async function waitOutClosing(kv, h, { waitMs = 10_000 } = {}) {
|
|
114
|
+
const until = Date.now() + waitMs;
|
|
115
|
+
while (await closing(kv, h)) {
|
|
116
|
+
if (Date.now() > until) return false;
|
|
117
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
118
|
+
}
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
92
122
|
/**
|
|
93
123
|
* Forget a copy its owner has written to the pod themselves: no write-back, no
|
|
94
124
|
* pod lease to free (the owner took it). Asked only by the lease's holder.
|
|
@@ -135,6 +165,12 @@ export function copyLease(kv, h, { id, log = () => {} }) {
|
|
|
135
165
|
return new Lease({ url: `copy:${account(h)}/lease`, fetchImpl: kvDocFetch(kv, `${h}/lease`), log, id });
|
|
136
166
|
}
|
|
137
167
|
|
|
168
|
+
/** Whether a browser, as against the gateway, holds the copy's lease now. */
|
|
169
|
+
export async function heldByBrowser(kv, h) {
|
|
170
|
+
const { doc } = await readJson(kv, `${account(h)}/lease`);
|
|
171
|
+
return !!doc && doc.holder !== GATEWAY_HOLDER && Date.now() < doc.expiresAt;
|
|
172
|
+
}
|
|
173
|
+
|
|
138
174
|
/** Whether `holder` holds the copy's lease now. */
|
|
139
175
|
export async function holds(kv, h, holder) {
|
|
140
176
|
const { doc } = await readJson(kv, `${account(h)}/lease`);
|
|
@@ -275,10 +311,14 @@ export async function fillCopy(kv, h, { pod, podFetch, stateUrl, webId = null, l
|
|
|
275
311
|
}
|
|
276
312
|
const undo = async (why) => {
|
|
277
313
|
for (const b of await kv.list(`${h}/d/`).catch(() => [])) await kv.delete(b.key).catch(() => {});
|
|
314
|
+
await kv.delete(`_copies/${h}`).catch(() => {});
|
|
278
315
|
await podLease.release().catch(() => {});
|
|
279
316
|
return { ok: false, why };
|
|
280
317
|
};
|
|
281
318
|
try {
|
|
319
|
+
// Listed from the start, so a making cut off part way is found by the
|
|
320
|
+
// round and its documents deleted (clearLeftover).
|
|
321
|
+
await kv.set(`_copies/${h}`, String(Date.now()));
|
|
282
322
|
// Whatever an interrupted give-up left behind is not this copy.
|
|
283
323
|
for (const b of await kv.list(`${h}/d/`)) await kv.delete(b.key);
|
|
284
324
|
const listing = await pod.list('');
|
|
@@ -388,3 +428,53 @@ export async function dropCopy(kv, h, { pod, podFetch, stateUrl, log = () => {}
|
|
|
388
428
|
log(`copy @${h}: given up; the pod holds everything`);
|
|
389
429
|
return { ok: true };
|
|
390
430
|
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* The end of the hold, for a person's copy: everything written to the pod, the
|
|
434
|
+
* pod's lease freed, and the copy deleted. Unlike giving the copy up
|
|
435
|
+
* (dropCopy), the copy's lease stays, so the browser holding it goes on holding
|
|
436
|
+
* it in the copy made next. Asked by the round, which holds lockCopy. Writes
|
|
437
|
+
* that reach the state API meanwhile are asked to come again (closing), and
|
|
438
|
+
* the next one finds the copy made again from the pod. Returns { ok } or
|
|
439
|
+
* { ok: false, why }; nothing is deleted unless the pod has it all.
|
|
440
|
+
*/
|
|
441
|
+
export async function holdOver(kv, h, { pod, podFetch, stateUrl, log = () => {} }) {
|
|
442
|
+
const meta = await copyMeta(kv, h);
|
|
443
|
+
if (!meta) return { ok: true, already: true };
|
|
444
|
+
await kv.set(`${account(h)}/closing`, String(Date.now()));
|
|
445
|
+
try {
|
|
446
|
+
// Again, twice at most, for a write that landed while the first went: the
|
|
447
|
+
// marker stops any more coming.
|
|
448
|
+
let behind = [];
|
|
449
|
+
for (let i = 0; i < 3; i++) {
|
|
450
|
+
await flushCopy(kv, h, { pod, log });
|
|
451
|
+
const { doc: flushed } = await readJson(kv, `${h}/flushed`);
|
|
452
|
+
behind = (await kv.list(`${h}/d/`)).filter((b) => flushed?.[b.key.slice(`${h}/d/`.length)]?.etag !== b.etag);
|
|
453
|
+
if (!behind.length) break;
|
|
454
|
+
}
|
|
455
|
+
if (behind.length) return { ok: false, why: `${behind.length} document(s) could not be written to the pod` };
|
|
456
|
+
const podLease = new Lease({ url: (stateUrl || meta.stateUrl) + 'lease.json', fetchImpl: podFetch, log, id: POD_HOLDER });
|
|
457
|
+
try { await podLease.write({ holder: POD_HOLDER, expiresAt: 0 }); } catch (e) {
|
|
458
|
+
return { ok: false, why: `the pod's lease could not be freed (${e.message})` };
|
|
459
|
+
}
|
|
460
|
+
// The copy is gone the moment its meta is. Its listing goes last, so a
|
|
461
|
+
// delete cut off part way is found and finished by the next round
|
|
462
|
+
// (clearLeftover).
|
|
463
|
+
await kv.delete(`${h}/meta`);
|
|
464
|
+
await kv.delete(`${h}/flushed`);
|
|
465
|
+
for (const b of await kv.list(`${h}/d/`)) await kv.delete(b.key);
|
|
466
|
+
await kv.delete(`_copies/${h}`);
|
|
467
|
+
log(`copy @${h}: written to the pod and deleted (the hold)`);
|
|
468
|
+
return { ok: true };
|
|
469
|
+
} finally { await kv.delete(`${h}/closing`).catch(() => {}); }
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/** A copy listed with no meta is one whose delete was cut off: its documents go. */
|
|
473
|
+
export async function clearLeftover(kv, h, { log = () => {} } = {}) {
|
|
474
|
+
if (await copyMeta(kv, h)) return false;
|
|
475
|
+
const left = await kv.list(`${account(h)}/d/`);
|
|
476
|
+
for (const b of left) await kv.delete(b.key);
|
|
477
|
+
await kv.delete(`_copies/${h}`);
|
|
478
|
+
if (left.length) log(`copy @${h}: ${left.length} document(s) left by a cut-off delete, deleted`);
|
|
479
|
+
return true;
|
|
480
|
+
}
|
|
@@ -23,7 +23,7 @@ import * as podRoot from '../pod/root.mjs';
|
|
|
23
23
|
import * as podPolicy from '../pod/policy.mjs';
|
|
24
24
|
import { podBaseOfWebId } from '../pod/urls.mjs';
|
|
25
25
|
import { routeQuietApi, noteOpened, noteReceived, closedState, accountState, closedAnswer, provedOwner } from './quiet.mjs';
|
|
26
|
-
import { holdsMail, isPresent, holdingPut, afterHeld, routeHereApi, routeKeeperApi } from './held-mail.mjs';
|
|
26
|
+
import { holdsMail, mayBeAway, isPresent, holdingPut, afterHeld, routeHereApi, routeKeeperApi } from './held-mail.mjs';
|
|
27
27
|
import { routeNoticesApi } from './notices.mjs';
|
|
28
28
|
import { withSecurityHeaders } from './headers.mjs';
|
|
29
29
|
import { podTokenVerifier } from './caches.mjs';
|
|
@@ -825,15 +825,15 @@ async function route(request, ctx) {
|
|
|
825
825
|
if (closed) { console.log(`door @${up.handle}: delivery → 410 (closed)`); return gone(doorCors, CLOSED); }
|
|
826
826
|
const policy = await policyFor(rec, ctx.fetchImpl || fetch);
|
|
827
827
|
const standing = await accountState(ctx, up.handle, rec);
|
|
828
|
-
// A browser account's mail waits here while its app is closed (held-mail.mjs).
|
|
829
|
-
const
|
|
828
|
+
// A browser account's mail waits here while its app is closed (held-mail.mjs), unless the hold is off.
|
|
829
|
+
const away = mayBeAway(ctx, rec) && !(await isPresent(ctx, up.handle)); const held = away && holdsMail(ctx, rec);
|
|
830
830
|
const { status, reason, content, bytes, type, notifies, name, raw, receipt } = await handleDelivery(request, identFor(rec, policy),
|
|
831
831
|
{ podPut: held ? holdingPut(ctx, up.handle, rec) : (u, b, ct) => ctx.podPut(up.handle, u, b, ct),
|
|
832
832
|
fetchImpl: ctx.fetchImpl, paused: standing.paused, origin: ctx.frontOrigin || null });
|
|
833
833
|
// One line per delivery, so "did it arrive at the door" has an answer.
|
|
834
834
|
console.log(`door @${up.handle}: delivery → ${status} (${reason}${held && status === 202 ? ', held' : ''})`);
|
|
835
835
|
if (status === 202 && content) await noteReceived(ctx, up.handle, rec, bytes);
|
|
836
|
-
if (
|
|
836
|
+
if (away && status === 202) await afterHeld(ctx, up.handle, rec, { type, notifies });
|
|
837
837
|
// A forum fedipod.net keeps: the delivery is placed now, in this request,
|
|
838
838
|
// and the run that carries it is started. Whatever could not be placed
|
|
839
839
|
// here is still in the inbox for that run.
|
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
// app is closed nobody reads it, so the door keeps it here instead
|
|
7
7
|
// (ctx.holdMail), and writes it to the pod inbox as one document when the app
|
|
8
8
|
// says it is open (POST /api/here) or when the timer comes round (flushAll).
|
|
9
|
-
// While the app is open, mail goes straight to the pod as it always has.
|
|
9
|
+
// While the app is open, mail goes straight to the pod as it always has. With
|
|
10
|
+
// the hold off (copy.mjs: FEDIPOD_HOLD), nothing is held: mail goes straight
|
|
11
|
+
// to the pod whether the app is open or not.
|
|
10
12
|
//
|
|
11
13
|
// A batch is plain JSON, `{ "batch": [ { name, body, receipt } ] }`: `name` is
|
|
12
14
|
// what the item would have been called in the inbox, `body` its bytes, and
|
|
@@ -23,7 +25,7 @@
|
|
|
23
25
|
// presentAt(handle) -> ms when it last said so
|
|
24
26
|
// noteNext(handle, at, {earliest}) when the account's keeper next has work
|
|
25
27
|
|
|
26
|
-
import { copyMeta, keptNow } from './copy.mjs';
|
|
28
|
+
import { copyMeta, keptNow, holdOn } from './copy.mjs';
|
|
27
29
|
|
|
28
30
|
// The app says it is open every five minutes; a little over that counts.
|
|
29
31
|
export const PRESENT_MS = 8 * 60_000;
|
|
@@ -37,7 +39,11 @@ const inboxOf = (rec) => rec.inboxUrl || rec.podHome + 'ap/inbox/';
|
|
|
37
39
|
// Only a browser account is held: its owner signs in (so the row has
|
|
38
40
|
// `openedAt`), and its app says when it is open. An account run by a DeviceAgent
|
|
39
41
|
// drains whenever mail lands, and keeps its mail as it always has.
|
|
40
|
-
export const holdsMail = (ctx, rec) => !!(ctx.holdMail && rec?.openedAt);
|
|
42
|
+
export const holdsMail = (ctx, rec) => !!(ctx.holdMail && rec?.openedAt && holdOn(ctx));
|
|
43
|
+
// A browser account whose app may be closed: its mail is held, or with the
|
|
44
|
+
// hold off, sent to the pod, and either way a follow or a notification starts
|
|
45
|
+
// the work that answers it (afterHeld).
|
|
46
|
+
export const mayBeAway = (ctx, rec) => !!(ctx.holdMail && rec?.openedAt);
|
|
41
47
|
|
|
42
48
|
// Asked on every delivery, so it is remembered a minute per running copy.
|
|
43
49
|
const seen = new Map(); // handle -> { at, asked }
|
|
@@ -204,5 +210,6 @@ export async function routeHereApi(request, pathname, ctx, deps) {
|
|
|
204
210
|
if (body.nextAt !== undefined && ctx.noteNext) await ctx.noteNext(handle, body.nextAt || null).catch(() => {});
|
|
205
211
|
const flushed = await flushHeld(ctx, handle, rec)
|
|
206
212
|
.catch((e) => { console.log(`here @${handle}: held mail not delivered: ${e?.message || e}`); return 0; });
|
|
207
|
-
|
|
213
|
+
// `hold`: with it off, a browser working from a copy goes back to the pod.
|
|
214
|
+
return j(200, { ok: true, handle, flushed, hold: holdOn(ctx) });
|
|
208
215
|
}
|
package/lib/gateway/keeper.mjs
CHANGED
|
@@ -18,7 +18,7 @@ import { keeperSession } from './keeper-session.mjs';
|
|
|
18
18
|
import { flushHeld, heldEntries } from './held-mail.mjs';
|
|
19
19
|
import { publishDue, nextDue } from '../core/scheduled.mjs';
|
|
20
20
|
import { lockCopy } from './copy.mjs';
|
|
21
|
-
import { reachAccount, stateAndLease, actingAgent } from './account-agent.mjs';
|
|
21
|
+
import { reachAccount, settle, stateAndLease, actingAgent } from './account-agent.mjs';
|
|
22
22
|
|
|
23
23
|
export { keeperSession };
|
|
24
24
|
|
|
@@ -27,10 +27,11 @@ export { keeperSession };
|
|
|
27
27
|
* keeperCredential and the held-mail stores. `session` may be handed in (a
|
|
28
28
|
* test does); otherwise the keeper's grant is used. Returns what happened.
|
|
29
29
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
30
|
+
* While the hold is on, the account is worked on in its copy (copy.mjs), made
|
|
31
|
+
* from the pod if there is none: its held mail is read straight into the
|
|
32
|
+
* copy, and the pod is written by the round, not here. With the hold off, it
|
|
33
|
+
* is worked on in the pod, and any held mail goes to the pod inbox first, so
|
|
34
|
+
* the drain reads it.
|
|
34
35
|
*/
|
|
35
36
|
export async function keepOnce(ctx, handle, rec, { log = console.log, session = null } = {}) {
|
|
36
37
|
// A forum, or one of its categories, is run whole by the forum's own code
|
|
@@ -40,9 +41,11 @@ export async function keepOnce(ctx, handle, rec, { log = console.log, session =
|
|
|
40
41
|
const at = await reachAccount(ctx, handle, rec, { log, session });
|
|
41
42
|
if (at.skipped) return { skipped: at.skipped };
|
|
42
43
|
|
|
43
|
-
const unlock =
|
|
44
|
+
const unlock = ctx.copyKv ? await lockCopy(ctx.copyKv, handle) : async () => {};
|
|
44
45
|
if (!unlock) return { skipped: 'the gateway is busy with this account', retry: 'soon' };
|
|
45
46
|
try {
|
|
47
|
+
const settled = await settle(ctx, handle, rec, at, { log });
|
|
48
|
+
if (!settled.ok) return { skipped: `the account's copy could not be made: ${settled.why}`, retry: 'soon' };
|
|
46
49
|
// Held mail goes in first, so this drain reads it.
|
|
47
50
|
const flushed = !at.inCopy && ctx.listHeld
|
|
48
51
|
? await flushHeld(ctx, handle, rec).catch((e) => { say(`held mail not delivered: ${e.message}`); return 0; }) : 0;
|