fedipod-bb 0.2.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 +195 -0
- package/bin/fedipod-bb.mjs +113 -0
- package/fep-draft.md +110 -0
- package/package.json +26 -0
- package/site/bb.js +1644 -0
- package/site/index.html +301 -0
- package/site/markdown.mjs +0 -0
- package/site/masto.mjs +179 -0
- package/site/mine.mjs +48 -0
- package/site/oidc-session.mjs +6 -0
- package/site/pod.mjs +394 -0
- package/site/private.mjs +98 -0
- package/site/read.mjs +294 -0
- package/site/seen.mjs +64 -0
- package/src/access.mjs +79 -0
- package/src/credential.mjs +37 -0
- package/src/forum-agent.mjs +991 -0
- package/src/forum-intake.mjs +112 -0
- package/src/index.mjs +9 -0
- package/src/moderation.mjs +303 -0
- package/src/provision.mjs +37 -0
- package/src/publish.mjs +244 -0
- package/src/run.mjs +52 -0
- package/src/settings.mjs +186 -0
- package/src/topics.mjs +165 -0
- package/src/urls.mjs +90 -0
- package/src/wire.mjs +114 -0
package/README.md
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# FediPod-BB
|
|
2
|
+
|
|
3
|
+
A forum whose record lives on a Solid pod, as ActivityPub documents in the
|
|
4
|
+
shapes the Fediverse's boards use: each category is a Group actor (FEP-1b12),
|
|
5
|
+
each topic is a context collection (FEP-7888), each post is a Page (with a title, as Lemmy's are) or a
|
|
6
|
+
Note that names its topic and its category.
|
|
7
|
+
|
|
8
|
+
This package holds the pod layout, the writers for it, the host, and the
|
|
9
|
+
website: where every document lives, how a topic's pages are written and
|
|
10
|
+
sealed, how a member's post is cached for readers, the process that runs
|
|
11
|
+
the forum from a moderator's machine, and the page that shows the forum.
|
|
12
|
+
A moderator's console is part of that page: the queue of reports, held posts
|
|
13
|
+
and asks, and the settings.
|
|
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. The design and the phases are in
|
|
16
|
+
`claude/plans/fedipod-bb.md` at the repository root.
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
node --test packages/fedipod-bb/test/*.test.mjs
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Hosting a forum
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
npm install -g fedipod-bb
|
|
26
|
+
fedipod-bb credential --home DIR --email you@example.org --pod https://forum.example/
|
|
27
|
+
fedipod-bb init --home DIR --handle forum --name "The Forum" \
|
|
28
|
+
--category gardening:Gardening --category compost:Compost --moderator <actor id>
|
|
29
|
+
fedipod-bb start --home DIR
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`credential` mints the forum's pod credential at the pod's server, with the
|
|
33
|
+
account's password asked at the terminal, and saves it as
|
|
34
|
+
`DIR/credential.json`; nothing is written to the pod.
|
|
35
|
+
|
|
36
|
+
`init` writes the forum's config and containers. `start` publishes every
|
|
37
|
+
actor the first time, then drains the forum's one inbox, hands each activity
|
|
38
|
+
to the category it names, places carried posts in topics, and carries them
|
|
39
|
+
to the category's followers. Several moderators run `start` on their own
|
|
40
|
+
machines against the same pod: one hosts, the others watch, and when the
|
|
41
|
+
host stops another takes over within fifteen minutes. A public
|
|
42
|
+
`ap/heartbeat` says when the forum was last hosted. `status` prints
|
|
43
|
+
what the pod's state says without hosting.
|
|
44
|
+
|
|
45
|
+
Every category is a FediPod group: joining is a Follow, a member's public
|
|
46
|
+
post addressed to the category is carried (FEP-1b12) and then placed in its
|
|
47
|
+
topic — the one its `context` names, the one its reply chain leads to, or a
|
|
48
|
+
new one with the post as its opening (FEP-7888). A post from a non-member is
|
|
49
|
+
not carried and opens nothing.
|
|
50
|
+
|
|
51
|
+
## Layout
|
|
52
|
+
|
|
53
|
+
Under one root, `fedipod-bb/`, on the forum's pod:
|
|
54
|
+
|
|
55
|
+
| path | what |
|
|
56
|
+
|---|---|
|
|
57
|
+
| `ap/actor` | the forum's own actor, an `Application` |
|
|
58
|
+
| `ap/inbox/` | the one inbox every category names as its own |
|
|
59
|
+
| `ap/categories` | the categories, an `OrderedCollection` of their actor ids |
|
|
60
|
+
| `ap/administrators` | the moderators' actor ids |
|
|
61
|
+
| `ap-state/` | owner-only: the forum's config, keys and lease |
|
|
62
|
+
| `c/<slug>/` | one FediPod group root per category, unchanged from a group's |
|
|
63
|
+
| `c/<slug>/ap/topics` | the category's topics, newest first, paged |
|
|
64
|
+
| `c/<slug>/ap/topic/<tid>` | a topic: the context collection, with pages `-1`, `-2`, … oldest first |
|
|
65
|
+
| `c/<slug>/ap/cache/<key>` | a readable copy of a member's post, for the website |
|
|
66
|
+
| `c/<slug>/ap-state/topics.json`, `topics/<tid>.json` | owner-only: the topic record |
|
|
67
|
+
|
|
68
|
+
A category's `ap/actor`, `ap/outbox`, `ap/followers`, `ap/moderators` and
|
|
69
|
+
`ap/featured` are what a FediPod group publishes today; the forum adds the
|
|
70
|
+
topic documents beside them. Posts are named in a topic by their authors'
|
|
71
|
+
own ids; the cached copy is for readers in a browser and is never listed in
|
|
72
|
+
a collection.
|
|
73
|
+
|
|
74
|
+
## The website
|
|
75
|
+
|
|
76
|
+
`site/` is a static page. Staged by `scripts/stage-site.mjs` under `/bb/`
|
|
77
|
+
on the fedipod.net site, it opens a forum attached there at
|
|
78
|
+
`/bb/?forum=<handle>`, reading everything through that site's own
|
|
79
|
+
`/u/<handle>/` addresses; `/bb/?pod=https://…/fedipod-bb/` reads a forum
|
|
80
|
+
from its pod directly. Where the site has `bb.<domain>` as a domain alias,
|
|
81
|
+
the same page answers there for every path, with the forum named by the
|
|
82
|
+
first path segment: `https://bb.fedipod.net/<handle>/`. It still reads
|
|
83
|
+
through `<domain>`, where the handles live (the hosts are `BB_HOSTS` in the
|
|
84
|
+
staging script). Anyone reads.
|
|
85
|
+
A topic whose newest post arrived since the reader last opened it is marked
|
|
86
|
+
`New` on the index. The mark is the reader's own: one entry per topic in their
|
|
87
|
+
browser's storage, written nowhere else and sent nowhere - a public forum has
|
|
88
|
+
no place to keep who read what, and no business keeping it. Arriving at a
|
|
89
|
+
forum for the first time reads everything before that moment, so a new reader
|
|
90
|
+
does not meet a page of `New`.
|
|
91
|
+
|
|
92
|
+
The page is built to WCAG 2.2 AA: a skip link, named landmarks, a label on
|
|
93
|
+
every control, an `aria-label` on each post's buttons naming whose post they
|
|
94
|
+
act on, `aria-current` on the category in view, `scope` on the index's
|
|
95
|
+
headers, a polite live region for what the page has just done, and a reply
|
|
96
|
+
box that takes the keyboard when it opens and returns it when it closes.
|
|
97
|
+
The front page is an index of the forum's newest posts, across every
|
|
98
|
+
category: topic, category, author, date and the topic's reply count, one
|
|
99
|
+
line each. The forum publishes that index itself (`ap/latest`, the copies it
|
|
100
|
+
already holds, newest first, capped at fifty), so the page makes one request
|
|
101
|
+
for the list rather than walking every category. The category chips filter
|
|
102
|
+
it; a topic's name opens the thread at that post. Pinned topics sort first -
|
|
103
|
+
a category's `featured` collection for a pin within it, the forum actor's own
|
|
104
|
+
for one that holds everywhere.
|
|
105
|
+
|
|
106
|
+
A thread is a tree: a reply sits under the post it answers, six deep. Posts
|
|
107
|
+
are written in Markdown (`site/markdown.mjs`, no dependency, escaped before
|
|
108
|
+
it is marked up) and carry their source alongside, so an edit reopens what
|
|
109
|
+
was typed. A topic is named by the activity that opens it; a post has no
|
|
110
|
+
title.
|
|
111
|
+
|
|
112
|
+
Each post offers Reply, Share and Report, and its author Edit and Delete; a
|
|
113
|
+
report is queued for the moderators. Under a topic's title a moderator gets
|
|
114
|
+
rename, pin, pin site-wide and delete. Every moderator request is published
|
|
115
|
+
at the moderator's own pod and fetched back from there before the forum acts
|
|
116
|
+
on it (FEP-fe34), which is what makes a button on a public page safe.
|
|
117
|
+
A post can be voted for: a `Like` to the category, its `Undo` to take it
|
|
118
|
+
back, counted per person and published with the post as AS2 `likes`. The
|
|
119
|
+
index sorts by newest or by votes, and searches what it is holding - topic
|
|
120
|
+
names, authors and the words of the posts. A byline opens that person's
|
|
121
|
+
posts in this forum.
|
|
122
|
+
|
|
123
|
+
A moderator's queue lives at `<pod>/fedipod-bb/mod/queue.json`, with a
|
|
124
|
+
record of what was done beside it in `log.json`. Neither is public: the
|
|
125
|
+
container's access rule names the moderators' WebIDs, and the page reads it
|
|
126
|
+
with the moderator's own login rather than through the Gateway. From it a
|
|
127
|
+
moderator lets a held post through (`Accept`), turns it away (`Reject`) or
|
|
128
|
+
bans its author (`Block`) - each published at their own pod and checked
|
|
129
|
+
there, like every other ask.
|
|
130
|
+
|
|
131
|
+
A moderator gets a Settings page: rename the forum, rename a category,
|
|
132
|
+
manage moderators and members, and make a new category. Each row carries
|
|
133
|
+
Open and Private. Every change is an ordinary activity - a `Create` of a
|
|
134
|
+
`Group`, an `Update`, an `Add` or a `Remove` - published at the moderator's
|
|
135
|
+
own pod and fetched back before it is applied.
|
|
136
|
+
|
|
137
|
+
**Open or private belongs to the category, and only the forum changes it.**
|
|
138
|
+
Joining does not, being admitted does not, and being removed does not. On
|
|
139
|
+
the wire it is `manuallyApprovesFollowers` on the Group.
|
|
140
|
+
|
|
141
|
+
A private category means all of this, and no more:
|
|
142
|
+
|
|
143
|
+
- Its trees are readable by named WebIDs only. The actor stays public, since
|
|
144
|
+
a server that cannot read the actor cannot deliver to the category.
|
|
145
|
+
- It publishes `c/<slug>/ap/members`, the WebIDs that may read it, readable
|
|
146
|
+
only by those same WebIDs. The forum's own WebID is one of them, because
|
|
147
|
+
the forum fetches every post back from its author's pod.
|
|
148
|
+
- Every join waits for a moderator, and admitting somebody requires a WebID.
|
|
149
|
+
A follower without one is let go with a `Reject`: carrying posts to
|
|
150
|
+
somebody refused the pages would not be private at all.
|
|
151
|
+
- A member's post is addressed to the category's followers collection rather
|
|
152
|
+
than to the public, and is written into a container of its own on the
|
|
153
|
+
member's pod whose access rule names the same reader list. One container
|
|
154
|
+
per category, rewritten on each post, so one rule covers that member's
|
|
155
|
+
whole history there.
|
|
156
|
+
|
|
157
|
+
What it does NOT mean, and the page says so in the box before anyone writes:
|
|
158
|
+
an ejected reader keeps a given author's older posts until that author next
|
|
159
|
+
writes; somebody admitted today reads the whole history; posts written while
|
|
160
|
+
the category was open stay public; pod operators hold the plaintext; and any
|
|
161
|
+
admitted member can copy anything.
|
|
162
|
+
|
|
163
|
+
The command-line equivalents are `--members-only <slug>` and
|
|
164
|
+
`--member <slug>:<webid>`. A draft FEP for the layout and this model is in
|
|
165
|
+
[fep-draft.md](fep-draft.md).
|
|
166
|
+
To reply, a reader signs in once with a
|
|
167
|
+
Mastodon account: the page registers itself on their server, sends them to
|
|
168
|
+
approve it, and keeps the token in their browser; a reply is posted from
|
|
169
|
+
their account with the category mentioned, and answers the thread's last
|
|
170
|
+
post as their server resolves it. The reply shows in the thread once the
|
|
171
|
+
forum's host has placed it; until then the reader sees their own copy marked
|
|
172
|
+
as waiting. A reader with a pod account is pointed at FediPod and the
|
|
173
|
+
category's address; a reader with nothing is pointed at sign-up.
|
|
174
|
+
|
|
175
|
+
## Attaching to a Gateway
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
node packages/fedipod-bb/bin/fedipod-bb.mjs attach --home DIR --front https://fedipod.net
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Takes `@<handle>@fedipod.net` for the forum and one address per category,
|
|
182
|
+
each a row of its own at the Gateway pointing at that category's tree on the
|
|
183
|
+
pod and at the forum's one inbox. Deliveries arrive verified at the front;
|
|
184
|
+
the next `start` republishes every actor under its front address.
|
|
185
|
+
|
|
186
|
+
A post is voted up or down, the way Lemmy federates a vote: a `Like` for up,
|
|
187
|
+
a `Dislike` for down, and the `Undo` of whichever was cast to take it back.
|
|
188
|
+
One person has one vote, so voting the other way is a changed mind rather
|
|
189
|
+
than a second vote. The up count is AS2's `likes` on the post's copy.
|
|
190
|
+
ActivityStreams has no property for a down count and none is invented: it is
|
|
191
|
+
an ordinary `Collection` published beside the copy, at that copy's address
|
|
192
|
+
with `-dislikes` after it, and taken down again when it reaches nought.
|
|
193
|
+
The copy also carries the count itself, as `dislikes` shaped like `likes`,
|
|
194
|
+
which is the forum's own word; the page reads it there rather than asking
|
|
195
|
+
for a document beside every post it shows.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// fedipod-bb.mjs — run a forum from this machine.
|
|
3
|
+
//
|
|
4
|
+
// fedipod-bb credential --home DIR --email you@example.org --pod https://forum.example/ [--issuer URL]
|
|
5
|
+
// Mint the forum's pod credential at the pod's server (the password is
|
|
6
|
+
// asked at the terminal) and save it as DIR/credential.json.
|
|
7
|
+
// fedipod-bb init --home DIR --handle forum --name "The Forum" \
|
|
8
|
+
// [--moderator-webid https://you.example/profile/card#me] \
|
|
9
|
+
// --category gardening:Gardening --category compost:Compost [--moderator <actor>]
|
|
10
|
+
// Writes the forum's config and containers to the pod; publishes nothing yet.
|
|
11
|
+
// fedipod-bb start --home DIR
|
|
12
|
+
// Host the forum from here. Publishes every actor on first start, then
|
|
13
|
+
// drains the forum's inbox, places posts in topics and carries them.
|
|
14
|
+
// Several moderators may run this on their own machines; one acts, the
|
|
15
|
+
// others watch and take over when it stops.
|
|
16
|
+
// fedipod-bb status --home DIR
|
|
17
|
+
// What the forum's state says, without hosting.
|
|
18
|
+
// fedipod-bb attach --home DIR --front https://fedipod.net
|
|
19
|
+
// Take addresses at a Gateway: @<handle>@<front> for the forum and one
|
|
20
|
+
// per category. Deliveries arrive verified at the front and are written
|
|
21
|
+
// into the forum's inbox; every actor is republished with its front ids
|
|
22
|
+
// on the next start.
|
|
23
|
+
|
|
24
|
+
import fs from 'node:fs';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
import { ForumAgent } from '../src/forum-agent.mjs';
|
|
27
|
+
|
|
28
|
+
const args = process.argv.slice(2);
|
|
29
|
+
const cmd = args[0];
|
|
30
|
+
const flag = (name) => { const i = args.indexOf('--' + name); return i >= 0 ? args[i + 1] : null; };
|
|
31
|
+
const flags = (name) => args.flatMap((a, i) => (a === '--' + name && args[i + 1] ? [args[i + 1]] : []));
|
|
32
|
+
const home = flag('home') || process.env.FEDIPOD_BB_HOME;
|
|
33
|
+
if (!home) { console.error('--home DIR is required'); process.exit(2); }
|
|
34
|
+
|
|
35
|
+
const logFile = path.join(home, 'forum.log');
|
|
36
|
+
const log = (...a) => {
|
|
37
|
+
const line = `${new Date().toISOString()} ${a.join(' ')}`;
|
|
38
|
+
console.log('[bb]', ...a);
|
|
39
|
+
try { fs.appendFileSync(logFile, line + '\n'); } catch { /* logging never throws */ }
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// --reply-policy open|review, checked here so a typo does not quietly become
|
|
43
|
+
// the strictest reading of it.
|
|
44
|
+
const replyPolicy = () => {
|
|
45
|
+
const said = String(flag('reply-policy') || '').toLowerCase();
|
|
46
|
+
if (said !== 'open' && said !== 'review') {
|
|
47
|
+
console.error('--reply-policy takes open or review');
|
|
48
|
+
process.exit(2);
|
|
49
|
+
}
|
|
50
|
+
return said;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
if (cmd === 'credential') {
|
|
54
|
+
const email = flag('email');
|
|
55
|
+
const pod = flag('pod');
|
|
56
|
+
if (!email || !pod) { console.error('--email and --pod are required'); process.exit(2); }
|
|
57
|
+
const { mintForumCredential } = await import('../src/credential.mjs');
|
|
58
|
+
try {
|
|
59
|
+
const file = await mintForumCredential({ email, pod, home, issuer: flag('issuer'), root: flag('root') || 'fedipod-bb/' });
|
|
60
|
+
console.log(`credential minted and saved to ${file}`);
|
|
61
|
+
} catch (e) { console.error(e.message); process.exit(1); }
|
|
62
|
+
} else if (cmd === 'init') {
|
|
63
|
+
const handle = flag('handle');
|
|
64
|
+
if (!handle) { console.error('--handle is required'); process.exit(2); }
|
|
65
|
+
const categories = flags('category').map(c => {
|
|
66
|
+
const [slug, name] = c.split(':');
|
|
67
|
+
return { slug, name: name || slug };
|
|
68
|
+
});
|
|
69
|
+
const agent = new ForumAgent({ home, log });
|
|
70
|
+
const cfg = await agent.init({
|
|
71
|
+
handle, name: flag('name') || handle, categories, moderators: flags('moderator'),
|
|
72
|
+
// A moderator's WebID, so the pod itself can let them read the queue;
|
|
73
|
+
// their actor id is what the wire uses and cannot be granted access.
|
|
74
|
+
moderatorWebIds: flags('moderator-webid'),
|
|
75
|
+
// A members-only category, and who may read it: --members-only <slug>
|
|
76
|
+
// and --member <slug>:<webid>. Only a WebID can be named; a follower
|
|
77
|
+
// from Mastodon has none, and serving them is the Server build's job.
|
|
78
|
+
membersOnly: flags('members-only'),
|
|
79
|
+
memberWebIds: flags('member').reduce((m, v) => {
|
|
80
|
+
const at = v.indexOf(':');
|
|
81
|
+
if (at < 1) return m;
|
|
82
|
+
const slug = v.slice(0, at);
|
|
83
|
+
(m[slug] ||= []).push(v.slice(at + 1));
|
|
84
|
+
return m;
|
|
85
|
+
}, {}),
|
|
86
|
+
approveJoins: args.includes('--approve-joins'), review: args.includes('--review'),
|
|
87
|
+
// What becomes of a post from somebody who has not joined: `open` takes
|
|
88
|
+
// the post as the joining, `review` holds it for a moderator. A private
|
|
89
|
+
// category holds it whatever this says.
|
|
90
|
+
...(flag('reply-policy') ? { replyPolicy: replyPolicy() } : {}),
|
|
91
|
+
});
|
|
92
|
+
console.log(JSON.stringify(cfg, null, 2));
|
|
93
|
+
} else if (cmd === 'start') {
|
|
94
|
+
const { runForum } = await import('../src/run.mjs');
|
|
95
|
+
const agent = await runForum({ home, log });
|
|
96
|
+
if (!agent) process.exit(1);
|
|
97
|
+
} else if (cmd === 'attach') {
|
|
98
|
+
const front = flag('front');
|
|
99
|
+
if (!front) { console.error('--front <https://gateway-origin> is required'); process.exit(2); }
|
|
100
|
+
const agent = new ForumAgent({ home, log });
|
|
101
|
+
if (!await agent.connect({ act: false })) { console.error('nothing to attach — run init first'); process.exit(1); }
|
|
102
|
+
const r = await agent.attach({ front });
|
|
103
|
+
console.log(`attached at ${r.front}: ${r.handles.map(h => '@' + h).join(', ')} — start the forum to publish its new addresses`);
|
|
104
|
+
process.exit(0);
|
|
105
|
+
} else if (cmd === 'status') {
|
|
106
|
+
const agent = new ForumAgent({ home, log: () => {} });
|
|
107
|
+
const up = await agent.connect({ act: false });
|
|
108
|
+
console.log(JSON.stringify(up ? agent.status() : { mode: 'unconfigured' }, null, 2));
|
|
109
|
+
process.exit(0);
|
|
110
|
+
} else {
|
|
111
|
+
console.log('usage: fedipod-bb <credential|init|start|status|attach> --home DIR [--email E --pod URL | --handle H --name N --category slug:Name … --reply-policy open|review | --front URL]');
|
|
112
|
+
process.exit(2);
|
|
113
|
+
}
|
package/fep-draft.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
---
|
|
2
|
+
slug: "xxxx"
|
|
3
|
+
authors: Jeff Zucker <https://jeffz.solidcommunity.net/profile/card#me>
|
|
4
|
+
status: DRAFT
|
|
5
|
+
dateReceived: 2026-09-16
|
|
6
|
+
---
|
|
7
|
+
# A discussion forum whose record is a Solid pod
|
|
8
|
+
|
|
9
|
+
## Summary
|
|
10
|
+
|
|
11
|
+
A forum — categories, topics, posts, moderation — kept as ActivityPub
|
|
12
|
+
documents on a Solid pod and served from static pages. No forum database and no
|
|
13
|
+
forum server. Readers read the pod. Members write to their own pods. A host
|
|
14
|
+
run by the moderators drains one inbox and writes the record back.
|
|
15
|
+
|
|
16
|
+
Existing FEPs cover the wire. This says where the documents live and how a
|
|
17
|
+
category is made private with the pod's own access control.
|
|
18
|
+
|
|
19
|
+
## Requirements
|
|
20
|
+
|
|
21
|
+
The key words MUST, SHOULD and MAY are to be interpreted as in RFC 2119.
|
|
22
|
+
|
|
23
|
+
## The layout
|
|
24
|
+
|
|
25
|
+
One container, by convention `fedipod-bb/`, on the forum's pod.
|
|
26
|
+
|
|
27
|
+
ap/actor the site, an Application
|
|
28
|
+
ap/inbox/ ONE inbox; every category names it as its sharedInbox
|
|
29
|
+
ap/categories OrderedCollection of category actor ids
|
|
30
|
+
ap/administrators FEP-baf5
|
|
31
|
+
ap-state/ owner-only: config, lease, keys
|
|
32
|
+
c/<slug>/ one per category, an ordinary group's tree
|
|
33
|
+
ap/actor a Group (FEP-1b12)
|
|
34
|
+
ap/followers its membership
|
|
35
|
+
ap/moderators its roster; the actor's attributedTo
|
|
36
|
+
ap/members who may READ it; present only when it is private
|
|
37
|
+
ap/topics(-N) OrderedCollection of topic ids
|
|
38
|
+
ap/topic/<tid>(-N) one topic: a context collection (FEP-7888)
|
|
39
|
+
ap/cache/<sha16> the forum's readable copy of a member's post
|
|
40
|
+
|
|
41
|
+
A topic is an `OrderedCollection` whose `attributedTo` is the category, whose
|
|
42
|
+
`orderedItems` are the authors' own post ids, and whose `name` is the topic's
|
|
43
|
+
title — given by the person opening it, in the `name` of their `Create`, and
|
|
44
|
+
never taken from the post. Every post in it carries `context` naming it.
|
|
45
|
+
|
|
46
|
+
Posts are named, not copied, in the record: the author's server stays the
|
|
47
|
+
authority on edits and deletions (FEP-fe34). Because a browser cannot fetch
|
|
48
|
+
from a stranger's server, the forum ALSO writes a verified copy under
|
|
49
|
+
`ap/cache/`, replaced by a `Tombstone` (FEP-4f05) when the original goes.
|
|
50
|
+
|
|
51
|
+
## Open and private
|
|
52
|
+
|
|
53
|
+
Open or private is a property of the category, set by its administrators. It
|
|
54
|
+
MUST NOT change as a side effect of anybody joining, being admitted, or being
|
|
55
|
+
removed. On the wire it is `manuallyApprovesFollowers` on the Group.
|
|
56
|
+
|
|
57
|
+
A private category:
|
|
58
|
+
|
|
59
|
+
1. Its trees are readable by named WebIDs only; the actor stays public, since
|
|
60
|
+
a server that cannot read the actor cannot deliver to it.
|
|
61
|
+
2. It publishes `ap/members`, an OrderedCollection of the WebIDs that may read
|
|
62
|
+
it, itself readable only by those WebIDs. The forum's own WebID is among
|
|
63
|
+
them, because the forum dereferences each post at its author's pod.
|
|
64
|
+
3. Every join waits for a moderator, and admitting somebody requires a WebID.
|
|
65
|
+
A follower without one MUST be let go with a `Reject`: carrying posts to
|
|
66
|
+
someone who is refused the pages is not privacy.
|
|
67
|
+
4. Members address their posts to the category's followers collection, not to
|
|
68
|
+
`as:Public`. A group MUST NOT otherwise carry a non-public post; a private
|
|
69
|
+
group carries them because its membership is exactly the set already
|
|
70
|
+
permitted to read them.
|
|
71
|
+
5. The member's own copy is written into a container on the member's pod whose
|
|
72
|
+
access rule names the same reader list. One container per category, so one
|
|
73
|
+
rule covers that member's whole history there and is rewritten with each
|
|
74
|
+
post.
|
|
75
|
+
|
|
76
|
+
## Votes
|
|
77
|
+
|
|
78
|
+
A vote is a `Like` or a `Dislike` addressed to the category, and the `Undo` of
|
|
79
|
+
whichever was cast to take it back; one actor MAY hold one of them per object,
|
|
80
|
+
so the second replaces the first. The up count is published as the object's
|
|
81
|
+
`likes`. ActivityStreams has no property for a down count: implementations
|
|
82
|
+
MUST NOT invent one. This publishes it as a `Collection` beside the object's
|
|
83
|
+
cached copy, at that copy's address with `-dislikes` after it, absent at nought.
|
|
84
|
+
|
|
85
|
+
## Security considerations
|
|
86
|
+
|
|
87
|
+
- **Revocation is not uniform.** The forum revokes its own copies at once.
|
|
88
|
+
Members' own copies are behind rules only those members can write, so an
|
|
89
|
+
ejected reader keeps a given author's older posts until that author next
|
|
90
|
+
writes. Implementations SHOULD rewrite the rule on every post.
|
|
91
|
+
- **Admission is retroactive.** A new member reads the whole history, because
|
|
92
|
+
the forum's copies are re-granted as a set.
|
|
93
|
+
- **Not confidentiality.** This is access control. Pod operators, and any
|
|
94
|
+
gateway a delivery passes through, hold the plaintext. Any admitted member
|
|
95
|
+
can copy anything. A category MUST NOT claim more than that.
|
|
96
|
+
- **Nothing is retroactive at the switch.** Posts written while a category was
|
|
97
|
+
open were published to the world and stay so.
|
|
98
|
+
- An unsigned append to a public inbox proves nothing, so a moderator's
|
|
99
|
+
request is published at the moderator's own pod and fetched back from there
|
|
100
|
+
before it is applied (FEP-fe34).
|
|
101
|
+
|
|
102
|
+
## References
|
|
103
|
+
|
|
104
|
+
FEP-1b12 group federation · FEP-7888 contexts · FEP-f15d relocation ·
|
|
105
|
+
FEP-4f05 tombstones · FEP-fe34 origin · FEP-7458 replies · FEP-b2b8 long-form ·
|
|
106
|
+
FEP-baf5 administrators · FEP-044f quotes · W3C Web Access Control · Solid-OIDC
|
|
107
|
+
|
|
108
|
+
## Copyright
|
|
109
|
+
|
|
110
|
+
CC0 1.0 Universal.
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fedipod-bb",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "FediPod-BB: a forum website whose record lives on a Solid pod.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "src/index.mjs",
|
|
8
|
+
"bin": {
|
|
9
|
+
"fedipod-bb": "bin/fedipod-bb.mjs"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"bin",
|
|
13
|
+
"src",
|
|
14
|
+
"site",
|
|
15
|
+
"README.md",
|
|
16
|
+
"fep-draft.md"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"pretest": "node scripts/link-fedipod.mjs",
|
|
20
|
+
"test": "node --test test/*.test.mjs"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"fedipod": "^1.42.0",
|
|
24
|
+
"fediverse-session": "^0.2.1"
|
|
25
|
+
}
|
|
26
|
+
}
|