Files
thepraandClaude Opus 5.5 f105d1f7ea
Build / Build (push) Failing after 7m51s
Deploy / privapub.thepra.dev (push) Successful in 7m58s
PieFed and Mbin take reports from the reporter too
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
2026-10-06 19:24:09 +02:00

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