fedipod-bb 0.4.0 → 0.4.2
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 +3 -4
- package/package.json +9 -3
- package/site/bb.js +4 -4
- package/site/read.mjs +48 -3
- package/src/gateway.mjs +74 -16
- package/src/keep.mjs +13 -5
- package/src/publish.mjs +2 -0
- package/src/setup.mjs +8 -5
package/README.md
CHANGED
|
@@ -12,11 +12,10 @@ the forum from a moderator's machine, and the page that shows the forum.
|
|
|
12
12
|
A moderator's console is part of that page: the queue of reports, held posts
|
|
13
13
|
and asks, and the settings.
|
|
14
14
|
The queue shows what needs a moderator: a report, a held post, a join
|
|
15
|
-
request, and a request of a moderator's that did not take.
|
|
16
|
-
`claude/plans/fedipod-bb.md` at the repository root.
|
|
15
|
+
request, and a request of a moderator's that did not take.
|
|
17
16
|
|
|
18
17
|
```
|
|
19
|
-
|
|
18
|
+
npm test
|
|
20
19
|
```
|
|
21
20
|
|
|
22
21
|
## Hosting a forum
|
|
@@ -181,7 +180,7 @@ category's address; a reader with nothing is pointed at sign-up.
|
|
|
181
180
|
## Attaching to a Gateway
|
|
182
181
|
|
|
183
182
|
```
|
|
184
|
-
|
|
183
|
+
fedipod-bb attach --home DIR --front https://fedipod.net
|
|
185
184
|
```
|
|
186
185
|
|
|
187
186
|
Takes `@<handle>@fedipod.net` for the forum and one address per category,
|
package/package.json
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fedipod-bb",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "FediPod-BB: a forum website whose record lives on a Solid pod.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"author": "Jeff Zucker",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/jeff-zucker/fedipod-bb.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/jeff-zucker/fedipod-bb#readme",
|
|
12
|
+
"bugs": "https://github.com/jeff-zucker/fedipod-bb/issues",
|
|
6
13
|
"type": "module",
|
|
7
14
|
"main": "src/index.mjs",
|
|
8
15
|
"exports": {
|
|
@@ -23,11 +30,10 @@
|
|
|
23
30
|
"fep-draft.md"
|
|
24
31
|
],
|
|
25
32
|
"scripts": {
|
|
26
|
-
"pretest": "node scripts/link-fedipod.mjs",
|
|
27
33
|
"test": "node --test test/*.test.mjs"
|
|
28
34
|
},
|
|
29
35
|
"dependencies": {
|
|
30
|
-
"fedipod": "^1.
|
|
36
|
+
"fedipod": "^1.50.2",
|
|
31
37
|
"fediverse-session": "^0.2.1"
|
|
32
38
|
}
|
|
33
39
|
}
|
package/site/bb.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// https://bb.<site>/<handle>/ the same forum at its own address
|
|
11
11
|
// #/ #/c/<slug> #/t/<slug>/<tid>
|
|
12
12
|
|
|
13
|
-
import { reader, placeOf, authorLabel, cacheKey, categoryBase } from './read.mjs';
|
|
13
|
+
import { reader, placeOf, authorLabel, cacheKey, categoryBase, cleanHtml } from './read.mjs';
|
|
14
14
|
import { MastoLogin, hostOfHandle, serverKind, actorOfHandle } from './masto.mjs';
|
|
15
15
|
import * as pod from './pod.mjs';
|
|
16
16
|
import { mine as keptByReader } from './mine.mjs';
|
|
@@ -70,7 +70,7 @@ const handleOf = (cat) => {
|
|
|
70
70
|
// A post names the category it is for, and every reader of this page is
|
|
71
71
|
// already in it: the address is how it got here, not something to read.
|
|
72
72
|
function body(p, cat) {
|
|
73
|
-
const html = p?.content
|
|
73
|
+
const html = cleanHtml(p?.content);
|
|
74
74
|
if (!cat) return html;
|
|
75
75
|
const d = document.createElement('div');
|
|
76
76
|
d.innerHTML = html;
|
|
@@ -297,7 +297,7 @@ async function showReplies() {
|
|
|
297
297
|
const href = cat && tid ? `#/t/${esc(cat.slug)}/${esc(tid)}/${esc(await cacheKey(p.id))}` : '#/';
|
|
298
298
|
items.push(`<li><a class="title" href="${href}">${esc(p.topicName || 'Topic')}</a>
|
|
299
299
|
<div class="meta">${who ? esc(who.handle) : esc(authorLabel(p.author || ''))} · ${esc(when(p.published))}</div>
|
|
300
|
-
<article class="post"><div class="body">${p.content
|
|
300
|
+
<article class="post"><div class="body">${cleanHtml(p.content)}</div></article></li>`);
|
|
301
301
|
}
|
|
302
302
|
$('main').innerHTML = `<ul class="list">${items.join('')}</ul>`;
|
|
303
303
|
}
|
|
@@ -391,7 +391,7 @@ async function showWho(handleOrId) {
|
|
|
391
391
|
const href = c && tid ? `#/t/${esc(c.slug)}/${esc(tid)}/${esc(await cacheKey(p.id))}` : '#/';
|
|
392
392
|
items.push(`<li><a class="title" href="${href}">${esc(p.topicName || 'Topic')}</a>
|
|
393
393
|
<div class="meta">${c ? esc(c.name) + ' · ' : ''}${esc(when(p.published))} · ▲ ${p.likes || 0} ▼ ${p.dislikes || 0}</div>
|
|
394
|
-
<article class="post"><div class="body">${p.content
|
|
394
|
+
<article class="post"><div class="body">${cleanHtml(p.content)}</div></article></li>`);
|
|
395
395
|
}
|
|
396
396
|
$('main').innerHTML = head + `<ul class="list">${items.join('')}</ul>`;
|
|
397
397
|
}
|
package/site/read.mjs
CHANGED
|
@@ -10,6 +10,51 @@ const MAX_PAGES = 50;
|
|
|
10
10
|
|
|
11
11
|
export const categoryBase = (actorId) => String(actorId).replace(/ap\/actor$/u, '');
|
|
12
12
|
|
|
13
|
+
// A post's text, one copy: a server that sends it once per language hands
|
|
14
|
+
// over a list, read as its first.
|
|
15
|
+
export function textOf(v) {
|
|
16
|
+
if (typeof v === 'string') return v;
|
|
17
|
+
if (Array.isArray(v)) return v.find(s => typeof s === 'string') || '';
|
|
18
|
+
return '';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// A post's text as the page shows it: only the tags and attributes a post may
|
|
22
|
+
// carry (the list the forum's host keeps when it stores one), built back up
|
|
23
|
+
// from a parse so nothing else reaches the page. DOMParser's parse is inert:
|
|
24
|
+
// it runs no script and loads no image. `parse` is for tests.
|
|
25
|
+
const KEEP = new Set(['p', 'br', 'a', 'span', 'em', 'strong', 'b', 'i', 'u', 'del',
|
|
26
|
+
'code', 'pre', 'blockquote', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4']);
|
|
27
|
+
const ATTRS = new Set(['href', 'class', 'lang', 'title']);
|
|
28
|
+
const DROP = new Set(['script', 'style', 'textarea', 'noscript', 'template', 'title', 'head']);
|
|
29
|
+
const escText = (s) => String(s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
|
30
|
+
const linkable = (href) => { try { return ['http:', 'https:', 'mailto:'].includes(new URL(href).protocol); } catch { return false; } };
|
|
31
|
+
const inert = (html) => new DOMParser().parseFromString(html, 'text/html').body;
|
|
32
|
+
|
|
33
|
+
function rebuild(node) {
|
|
34
|
+
let out = '';
|
|
35
|
+
for (const n of node.childNodes) {
|
|
36
|
+
if (n.nodeType === 3) { out += escText(n.nodeValue); continue; }
|
|
37
|
+
if (n.nodeType !== 1) continue;
|
|
38
|
+
const tag = n.nodeName.toLowerCase();
|
|
39
|
+
if (DROP.has(tag)) continue;
|
|
40
|
+
if (!KEEP.has(tag)) { out += rebuild(n); continue; }
|
|
41
|
+
let attrs = '';
|
|
42
|
+
for (const a of n.attributes) {
|
|
43
|
+
const name = a.name.toLowerCase();
|
|
44
|
+
if (!ATTRS.has(name) || (name === 'href' && (tag !== 'a' || !linkable(a.value)))) continue;
|
|
45
|
+
attrs += ` ${name}="${escText(a.value)}"`;
|
|
46
|
+
}
|
|
47
|
+
if (tag === 'a') attrs += ' rel="nofollow noopener noreferrer"';
|
|
48
|
+
out += tag === 'br' ? '<br>' : `<${tag}${attrs}>${rebuild(n)}</${tag}>`;
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function cleanHtml(html, parse = inert) {
|
|
54
|
+
const text = textOf(html);
|
|
55
|
+
return text ? rebuild(parse(text)) : '';
|
|
56
|
+
}
|
|
57
|
+
|
|
13
58
|
// Where a forum is read from: through a Gateway at `<origin>/u/<handle>/`, or
|
|
14
59
|
// a pod home given outright.
|
|
15
60
|
export const forumBase = ({ origin = null, handle = null, pod = null } = {}) => {
|
|
@@ -213,8 +258,8 @@ export function reader({ fetch: f = globalThis.fetch.bind(globalThis), session =
|
|
|
213
258
|
id: copy.id || url,
|
|
214
259
|
|
|
215
260
|
author: idOf([].concat(copy.attributedTo || [])[0]) || null,
|
|
216
|
-
name: copy.name || null,
|
|
217
|
-
content:
|
|
261
|
+
name: textOf(copy.name) || null,
|
|
262
|
+
content: textOf(copy.content),
|
|
218
263
|
published: copy.published || null,
|
|
219
264
|
topic: idOf(copy.context) || null,
|
|
220
265
|
inReplyTo: idOf(copy.inReplyTo) || null,
|
|
@@ -278,7 +323,7 @@ export function reader({ fetch: f = globalThis.fetch.bind(globalThis), session =
|
|
|
278
323
|
return {
|
|
279
324
|
id: copy.id || postId, type: copy.type || 'Note', gone: copy.type === 'Tombstone', page,
|
|
280
325
|
author: idOf([].concat(copy.attributedTo || [])[0]) || null,
|
|
281
|
-
name: copy.name || null, content:
|
|
326
|
+
name: textOf(copy.name) || null, content: textOf(copy.content),
|
|
282
327
|
// What its author typed, when they said so: an edit reopens this
|
|
283
328
|
// rather than guessing it back out of the HTML.
|
|
284
329
|
source: copy.source?.mediaType === 'text/markdown' && typeof copy.source?.content === 'string' ? copy.source.content : null,
|
package/src/gateway.mjs
CHANGED
|
@@ -27,7 +27,9 @@ export const isForumRow = (rec) => !!rec?.inboxUrl && (rec.kind === 'group' || r
|
|
|
27
27
|
export async function forumFor(ctx, handle, rec) {
|
|
28
28
|
const forumHandle = rec.kind === 'application' ? handle : rec.forum;
|
|
29
29
|
if (!forumHandle) return { skipped: 'the category names no forum; run `fedipod-bb keep` again' };
|
|
30
|
-
|
|
30
|
+
// Read fresh where the front offers it: the forum's row is switched on
|
|
31
|
+
// last, and its first run starts in the same breath.
|
|
32
|
+
const forumRec = forumHandle === handle ? rec : await (ctx.lookupFresh || ctx.lookup)(forumHandle);
|
|
31
33
|
if (!forumRec || !isForumRow(forumRec) || !forumRec.keeper) return { skipped: `@${forumHandle} is not a forum this gateway keeps` };
|
|
32
34
|
if (!forumRec.webId || !forumRec.podHome) return { skipped: 'the forum row names no owner or pod' };
|
|
33
35
|
const home = forumRec.podHome.replace(/\/?$/u, '/');
|
|
@@ -89,7 +91,7 @@ async function readConfig(ctx, f, podFetch) {
|
|
|
89
91
|
export async function ensureForumCopies(ctx, f, config, log) {
|
|
90
92
|
const rows = [[f.forumHandle, f.forumRec]];
|
|
91
93
|
for (const c of config?.categories || []) {
|
|
92
|
-
const r = await ctx.lookup(c.slug);
|
|
94
|
+
const r = await (ctx.lookupFresh || ctx.lookup)(c.slug);
|
|
93
95
|
if (r && isForumRow(r)) rows.push([c.slug, r]);
|
|
94
96
|
}
|
|
95
97
|
const have = new Set();
|
|
@@ -139,6 +141,33 @@ function nextRun(forum, asksPending) {
|
|
|
139
141
|
* Returns what the keeper's bookkeeping expects: { drained, waiting, nextAt }
|
|
140
142
|
* or { skipped, retry }.
|
|
141
143
|
*/
|
|
144
|
+
// A queued delivery's identity: where it goes and what it carries.
|
|
145
|
+
const queueKey = (item) => `${item.inbox}\u0000${typeof item.activity === 'string' ? item.activity : JSON.stringify(item.activity)}`;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The queue after a carry that ran with nobody holding the forum: `before`
|
|
149
|
+
* is what the run took out to carry, `after` what the carry left of it
|
|
150
|
+
* (delivered items gone, failed ones with their next time), `fresh` the
|
|
151
|
+
* queue as the copy holds it now, which the door may have added to in the
|
|
152
|
+
* meantime. A door only ever appends, so: an item the run took out is kept
|
|
153
|
+
* as the carry left it or dropped if delivered; anything else is the door's
|
|
154
|
+
* and stays.
|
|
155
|
+
*/
|
|
156
|
+
export function mergeQueues(before, after, fresh) {
|
|
157
|
+
const taken = new Set(before.map(queueKey));
|
|
158
|
+
const left = new Map(after.map((i) => [queueKey(i), i]));
|
|
159
|
+
const out = [];
|
|
160
|
+
for (const item of fresh) {
|
|
161
|
+
const k = queueKey(item);
|
|
162
|
+
if (!taken.has(k)) { out.push(item); continue; }
|
|
163
|
+
if (left.has(k)) out.push(left.get(k));
|
|
164
|
+
}
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const storesOf = (forum) => [forum.store, ...forum.categories.map((c) => c.store)];
|
|
169
|
+
const deliverersOf = (forum) => [...forum.categories.map((c) => c.deliverer), forum.siteAgent.deliverer];
|
|
170
|
+
|
|
142
171
|
export async function keepOnce(ctx, handle, rec, { log = console.log, session = null, remote: given = null, prepare = null } = {}) {
|
|
143
172
|
const f = await forumFor(ctx, handle, rec);
|
|
144
173
|
if (f.skipped) return { skipped: f.skipped };
|
|
@@ -149,10 +178,19 @@ export async function keepOnce(ctx, handle, rec, { log = console.log, session =
|
|
|
149
178
|
const config = await readConfig(ctx, f, podFetch);
|
|
150
179
|
if (!config) return { skipped: 'no forum on the pod' };
|
|
151
180
|
const copies = await ensureForumCopies(ctx, f, config, say);
|
|
181
|
+
// The forum is held only while its state changes: the drain, the asks, the
|
|
182
|
+
// queue, the heartbeat, the write to the copies. The carry — one signed
|
|
183
|
+
// POST per follower server, each with its own timeout — runs with the forum
|
|
184
|
+
// free, so a post arriving at the door meanwhile is placed at once instead
|
|
185
|
+
// of waiting for the run to end. The queue the carry changes is written
|
|
186
|
+
// back under the lock, merged with whatever the door queued in between.
|
|
152
187
|
const unlock = await lockCopy(ctx.copyKv, f.forumHandle);
|
|
153
188
|
if (!unlock) return { skipped: 'the gateway is busy with this forum', retry: 'soon' };
|
|
189
|
+
let forum;
|
|
190
|
+
let before;
|
|
191
|
+
let asks = 0;
|
|
154
192
|
try {
|
|
155
|
-
|
|
193
|
+
forum = agentFor(ctx, f, { remote, podFetch, copies, log: say, mode: 'run' });
|
|
156
194
|
if (!await forum.connect({ act: false })) return { skipped: 'no forum on the pod' };
|
|
157
195
|
prepare?.(forum); // a test's origins, in place of the network
|
|
158
196
|
if (!await forum.lease.acquire()) return { skipped: 'another host has the forum', retry: 'soon' };
|
|
@@ -161,22 +199,42 @@ export async function keepOnce(ctx, handle, rec, { log = console.log, session =
|
|
|
161
199
|
await forum.intake.drain();
|
|
162
200
|
await forum.applyVerifiedAsks().catch((e) => say(`asks: ${e.message}`));
|
|
163
201
|
await forum.publishModQueue().catch((e) => say(`queue: ${e.message}`));
|
|
164
|
-
for (const cat of forum.categories) await cat.deliverer.drainQueue();
|
|
165
|
-
await forum.siteAgent.deliverer.drainQueue();
|
|
166
202
|
await publish.publishHeartbeat(forum.siteAgent).catch((e) => say(`heartbeat: ${e.message}`));
|
|
167
|
-
await Promise.allSettled(
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
// The run wrote on the pod (what the door could not place, the queue,
|
|
171
|
-
// the heartbeat): the edge's copies of the forum's documents go.
|
|
172
|
-
if (ctx.purge) {
|
|
173
|
-
const tags = [`u-${f.forumHandle}`, ...(config.categories || []).map((c) => `u-${c.slug}`)];
|
|
174
|
-
await ctx.purge(tags).catch((e) => say(`purge: ${e.message}`));
|
|
175
|
-
}
|
|
176
|
-
say(`done: inbox drained, ${waiting} delivery(ies) waiting, ${asks} ask(s) pending`);
|
|
177
|
-
return { drained: true, waiting, nextAt: nextRun(forum, asks) };
|
|
203
|
+
await Promise.allSettled(storesOf(forum).map((s) => s.flush()));
|
|
204
|
+
asks = storesOf(forum).reduce((n, s) => n + s.read('modqueue.json', []).length, 0);
|
|
205
|
+
before = new Map(storesOf(forum).map((s) => [s, s.getQueue?.() || []]));
|
|
178
206
|
} finally { await forum.lease.release().catch(() => {}); }
|
|
179
207
|
} finally { await unlock(); }
|
|
208
|
+
|
|
209
|
+
// The carry, with the forum free.
|
|
210
|
+
for (const d of deliverersOf(forum)) await d.drainQueue().catch((e) => say(`carry: ${e.message}`));
|
|
211
|
+
const after = new Map(storesOf(forum).map((s) => [s, s.getQueue?.() || []]));
|
|
212
|
+
|
|
213
|
+
// What the carry left, written back under the lock beside what the door
|
|
214
|
+
// queued meanwhile. The door holds the forum for seconds, so the wait is short.
|
|
215
|
+
const relock = await lockCopy(ctx.copyKv, f.forumHandle, { waitMs: 20_000 });
|
|
216
|
+
if (!relock) { say('the queue could not be written back: the forum stayed busy'); return { drained: true, waiting: null, retry: 'soon' }; }
|
|
217
|
+
let waiting = 0;
|
|
218
|
+
try {
|
|
219
|
+
// The copy takes a write only from the lease's holder; the lease again, briefly.
|
|
220
|
+
if (!await forum.lease.acquire()) { say('the queue could not be written back: another host has the forum'); return { drained: true, waiting: null, retry: 'soon' }; }
|
|
221
|
+
for (const s of storesOf(forum)) {
|
|
222
|
+
let fresh = null;
|
|
223
|
+
try { await s.load(); fresh = s.getQueue?.() || []; } catch { fresh = null; }
|
|
224
|
+
const merged = fresh ? mergeQueues(before.get(s) || [], after.get(s) || [], fresh) : (after.get(s) || []);
|
|
225
|
+
s.setQueue?.(merged);
|
|
226
|
+
waiting += merged.length;
|
|
227
|
+
}
|
|
228
|
+
await Promise.allSettled(storesOf(forum).map((s) => s.flush()));
|
|
229
|
+
} finally { await forum.lease.release().catch(() => {}); await relock(); }
|
|
230
|
+
// The run wrote on the pod (what the door could not place, the queue, the
|
|
231
|
+
// heartbeat): the edge's copies of the forum's documents go.
|
|
232
|
+
if (ctx.purge) {
|
|
233
|
+
const tags = [`u-${f.forumHandle}`, ...(config.categories || []).map((c) => `u-${c.slug}`)];
|
|
234
|
+
await ctx.purge(tags).catch((e) => say(`purge: ${e.message}`));
|
|
235
|
+
}
|
|
236
|
+
say(`done: inbox drained, ${waiting} delivery(ies) waiting, ${asks} ask(s) pending`);
|
|
237
|
+
return { drained: true, waiting, nextAt: nextRun(forum, asks) };
|
|
180
238
|
}
|
|
181
239
|
|
|
182
240
|
/**
|
package/src/keep.mjs
CHANGED
|
@@ -12,11 +12,19 @@ import { keepRows } from './setup.mjs';
|
|
|
12
12
|
// does, from the pod's own rules, which name it beside the owner from here
|
|
13
13
|
// on. `on: false` takes the name out again.
|
|
14
14
|
export async function keepForum(forum, { front, on = true }) {
|
|
15
|
-
|
|
16
|
-
forum
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
// Handing over: the categories' rows, then every rule on the tree with the
|
|
16
|
+
// keeper's name, then the forum's own row — whose switch starts the
|
|
17
|
+
// Gateway's first run, which reads the pod under those rules. Taking it
|
|
18
|
+
// back: the rows, then the rules without the name.
|
|
19
|
+
const restate = async (keeper) => {
|
|
20
|
+
forum.keepers = on && keeper ? [keeper] : [];
|
|
21
|
+
forum.remote.keepers = [...forum.keepers];
|
|
22
|
+
forum.remote.aclOwner = forum.readCredential()?.webId || forum.remote.aclOwner || null;
|
|
23
|
+
await forum.restateRules();
|
|
24
|
+
};
|
|
25
|
+
const { keeper, handles } = await keepRows({ fetch: (u, i) => forum.remote.session.fetch(u, i), front, config: forum.config, on,
|
|
26
|
+
between: on ? restate : null });
|
|
27
|
+
if (!on) await restate(null);
|
|
20
28
|
return { keeper: on ? keeper : null, handles, on };
|
|
21
29
|
}
|
|
22
30
|
|
package/src/publish.mjs
CHANGED
|
@@ -175,6 +175,8 @@ export async function cachePost({ remote, urls }, note, { topic = null, replies
|
|
|
175
175
|
// count beside every post it shows.
|
|
176
176
|
if (Number.isFinite(likes)) copy.likes = { type: 'Collection', totalItems: likes };
|
|
177
177
|
if (Number.isFinite(dislikes)) copy.dislikes = { type: 'Collection', totalItems: dislikes };
|
|
178
|
+
// One copy of the text: a server that sends it once per language hands over a list.
|
|
179
|
+
if (Array.isArray(copy.content)) copy.content = copy.content.find(s => typeof s === 'string') || '';
|
|
178
180
|
if (typeof copy.content === 'string') copy.content = sanitizeHtml(copy.content);
|
|
179
181
|
if (!copy['@context']) copy['@context'] = 'https://www.w3.org/ns/activitystreams';
|
|
180
182
|
const url = urls.cached(note.id);
|
package/src/setup.mjs
CHANGED
|
@@ -99,15 +99,18 @@ export async function attachRows({ fetch: f, front, config, plain }, { log = ()
|
|
|
99
99
|
/**
|
|
100
100
|
* Let a Gateway keep the forum (or stop): every row, the categories first and
|
|
101
101
|
* the forum's own row last, because turning the forum's row on is what starts
|
|
102
|
-
* its first run, and that run wants every category kept already.
|
|
103
|
-
* keeper's WebID
|
|
102
|
+
* its first run, and that run wants every category kept already. `between`,
|
|
103
|
+
* given the keeper's WebID, runs after the categories and before that last
|
|
104
|
+
* switch: the caller names the keeper in the pod's rules there, so the first
|
|
105
|
+
* run can read the pod the moment it starts. Returns the keeper's WebID.
|
|
104
106
|
*/
|
|
105
|
-
export async function keepRows({ fetch: f, front, config, on = true }) {
|
|
107
|
+
export async function keepRows({ fetch: f, front, config, on = true, between = null }) {
|
|
106
108
|
const origin = String(front).replace(/\/+$/u, '');
|
|
107
109
|
const forumHandle = config.handle;
|
|
108
110
|
const handles = [...(config.categories || []).map(c => c.slug), forumHandle];
|
|
109
111
|
let keeper = null;
|
|
110
112
|
for (const handle of handles) {
|
|
113
|
+
if (handle === forumHandle && between) await between(keeper);
|
|
111
114
|
const res = await f(`${origin}/api/keeper`, {
|
|
112
115
|
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
113
116
|
body: JSON.stringify({ handle, on, ...(handle === forumHandle ? {} : { forum: forumHandle }) }),
|
|
@@ -156,8 +159,8 @@ export async function setUpForumAtGateway({ remote, storageFor, fetch: f, front,
|
|
|
156
159
|
await writeConfig(storage, { ...config, gateway, republish: true });
|
|
157
160
|
onStep('attach', 'ok');
|
|
158
161
|
onStep('keep', 'running');
|
|
159
|
-
const { keeper } = await keepRows({ fetch: f, front, config, on: true
|
|
160
|
-
|
|
162
|
+
const { keeper } = await keepRows({ fetch: f, front, config, on: true,
|
|
163
|
+
between: (k) => nameKeeperInRules(remote, { config, plain, keeper: k, ownerWebId }) });
|
|
161
164
|
onStep('keep', 'ok');
|
|
162
165
|
return { handle: config.handle, front: gateway.front, handles, keeper };
|
|
163
166
|
}
|