Pasture: NodeBB joins
NodeBB 4.16.1 runs in the pasture on the pasture's Mongo, set up by its automated setup, with an API token written where NodeBB keeps them. scenarios/nodebb.sh passes its 23 checks with no change to PrivaPub: a category followed and its topic as a titled thread, replies both ways, a follow of alice and her post there, votes both ways, an edit and a deletion, a chat both ways, the unfollow and statistics. NodeBB never federates a topic's lock, and its API follows an account elsewhere only when named by its handle; both are in docs/INTEROP.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
1 parent
26346cde30
commit
6b08da458a
7 files changed
+158
-1
No files matched your search
@@ -102,3 +102,8 @@ mbin.test {
|
||||
tls internal
|
||||
reverse_proxy pasture-mbin:80
|
||||
}
|
||||
|
||||
nodebb.test {
|
||||
tls internal
|
||||
reverse_proxy pasture-nodebb:4567
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
# NodeBB 4.16.1: forum software whose categories are ActivityPub groups and whose topics are threads; federation is on
|
||||
# by default since 4.0. The official image on the pasture's Mongo (database nodebb), set up once by its automated setup
|
||||
# (SETUP with NODEBB_* variables: admin nbuser), then started on that config, trusting the pasture's CA through
|
||||
# NODE_EXTRA_CA_CERTS. Its image runs `npm install` at every start. Its API (/api/v3) takes a bearer token, which
|
||||
# nodebb_settle writes into its database as NodeBB's own token list does (token:<t>, tokens:createtime, tokens:uid).
|
||||
NODEBB_IMAGE=${NODEBB_IMAGE:-ghcr.io/nodebb/nodebb:4.16.1}
|
||||
NODEBB_PASSWORD=NodeBB-Pasture-Pass-1
|
||||
|
||||
nodebb_up() {
|
||||
podman volume exists pasture-nodebb-config || podman volume create --label pasture=1 pasture-nodebb-config >/dev/null
|
||||
podman volume exists pasture-nodebb-uploads || podman volume create --label pasture=1 pasture-nodebb-uploads >/dev/null
|
||||
local common=(--network $net --label pasture=1 -v pasture-nodebb-config:/opt/config:U -v pasture-nodebb-uploads:/usr/src/app/public/uploads:U
|
||||
-v "$ca/bundle.pem:/ca/bundle.pem:z,ro" -e NODE_EXTRA_CA_CERTS=/ca/bundle.pem)
|
||||
if ! podman run --rm "${common[@]}" --entrypoint test "$NODEBB_IMAGE" -s /opt/config/config.json; then
|
||||
podman run --rm "${common[@]}" -e SETUP=true -e NODEBB_URL=https://nodebb.test -e NODEBB_PORT=4567 \
|
||||
-e NODEBB_ADMIN_USERNAME=nbuser -e NODEBB_ADMIN_PASSWORD="$NODEBB_PASSWORD" -e NODEBB_ADMIN_EMAIL=nbuser@nodebb.test \
|
||||
-e NODEBB_DB=mongo -e NODEBB_DB_HOST=mongo -e NODEBB_DB_PORT=27017 -e NODEBB_DB_NAME=nodebb "$NODEBB_IMAGE" \
|
||||
> "$here/.state/nodebb-setup.log" 2>&1
|
||||
fi
|
||||
podman run -d --replace --name pasture-nodebb "${common[@]}" "$NODEBB_IMAGE" >/dev/null
|
||||
for _ in $(seq 1 150); do
|
||||
site nodebb.test -s -o /dev/null -w '%{http_code}' https://nodebb.test:6443/api/config 2>/dev/null | grep -q 200 && break
|
||||
sleep 2
|
||||
done
|
||||
nodebb_settle
|
||||
echo "nodebb: https://nodebb.test:6443"
|
||||
}
|
||||
|
||||
# nbuser's API token, written where NodeBB keeps them
|
||||
nodebb_settle() {
|
||||
local token="pasture-nbuser-token"
|
||||
podman exec pasture-mongo mongosh --quiet nodebb --eval "
|
||||
const now = Date.now();
|
||||
db.objects.updateOne({_key: 'token:$token'}, {\$set: {uid: 1, description: 'pasture', timestamp: now}}, {upsert: true});
|
||||
db.objects.updateOne({_key: 'tokens:createtime', value: '$token'}, {\$set: {score: now}}, {upsert: true});
|
||||
db.objects.updateOne({_key: 'tokens:uid', value: '$token'}, {\$set: {score: 1}}, {upsert: true});" >/dev/null
|
||||
echo "$token" > "$here/.state/nodebb.token"
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
# NodeBB 4.16.1: a forum whose categories are groups. alice follows a category; a topic there reaches her as a titled
|
||||
# thread; replies both ways; nbuser follows alice and her public post becomes a topic there; votes both ways; an edit
|
||||
# and a deletion; a chat both ways; the unfollow; statistics. NodeBB is driven through its API (/api/v3) as
|
||||
# nbuser; a remote account's uid there is its actor's URL.
|
||||
NB=https://nodebb.test:6443
|
||||
NBT=$(cat "$here/.state/nodebb.token" 2>/dev/null)
|
||||
# nb <method> <path> [json]: a NodeBB API call as nbuser, its `response`
|
||||
nb() {
|
||||
local method=$1 path=$2 body=${3:-}
|
||||
if [ -n "$body" ]; then
|
||||
site nodebb.test -s -X "$method" "$NB/api/v3/$path" -H "Authorization: Bearer $NBT" -H 'Content-Type: application/json' -d "$body"
|
||||
else
|
||||
site nodebb.test -s -X "$method" "$NB/api/v3/$path" -H "Authorization: Bearer $NBT"
|
||||
fi
|
||||
}
|
||||
nb_get() { site nodebb.test -s "$NB/api/$1" -H "Authorization: Bearer $NBT"; }
|
||||
enc() { python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "$1"; }
|
||||
p_home_has() { curl -s -H "$PH" "$P/api/v1/timelines/home?limit=40" | j "print(next(((s.get('reblog') or s)['id'] for s in d if '$1' in ((s.get('reblog') or s)['content'] or '') or '$1' in (((s.get('reblog') or s).get('privapub') or {}).get('title') or '')), ''))"; }
|
||||
|
||||
echo "nodebb"
|
||||
[ "$(nb GET users/1 | j "print(d['response']['username'])")" = "nbuser" ] && ok "NodeBB token for nbuser" || { ko "NodeBB token"; return 1; }
|
||||
PT=$(privapub_token alice_nodebb)
|
||||
PH="Authorization: Bearer $PT"
|
||||
[ -n "$PT" ] && ok "PrivaPub token for alice_nodebb" || { ko "PrivaPub token for alice_nodebb"; return 1; }
|
||||
alice_uri="$(curl -s -H "$PH" "$P/api/v1/accounts/verify_credentials" | j "print(d['url'])" | sed 's|/@|/peasants/|')"
|
||||
run=$(date +%s)
|
||||
|
||||
echo " categories"
|
||||
cat=$(nb POST categories "{\"name\":\"Pasture talk $run\",\"description\":\"talk of the pasture\"}" | j "print(json.dumps(d['response']))")
|
||||
cid=$(echo "$cat" | j "print(d['cid'])"); handle=$(echo "$cat" | j "print(d['handle'])")
|
||||
[ -n "$cid" ] && ok "nbuser opens a category" || ko "the category could not be made"
|
||||
cat_on_p=$(curl -s -H "$PH" "$P/api/v2/search?q=$handle@nodebb.test&resolve=true&type=accounts" | j "print(d['accounts'][0]['id'])")
|
||||
[ -n "$cat_on_p" ] && ok "PrivaPub resolves the category as a group" || ko "PrivaPub cannot resolve the category"
|
||||
curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/accounts/$cat_on_p/follow"
|
||||
until_true 45 '[ "$(curl -s -H "$PH" "$P/api/v1/accounts/relationships?id[]=$cat_on_p" | j "print(d[0][\"following\"])")" = "True" ]' \
|
||||
&& ok "alice follows the category (Accept arrived)" || ko "the category's Accept never arrived"
|
||||
|
||||
echo " topics"
|
||||
topic=$(nb POST topics "{\"cid\":$cid,\"title\":\"A NodeBB topic $run\",\"content\":\"posted in a NodeBB category\"}" | j "print(json.dumps(d['response']))")
|
||||
tid=$(echo "$topic" | j "print(d['tid'])"); main_pid=$(echo "$topic" | j "print(d['mainPid'])")
|
||||
until_true 45 '[ -n "$(p_home_has "A NodeBB topic $run")" ]' && ok "the category brings its topic to alice's home" || ko "the topic never reached alice"
|
||||
topic_on_p=$(p_home_has "A NodeBB topic $run")
|
||||
[ "$(curl -s -H "$PH" "$P/api/v1/statuses/$topic_on_p" | j "print((d.get('privapub') or {}).get('title'))")" = "A NodeBB topic $run" ] \
|
||||
&& ok "as a thread with its title" || ko "the topic arrived without its title"
|
||||
|
||||
echo " replies"
|
||||
curl -s -o /dev/null -X POST -H "$PH" $P/api/v1/statuses -d "status=a PrivaPub reply in the topic $run&in_reply_to_id=$topic_on_p&visibility=public"
|
||||
nb_topic_posts() { nb_get "topic/$tid" | j "print(json.dumps([p for p in d.get('posts', [])]))"; }
|
||||
until_true 45 '[ "$(nb_topic_posts | j "print(any(\"a PrivaPub reply in the topic\" in (p.get(\"content\") or \"\") for p in d))")" = "True" ]' \
|
||||
&& ok "alice's reply joins the topic on NodeBB" || ko "alice's reply never reached the topic"
|
||||
nb POST "topics/$tid" "{\"content\":\"a NodeBB reply to the topic $run\"}" >/dev/null
|
||||
until_true 45 '[ "$(curl -s -H "$PH" "$P/api/v1/statuses/$topic_on_p/context" | j "print(any(\"a NodeBB reply to the topic\" in s[\"content\"] for s in d[\"descendants\"]))")" = "True" ]' \
|
||||
&& ok "nbuser's reply threads under the topic on PrivaPub" || ko "nbuser's reply missing on PrivaPub"
|
||||
|
||||
echo " following alice"
|
||||
# (its API follows an account elsewhere only when named by its handle; a URL there makes a local follow of nobody)
|
||||
nb PUT "users/$(enc alice_nodebb@privapub.test)/follow" >/dev/null
|
||||
until_true 45 '[ "$(curl -s -H "$PH" "$P/api/v1/accounts/verify_credentials" | j "print(d[\"followers_count\"])")" -ge 1 ]' \
|
||||
&& ok "nbuser follows alice" || ko "NodeBB's follow never reached alice"
|
||||
p_post=$(curl -s -X POST -H "$PH" $P/api/v1/statuses -d "status=a PrivaPub post for NodeBB $run&visibility=public")
|
||||
p_post_id=$(echo "$p_post" | j "print(d['id'])"); p_post_uri=$(echo "$p_post" | j "print(d['uri'])")
|
||||
until_true 45 '[ "$(nb GET "posts/$(enc "$p_post_uri")" | j "print(\"a PrivaPub post for NodeBB\" in (d[\"response\"].get(\"content\") or \"\"))")" = "True" ]' \
|
||||
&& ok "alice's post reaches NodeBB" || ko "alice's post never reached NodeBB"
|
||||
|
||||
echo " votes"
|
||||
nb PUT "posts/$(enc "$p_post_uri")/vote" '{"delta":1}' >/dev/null
|
||||
until_true 45 '[ "$(curl -s -H "$PH" "$P/api/v1/statuses/$p_post_id" | j "print(d[\"favourites_count\"])")" = "1" ]' \
|
||||
&& ok "nbuser's upvote counts as a like on PrivaPub" || ko "NodeBB's upvote not counted"
|
||||
nb DELETE "posts/$(enc "$p_post_uri")/vote" >/dev/null
|
||||
until_true 45 '[ "$(curl -s -H "$PH" "$P/api/v1/statuses/$p_post_id" | j "print(d[\"favourites_count\"])")" = "0" ]' \
|
||||
&& ok "taking it back reaches PrivaPub" || ko "NodeBB's vote taken back still counts"
|
||||
curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/statuses/$topic_on_p/favourite"
|
||||
until_true 45 '[ "$(nb GET "posts/$main_pid" | j "print(d[\"response\"].get(\"upvotes\"))")" = "1" ]' \
|
||||
&& ok "alice's like is an upvote on NodeBB" || ko "alice's like not counted on NodeBB ($(nb GET "posts/$main_pid" | j "print(d[\"response\"].get(\"upvotes\"), d[\"response\"].get(\"votes\"))"))"
|
||||
|
||||
echo " edits, locks and deletes"
|
||||
nb PUT "posts/$main_pid" "{\"content\":\"posted in a NodeBB category, then edited $run\",\"title\":\"A NodeBB topic $run\"}" >/dev/null
|
||||
until_true 45 'curl -s -H "$PH" "$P/api/v1/statuses/$topic_on_p" | grep -q "then edited $run"' && ok "nbuser's edit reaches PrivaPub" || ko "the edit never reached PrivaPub"
|
||||
# (NodeBB never federates a topic's lock: its activitypub/out.js has no Lock)
|
||||
nb DELETE "topics/$tid/state" >/dev/null
|
||||
until_true 45 '[ "$(curl -s -o /dev/null -w "%{http_code}" -H "$PH" "$P/api/v1/statuses/$topic_on_p")" = "404" ]' \
|
||||
&& ok "the deleted topic leaves PrivaPub" || ko "the deleted topic stays on PrivaPub"
|
||||
|
||||
echo " chats"
|
||||
room=$(nb POST chats "{\"uids\":[\"$alice_uri\"]}" | j "print(d['response']['roomId'])")
|
||||
nb POST "chats/$room" "{\"message\":\"a NodeBB chat $run\"}" >/dev/null
|
||||
until_true 45 'curl -s -H "$PH" "$P/api/v1/conversations" | grep -q "a NodeBB chat $run"' && ok "nbuser's chat arrives as a DM" || ko "NodeBB's chat never reached alice"
|
||||
dm=$(curl -s -H "$PH" "$P/api/v1/conversations" | j "print(next(c['last_status']['id'] for c in d if 'a NodeBB chat $run' in c['last_status']['content']))")
|
||||
curl -s -o /dev/null -X POST -H "$PH" $P/api/v1/statuses -d "status=@nbuser@nodebb.test a PrivaPub answer $run&in_reply_to_id=$dm&visibility=direct"
|
||||
until_true 45 '[ "$(nb GET "chats/$room/messages" | j "print(any(\"a PrivaPub answer $run\" in (m.get(\"content\") or \"\") for m in d[\"response\"][\"messages\"]))")" = "True" ]' \
|
||||
&& ok "alice's answer joins the chat on NodeBB" || ko "alice's answer missing from the chat"
|
||||
|
||||
echo " unfollow"
|
||||
curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/accounts/$cat_on_p/unfollow"
|
||||
until_true 45 '[ "$(nb_get "category/$cid" | j "print(d.get(\"follower_count\", d.get(\"followerCount\", 0)))")" = "0" ]' \
|
||||
&& ok "alice's unfollow reaches the category" || ko "the category still counts alice ($(nb_get "category/$cid" | j "print({k: v for k, v in d.items() if 'follow' in k.lower()})"))"
|
||||
|
||||
echo " statistics"
|
||||
stats_check nodebb.test nodebb
|
||||
Reference in new issue
Block a user