Both speak Lemmy's shapes and joined the servers that get reports in that shape only once the pasture showed each keeps one with its reason. By their source, PieFed dropped the instance actor's report (it makes a user only of a Person or a Service) and Mbin kept it without the persona's words (it reads the reason from `summary` alone). With them in `ServiceReportTakers`, each keeps the reports of a thread and of a comment from "Reports from privapub.test" with alice's words, and none names her (piefed.sh and mbin.sh, 65 checks with the full sweep's 876). A report of a PieFed account alone, which PieFed would take from the reporter, is not sent yet; INTEROP says so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
137 lines
12 KiB
Bash
137 lines
12 KiB
Bash
# Mbin 1.10.1: the threadiverse in Symfony. Magazines both ways (FEP-1b12), threads with titles, a Note addressed to a
|
|
# magazine as one of its microblog posts, comments both ways, favourites (Like) and upvotes (Announce) both ways (Mbin
|
|
# sends no downvote), private messages both ways, a moderator's lock and removal, the unfollow, statistics. Mbin is driven
|
|
# through its API as mbuser, with the token peers/mbin_token.py got through the authorization-code flow.
|
|
MB=https://mbin.test:6443
|
|
MT=$(cat "$here/.state/mbin.token" 2>/dev/null)
|
|
# mb <method> <path> [json]: an Mbin API call as mbuser. Mbin names what it makes during a request after the request's
|
|
# host, so the call says mbin.test, without the workstation's port
|
|
mb() {
|
|
local method=$1 path=$2 body=${3:-}
|
|
if [ -n "$body" ]; then
|
|
site mbin.test -s -X "$method" "$MB/api/$path" -H 'Host: mbin.test' -H "Authorization: Bearer $MT" -H 'Content-Type: application/json' -d "$body"
|
|
else
|
|
site mbin.test -s -X "$method" "$MB/api/$path" -H 'Host: mbin.test' -H "Authorization: Bearer $MT"
|
|
fi
|
|
}
|
|
mb_sql() { podman exec pasture-postgres psql -U pasture -d mbin -tAc "$1" 2>/dev/null; }
|
|
# what Mbin finds for a handle or a URL (search/v2 resolves it): the first of its kind, as JSON
|
|
mb_resolve() { mb GET "search/v2?q=$(python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1]))' "$1")" | j "print(json.dumps(next((r['$2'] for r in d.get('apResults', []) if r.get('$2')), None)))"; }
|
|
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 "mbin"
|
|
# Mbin's access token lasts an hour: a stale one is replaced through the same authorization-code flow
|
|
if [ "$(mb GET users/me | j "print(d['username'])")" != "mbuser" ]; then
|
|
python3 "$here/peers/mbin_token.py" Mbin-Pasture-Pass-1 "$here/.state/mbin/client.json" > "$here/.state/mbin.token" 2>"$here/.state/mbin/token.log"
|
|
MT=$(cat "$here/.state/mbin.token" 2>/dev/null)
|
|
fi
|
|
[ -n "$MT" ] && [ "$(mb GET users/me | j "print(d['username'])")" = "mbuser" ] && ok "Mbin token for mbuser" || { ko "Mbin token"; return 1; }
|
|
PT=$(privapub_token alice_mbin)
|
|
PH="Authorization: Bearer $PT"
|
|
[ -n "$PT" ] && ok "PrivaPub token for alice_mbin" || { ko "PrivaPub token for alice_mbin"; return 1; }
|
|
jwt=$(privapub_root)
|
|
alice_id=$(curl -s -H "$PH" "$P/api/v1/accounts/verify_credentials" | j "print(d['id'])")
|
|
run=$(date +%s)
|
|
dogs="mbdogs$run"
|
|
books="books$run"
|
|
|
|
echo " magazines"
|
|
dogs_gid=$(curl -s -X POST $P/clientapi/group/insert -H 'Content-Type: application/json' -H "Authorization: Bearer $jwt" \
|
|
-d "{\"avatarId\":\"$alice_id\",\"userName\":\"$dogs\",\"name\":\"Pasture dogs\",\"description\":\"a PrivaPub community\",\"isCommunity\":true}" | j "print(d['id'])")
|
|
dogs_on_mb=$(mb_resolve "!$dogs@privapub.test" magazine | j "print(d['magazineId'])")
|
|
[ -n "$dogs_on_mb" ] && ok "Mbin resolves a PrivaPub community as a magazine" || ko "Mbin cannot resolve the PrivaPub community"
|
|
dogs_on_p=$(curl -s -H "$PH" "$P/api/v1/accounts/lookup?acct=$dogs" | j "print(d['id'])")
|
|
mb PUT "magazine/$dogs_on_mb/subscribe" >/dev/null
|
|
until_true 45 '[ "$(curl -s -H "$PH" "$P/api/v1/accounts/$dogs_on_p" | j "print(d[\"followers_count\"])")" = "1" ]' \
|
|
&& ok "mbuser follows the PrivaPub community" || ko "Mbin's follow of the PrivaPub community never arrived"
|
|
podman exec pasture-mbin php bin/console mbin:magazine:create "$books" --owner=mbuser >/dev/null 2>&1
|
|
books_on_mb=$(mb GET "magazine/name/$books" | j "print(d['magazineId'])")
|
|
books_on_p=$(curl -s -H "$PH" "$P/api/v2/search?q=@$books@mbin.test&resolve=true&type=accounts" | j "print(d['accounts'][0]['id'])")
|
|
[ -n "$books_on_p" ] && ok "PrivaPub resolves an Mbin magazine" || ko "PrivaPub cannot resolve the Mbin magazine"
|
|
curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/accounts/$books_on_p/follow"
|
|
until_true 45 '[ "$(curl -s -H "$PH" "$P/api/v1/accounts/relationships?id[]=$books_on_p" | j "print(d[0][\"following\"])")" = "True" ]' \
|
|
&& ok "alice follows the Mbin magazine (Accept arrived)" || ko "the magazine's Accept never arrived"
|
|
|
|
echo " threads"
|
|
mb POST "magazine/$dogs_on_mb/article" "{\"title\":\"An Mbin thread $run\",\"body\":\"posted from Mbin into PrivaPub\",\"lang\":\"en\"}" >/dev/null
|
|
until_true 45 '[ "$(curl -s -H "$PH" "$P/api/v1/accounts/$dogs_on_p/statuses" | j "print(any((s.get(\"privapub\") or {}).get(\"title\")==\"An Mbin thread $run\" for s in d))")" = "True" ]' \
|
|
&& ok "an Mbin thread in the PrivaPub community arrives with its title" || ko "Mbin's thread missing or untitled on PrivaPub"
|
|
curl -s -o /dev/null -X POST $P/clientapi/post/insert -H 'Content-Type: application/json' -H "Authorization: Bearer $jwt" \
|
|
-d "{\"avatarId\":\"$alice_id\",\"title\":\"A PrivaPub community thread $run\",\"text\":\"posted into our own community for Mbin\",\"groupId\":\"$dogs_gid\"}"
|
|
until_true 45 '[ "$(mb GET "magazine/$dogs_on_mb/entries?sort=newest" | j "print(any(e[\"title\"]==\"A PrivaPub community thread $run\" for e in d.get(\"items\", [])))")" = "True" ]' \
|
|
&& ok "a titled post in the PrivaPub community reaches Mbin as a thread" || ko "PrivaPub community post missing on Mbin"
|
|
books_thread=$(mb POST "magazine/$books_on_mb/article" "{\"title\":\"Books only $run\",\"body\":\"an Mbin magazine thread\",\"lang\":\"en\"}" | j "print(d['entryId'])")
|
|
until_true 45 '[ -n "$(p_home_has "Books only $run")" ]' && ok "the magazine's announce brings its thread to alice's home" || ko "Mbin magazine thread missing from alice's home"
|
|
books_thread_on_p=$(p_home_has "Books only $run")
|
|
p_note=$(curl -s -X POST -H "$PH" $P/api/v1/statuses --data-urlencode "status=@$books@mbin.test a PrivaPub note for the magazine $run" -d 'visibility=public')
|
|
p_note_id=$(echo "$p_note" | j "print(d['id'])")
|
|
mb_post_of() { mb GET "magazine/$books_on_mb/posts?sort=newest" | j "print(next((p['postId'] for p in d.get('items', []) if '$1' in (p.get('body') or '')), ''))"; }
|
|
until_true 45 '[ -n "$(mb_post_of "a PrivaPub note for the magazine $run")" ]' && ok "alice's note lands in the magazine as a microblog post" || ko "PrivaPub note missing from the Mbin magazine"
|
|
p_note_on_mb=$(mb_post_of "a PrivaPub note for the magazine $run")
|
|
|
|
echo " comments"
|
|
mb POST "posts/$p_note_on_mb/comments" "{\"body\":\"an Mbin comment on PrivaPub $run\",\"lang\":\"en\"}" >/dev/null
|
|
until_true 45 '[ "$(curl -s -H "$PH" "$P/api/v1/statuses/$p_note_id/context" | j "print(any(\"an Mbin comment on PrivaPub\" in s[\"content\"] for s in d[\"descendants\"]))")" = "True" ]' \
|
|
&& ok "Mbin's comment threads under alice's note" || ko "Mbin's comment missing on PrivaPub"
|
|
curl -s -o /dev/null -X POST -H "$PH" $P/api/v1/statuses -d "status=a PrivaPub comment on Mbin $run&in_reply_to_id=$books_thread_on_p&visibility=public"
|
|
until_true 45 '[ "$(mb GET "entry/$books_thread/comments" | j "print(any(\"a PrivaPub comment on Mbin\" in (c.get(\"body\") or \"\") for c in d.get(\"items\", [])))")" = "True" ]' \
|
|
&& ok "alice's reply becomes a comment on the Mbin thread" || ko "PrivaPub reply missing as an Mbin comment"
|
|
|
|
echo " votes"
|
|
p_counts() { curl -s -H "$PH" "$P/api/v1/statuses/$p_note_id" | j "print(d['favourites_count'], d['reblogs_count'], ((d.get('privapub') or {}).get('votes') or {}).get('down', 0))"; }
|
|
mb PUT "post/$p_note_on_mb/favourite" >/dev/null
|
|
until_true 45 '[ "$(p_counts | cut -d" " -f1)" = "1" ]' && ok "Mbin's favourite counts as a like on PrivaPub" || ko "Mbin's favourite not counted ($(p_counts))"
|
|
mb PUT "post/$p_note_on_mb/vote/1" >/dev/null
|
|
until_true 45 '[ "$(p_counts | cut -d" " -f2)" = "1" ]' && ok "Mbin's upvote arrives as a boost" || ko "Mbin's upvote not counted ($(p_counts))"
|
|
# (Mbin keeps downvotes to itself, and sends nothing when an upvote is taken back: its vote listener only announces an
|
|
# upvote, VoteHandleSubscriber::onVote)
|
|
curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/statuses/$books_thread_on_p/favourite"
|
|
until_true 45 '[ "$(mb GET "entry/$books_thread" | j "print(d[\"favourites\"])")" = "1" ]' \
|
|
&& ok "alice's like is a favourite on Mbin" || ko "alice's like not counted on Mbin ($(mb GET "entry/$books_thread" | j "print(d[\"favourites\"], d[\"uv\"])"))"
|
|
curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/statuses/$books_thread_on_p/reblog"
|
|
until_true 45 '[ "$(mb GET "entry/$books_thread" | j "print(d[\"uv\"])")" = "1" ]' \
|
|
&& ok "alice's boost is an upvote on Mbin" || ko "alice's boost not counted on Mbin ($(mb GET "entry/$books_thread" | j "print(d[\"favourites\"], d[\"uv\"])"))"
|
|
|
|
echo " private messages"
|
|
# Mbin takes a private message only as a ChatMessage, which PrivaPub sends it since NodeInfo names Mbin (owner decision
|
|
# 2026-10-05); its API never starts a conversation with an account elsewhere, so mbuser answers in the thread alice began
|
|
curl -s -o /dev/null -X POST -H "$PH" $P/api/v1/statuses -d "status=@mbuser@mbin.test a secret from PrivaPub $run&visibility=direct"
|
|
until_true 45 '[ "$(mb_sql "select count(*) from message where body like '"'%a secret from PrivaPub $run%'"'")" = "1" ]' \
|
|
&& ok "alice's DM arrives as an Mbin private message" || ko "alice's DM never reached Mbin"
|
|
thread=$(mb_sql "select thread_id from message where body like '%a secret from PrivaPub $run%' limit 1")
|
|
mb POST "messages/thread/$thread/reply" "{\"body\":\"an Mbin answer $run\"}" >/dev/null
|
|
until_true 45 'curl -s -H "$PH" "$P/api/v1/conversations" | grep -q "an Mbin answer $run"' && ok "mbuser's answer arrives as a DM" || ko "Mbin's answer never reached alice"
|
|
|
|
echo " moderation"
|
|
p_locked() { curl -s -H "$PH" "$P/api/v1/statuses/$books_thread_on_p" | j "print((d.get('privapub') or {}).get('locked'))"; }
|
|
mb PUT "moderate/entry/$books_thread/lock" >/dev/null
|
|
until_true 45 '[ "$(p_locked)" = "True" ]' && ok "a moderator's lock reaches PrivaPub" || ko "a moderator's lock did not reach PrivaPub"
|
|
mb PUT "moderate/entry/$books_thread/lock" >/dev/null
|
|
until_true 45 '[ "$(p_locked)" = "False" ]' && ok "a moderator's unlock reaches PrivaPub" || ko "a moderator's unlock did not reach PrivaPub"
|
|
echo " reports"
|
|
# Mbin reads a report's reason from its summary alone (owner decision 2026-10-06): alice's report of mbuser's thread and
|
|
# comment leaves from PrivaPub's reporter, one Flag each with alice's words, and never names her
|
|
mb POST "entry/$books_thread/comments" "{\"body\":\"an Mbin comment to report $run\",\"lang\":\"en\"}" >/dev/null
|
|
mb_comment_on_p() { curl -s -H "$PH" "$P/api/v1/statuses/$books_thread_on_p/context" | j "print(next((s['id'] for s in d['descendants'] if 'an Mbin comment to report $run' in s['content']), ''))"; }
|
|
until_true 45 '[ -n "$(mb_comment_on_p)" ]' && ok "mbuser's comment to report reaches PrivaPub" || ko "mbuser's comment never reached PrivaPub"
|
|
mb_author_on_p=$(curl -s -H "$PH" "$P/api/v1/statuses/$books_thread_on_p" | j "print(d['account']['id'])")
|
|
reason="a pasture report $run"
|
|
[ "$(p_report "$PH" "$mb_author_on_p" "$reason" "$books_thread_on_p" "$(mb_comment_on_p)")" = "True" ] \
|
|
&& ok "PrivaPub says alice's report was forwarded" || ko "PrivaPub did not forward alice's report"
|
|
mb_reports() { mb_sql "select count(*) from report r join \"user\" u on u.id = r.reporting_id where r.reason = '$reason' and u.ap_profile_id = '$P_REPORTER' and r.$1"; }
|
|
until_true 45 '[ "$(mb_reports "entry_id is not null and r.entry_comment_id is null")" = "1" ]' && ok "Mbin keeps the thread's report from PrivaPub's reporter, with alice's words" || ko "Mbin kept no report of the thread with alice's words"
|
|
until_true 45 '[ "$(mb_reports "entry_comment_id is not null")" = "1" ]' && ok "Mbin keeps the comment's report from PrivaPub's reporter, with alice's words" || ko "Mbin kept no report of the comment with alice's words"
|
|
[ "$(mb_sql "select count(*) from report r join \"user\" u on u.id = r.reporting_id where u.ap_profile_id like '%alice_mbin%'")" = "0" ] \
|
|
&& ok "no report on Mbin names alice" || ko "a report on Mbin names alice"
|
|
mb PUT "moderate/entry/$books_thread/trash" >/dev/null
|
|
until_true 45 '[ "$(curl -s -o /dev/null -w "%{http_code}" -H "$PH" "$P/api/v1/statuses/$books_thread_on_p")" = "404" ]' \
|
|
&& ok "a moderator's removal reaches PrivaPub" || ko "a moderator's removal did not reach PrivaPub"
|
|
|
|
echo " unfollow"
|
|
curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/accounts/$books_on_p/unfollow"
|
|
until_true 45 '[ "$(mb_sql "select count(*) from magazine_subscription s join \"user\" u on u.id = s.user_id where u.ap_profile_id like '"'%alice_mbin%'"' and s.magazine_id = $books_on_mb")" = "0" ]' \
|
|
&& ok "alice's unfollow reaches the Mbin magazine" || ko "the Mbin magazine still counts alice"
|
|
|
|
echo " statistics"
|
|
stats_check mbin.test mbin
|