# 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 [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