Files
SocialPub/tools/pasture/scenarios/lemmy19.sh
T
thepraandClaude Opus 5.5 f66c280b0b Reports reach Lemmy's moderators, from an anonymous reporter
Lemmy takes a report only from a person or a service, about one post or comment, addressed to its community, and it
answered PrivaPub's Flag (the instance actor's, an Application, with no `to` and the account and posts as its object)
400. A report of a post or comment in a community on a server whose NodeInfo names Lemmy now leaves from
`privapub_reports`, a Service with its own key that names nobody: one Flag per post, `to` the community (its own
audience, else its thread's), with the persona's words, or the category, in `summary` and `content`, sent to the
community's inbox. This is the second exception to "a server's software is for display" (owner decision 2026-10-06,
`ReportService.ServiceReportTakers`). Every other server keeps the instance actor's report. An account alone is not
reported to Lemmy, which takes no such report, and `forwarded` now says whether anything left.

The reporter is read unsigned in SecureMode and answers WebFinger like the instance actor. Nobody follows or mentions
it, the Mastodon API has no account for it, and a migration reserves its name. Checked live: Lemmy 1.0 and 0.19 keep the
reports of a thread and of a comment, with alice's words, from "Reports from privapub.test", and none names her (69
checks). A sweep of every scenario with this and the next commit: 876 checks pass; Ghost's Network feed listed alice's
post too late once, and Ghost passes alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
2026-10-06 19:23:17 +02:00

139 lines
14 KiB
Bash

# Lemmy 0.19.20, what most of the threadiverse runs: communities both ways (FEP-1b12), threads with titles, comments both
# ways, votes up and down both ways, private messages both ways, a moderator's lock, ban and removal, statistics. Driven
# through its v3 API; scenarios/lemmy.sh does the same against Lemmy 1.0.
LM=https://lemmy19.test:6443
LT=$(cat "$here/.state/lemmy19.token" 2>/dev/null)
# lm <method> <path> [json]: a Lemmy v3 API call as lemmyuser
lm() {
local method=$1 path=$2 body=${3:-}
if [ -n "$body" ]; then
site lemmy19.test -s -X "$method" "$LM/api/v3/$path" -H "Authorization: Bearer $LT" -H 'Content-Type: application/json' -d "$body"
else
site lemmy19.test -s -X "$method" "$LM/api/v3/$path" -H "Authorization: Bearer $LT"
fi
}
lm_posts() { lm GET "post/list?community_id=$1&sort=New&limit=50" | j "print(json.dumps([p['post'] for p in d.get('posts', [])]))"; }
lm_post_by_ap() { lm_posts "$1" | j "print(json.dumps(next((p for p in d if p['ap_id']=='$2'), None)))"; }
lm_worker() { podman exec pasture-postgres psql -U pasture -d lemmy19 -tAc "select count(*) from federation_queue_state q join instance i on i.id=q.instance_id where i.domain='privapub.test'"; }
echo "lemmy19"
[ -n "$LT" ] && ok "Lemmy 0.19 token for lemmyuser" || { ko "Lemmy 0.19 token"; return 1; }
LAT=$(privapub_token alice_lemmy19)
LAH="Authorization: Bearer $LAT"
[ -n "$LAT" ] && ok "PrivaPub token for alice_lemmy19" || { ko "PrivaPub token for alice_lemmy19"; return 1; }
jwt=$(privapub_root)
alice_id=$(curl -s -H "$LAH" "$P/api/v1/accounts/verify_credentials" | j "print(d['id'])")
run=$(date +%s)
dogs="ldogs$run"
cats="lcats$run"
echo " communities"
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_lm=$(lm GET "resolve_object?q=!$dogs@privapub.test" | j "print(d['community']['community']['id'])")
[ -n "$dogs_on_lm" ] && ok "Lemmy 0.19 resolves a PrivaPub community" || ko "Lemmy 0.19 cannot resolve the PrivaPub community"
# (a newly seen server gets its send worker within a minute; what was queued before it starts is never sent)
until_true 45 '[ "$(lm_worker)" = "1" ]' || true
lm POST community/follow "{\"community_id\":$dogs_on_lm,\"follow\":true}" >/dev/null
until_true 45 '[ "$(lm GET "community?id=$dogs_on_lm" | j "print(d[\"community_view\"][\"subscribed\"])")" = "Subscribed" ]' \
&& ok "lemmyuser follows the PrivaPub community (Accept arrived)" || ko "Lemmy 0.19's community follow not accepted"
cats_on_lm=$(lm POST community "{\"name\":\"$cats\",\"title\":\"Pasture cats\"}" | j "print(d['community_view']['community']['id'])")
cats_on_p=$(curl -s -H "$LAH" "$P/api/v2/search?q=@$cats@lemmy19.test&resolve=true&type=accounts" | j "print(d['accounts'][0]['id'])")
[ -n "$cats_on_p" ] && ok "PrivaPub resolves a Lemmy 0.19 community" || ko "PrivaPub cannot resolve the Lemmy 0.19 community"
curl -s -o /dev/null -X POST -H "$LAH" "$P/api/v1/accounts/$cats_on_p/follow"
until_true 45 '[ "$(curl -s -H "$LAH" "$P/api/v1/accounts/relationships?id[]=$cats_on_p" | j "print(d[0][\"following\"])")" = "True" ]' \
&& ok "alice follows the Lemmy 0.19 community (Accept arrived)" || ko "PrivaPub's community follow not accepted"
echo " threads"
lm_thread=$(lm POST post "{\"name\":\"A Lemmy 0.19 thread\",\"body\":\"posted from Lemmy 0.19 into PrivaPub\",\"community_id\":$dogs_on_lm}" | j "print(d['post_view']['post']['ap_id'])")
dogs_on_p=$(curl -s -H "$LAH" "$P/api/v1/accounts/lookup?acct=$dogs" | j "print(d['id'])")
until_true 45 '[ "$(curl -s -H "$LAH" "$P/api/v1/accounts/$dogs_on_p/statuses" | j "print(any(s[\"uri\"]==\"$lm_thread\" and (s.get(\"privapub\") or {}).get(\"title\")==\"A Lemmy 0.19 thread\" for s in d))")" = "True" ]' \
&& ok "a Lemmy 0.19 thread in the PrivaPub community arrives with its title" || ko "Lemmy 0.19'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 Lemmy 0.19\",\"groupId\":\"$dogs_gid\"}"
dogs_post=$(podman exec pasture-mongo mongosh --quiet PrivaPub --eval 'print(db.Post.findOne({Text:/posted into our own community for Lemmy 0.19/}, {}, {sort:{_id:-1}}).ObjectURI)')
until_true 45 '[ "$(lm_post_by_ap "$dogs_on_lm" "$dogs_post" | j "print(d and d[\"name\"])")" = "A PrivaPub community thread $run" ]' \
&& ok "a titled post in the PrivaPub community reaches Lemmy 0.19 through its announce" || ko "PrivaPub community post missing on Lemmy 0.19"
lm_cats_thread=$(lm POST post "{\"name\":\"Cats only $run\",\"body\":\"a Lemmy 0.19 community post\",\"community_id\":$cats_on_lm}" | j "print(d['post_view']['post']['ap_id'])")
p_home_has() { curl -s -H "$LAH" "$P/api/v1/timelines/home?limit=40" | j "print(next(((s.get('reblog') or s)['id'] for s in d if (s.get('reblog') or s)['uri']=='$1'), ''))"; }
until_true 45 '[ -n "$(p_home_has "$lm_cats_thread")" ]' && ok "the Lemmy 0.19 community's announce brings its thread to alice's home" || ko "Lemmy 0.19 community thread missing from alice's home"
lm_cats_on_p=$(p_home_has "$lm_cats_thread")
p_thread=$(curl -s -X POST -H "$LAH" $P/api/v1/statuses --data-urlencode "status=A PrivaPub thread
@$cats@lemmy19.test posted from PrivaPub" -d 'visibility=public')
p_thread_id=$(echo "$p_thread" | j "print(d['id'])"); p_thread_uri=$(echo "$p_thread" | j "print(d['uri'])")
until_true 45 '[ "$(lm_post_by_ap "$cats_on_lm" "$p_thread_uri")" != "null" ]' && ok "alice's thread lands in the Lemmy 0.19 community" || ko "PrivaPub thread missing from the Lemmy 0.19 community"
p_thread_on_lm=$(lm_post_by_ap "$cats_on_lm" "$p_thread_uri" | j "print(d['id'])")
echo " comments"
lm POST comment "{\"post_id\":$p_thread_on_lm,\"content\":\"a Lemmy 0.19 comment on PrivaPub\"}" >/dev/null
until_true 45 '[ "$(curl -s -H "$LAH" "$P/api/v1/statuses/$p_thread_id/context" | j "print(any(\"a Lemmy 0.19 comment on PrivaPub\" in s[\"content\"] for s in d[\"descendants\"]))")" = "True" ]' \
&& ok "Lemmy 0.19's comment threads under alice's post" || ko "Lemmy 0.19's comment missing on PrivaPub"
curl -s -o /dev/null -X POST -H "$LAH" $P/api/v1/statuses -d "status=a PrivaPub comment on Lemmy 0.19&in_reply_to_id=$lm_cats_on_p&visibility=public"
lm_cats_id=$(lm GET "resolve_object?q=$lm_cats_thread" | j "print(d['post']['post']['id'])")
until_true 45 '[ "$(lm GET "comment/list?post_id=$lm_cats_id" | j "print(any(\"a PrivaPub comment on Lemmy 0.19\" in c[\"comment\"][\"content\"] for c in d.get(\"comments\", [])))")" = "True" ]' \
&& ok "alice's reply becomes a comment on Lemmy 0.19" || ko "PrivaPub reply missing as a Lemmy 0.19 comment"
echo " votes"
lm POST post/like "{\"post_id\":$p_thread_on_lm,\"score\":1}" >/dev/null
until_true 60 '[ "$(curl -s -H "$LAH" "$P/api/v1/statuses/$p_thread_id" | j "print(d[\"favourites_count\"])")" = "1" ]' \
&& ok "Lemmy 0.19's upvote counts as a like on PrivaPub" || ko "Lemmy 0.19's relayed upvote not counted"
lm POST post/like "{\"post_id\":$p_thread_on_lm,\"score\":-1}" >/dev/null
until_true 60 '[ "$(curl -s -H "$LAH" "$P/api/v1/statuses/$p_thread_id" | j "print(d[\"favourites_count\"], ((d.get(\"privapub\") or {}).get(\"votes\") or {}).get(\"down\"))")" = "0 1" ]' \
&& ok "Lemmy 0.19's change to a downvote reaches PrivaPub" || ko "Lemmy 0.19's change to a downvote not applied"
curl -s -o /dev/null -X POST -H "$LAH" "$P/api/v1/statuses/$lm_cats_on_p/favourite"
until_true 45 '[ "$(lm GET "post?id=$lm_cats_id" | j "print(d[\"post_view\"][\"counts\"][\"upvotes\"])")" = "2" ]' && ok "alice's like is an upvote on Lemmy 0.19" || ko "like not counted as an upvote on Lemmy 0.19"
echo " private messages"
alice_on_lm=$(lm GET "resolve_object?q=@alice_lemmy19@privapub.test" | j "print(d['person']['person']['id'])")
lm POST private_message "{\"content\":\"a secret from Lemmy 0.19\",\"recipient_id\":$alice_on_lm}" >/dev/null
until_true 45 'curl -s -H "$LAH" "$P/api/v1/conversations" | grep -q "a secret from Lemmy 0.19"' && ok "Lemmy 0.19's private message arrives as a DM" || ko "Lemmy 0.19's private message missing on PrivaPub"
# lemmyuser wrote as a ChatMessage, so alice's answer goes out as one
curl -s -o /dev/null -X POST -H "$LAH" $P/api/v1/statuses -d "status=@lemmyuser@lemmy19.test an answer from PrivaPub $run&visibility=direct"
until_true 45 'lm GET "private_message/list?limit=50" | grep -q "an answer from PrivaPub $run"' && ok "alice's answer reaches Lemmy 0.19 as a ChatMessage" || ko "alice's answer never reached Lemmy 0.19"
# another persona writing to lemmyuser, who writes to us as ChatMessages, does so in kind too
LBT=$(privapub_token bob_lemmy19)
curl -s -o /dev/null -X POST -H "Authorization: Bearer $LBT" $P/api/v1/statuses -d "status=@lemmyuser@lemmy19.test a word from bob $run&visibility=direct"
until_true 45 'lm GET "private_message/list?limit=50" | grep -q "a word from bob $run"' && ok "bob's DM to lemmyuser goes as a ChatMessage too" || ko "bob's DM never reached Lemmy 0.19"
# (0.19 takes a private message only as a ChatMessage and answers a direct Note 400: a first message to someone who
# never wrote to anyone here goes as a ChatMessage because NodeInfo names Lemmy before 1.0, owner decision 2026-10-05)
quiet="quiet$run"
QT=$(site lemmy19.test -s -X POST "$LM/api/v3/user/register" -H 'Content-Type: application/json' \
-d "{\"username\":\"$quiet\",\"password\":\"Lemmy-Pasture-Pass-1\",\"password_verify\":\"Lemmy-Pasture-Pass-1\",\"show_nsfw\":false}" | j "print(d['jwt'])")
curl -s -o /dev/null -X POST -H "$LAH" $P/api/v1/statuses -d "status=@$quiet@lemmy19.test a first word from PrivaPub $run&visibility=direct"
until_true 30 'site lemmy19.test -s "$LM/api/v3/private_message/list?limit=50" -H "Authorization: Bearer $QT" | grep -q "a first word from PrivaPub $run"' \
&& ok "alice's first DM to someone there arrives as a ChatMessage" || ko "alice's first DM to someone there never arrived"
echo " moderation"
p_locked() { curl -s -H "$LAH" "$P/api/v1/statuses/$lm_cats_on_p" | j "print((d.get('privapub') or {}).get('locked'))"; }
p_reply() { curl -s -o /dev/null -w '%{http_code}' -X POST -H "$LAH" $P/api/v1/statuses -d "status=$1&in_reply_to_id=$lm_cats_on_p&visibility=public"; }
p_banned() { curl -s -H "$LAH" "$P/api/v1/accounts/relationships?id[]=$cats_on_p" | j "print(d[0]['blocked_by'])"; }
lm POST post/lock "{\"post_id\":$lm_cats_id,\"locked\":true}" >/dev/null
until_true 45 '[ "$(p_locked)" = "True" ]' && ok "a moderator's lock reaches PrivaPub" || ko "a moderator's lock did not reach PrivaPub"
[ "$(p_reply "too late")" = "422" ] && ok "a reply to the locked thread is refused" || ko "a reply to the locked thread was taken"
lm POST post/lock "{\"post_id\":$lm_cats_id,\"locked\":false}" >/dev/null
until_true 45 '[ "$(p_locked)" = "False" ]' && ok "a moderator's unlock reaches PrivaPub" || ko "a moderator's unlock did not reach PrivaPub"
lm POST community/ban_user "{\"community_id\":$cats_on_lm,\"person_id\":$alice_on_lm,\"ban\":true,\"reason\":\"pasture\"}" >/dev/null
until_true 45 '[ "$(p_banned)" = "True" ]' && ok "the community's ban of alice_lemmy19 reaches PrivaPub (blocked_by)" || ko "the community's ban did not reach PrivaPub"
[ "$(p_reply "while banned")" = "422" ] && ok "a banned persona's reply in the community is refused" || ko "a banned persona's reply was taken"
lm POST community/ban_user "{\"community_id\":$cats_on_lm,\"person_id\":$alice_on_lm,\"ban\":false,\"reason\":\"pasture\"}" >/dev/null
until_true 45 '[ "$(p_banned)" = "False" ]' && ok "the community's unban reaches PrivaPub" || ko "the community's unban did not reach PrivaPub"
echo " reports"
# as on Lemmy 1.0: the report leaves from PrivaPub's reporter, one Flag for the thread and one for the comment
lm POST comment "{\"post_id\":$lm_cats_id,\"content\":\"a Lemmy 0.19 comment to report\"}" >/dev/null
lm_comment_on_p() { curl -s -H "$LAH" "$P/api/v1/statuses/$lm_cats_on_p/context" | j "print(next((s['id'] for s in d['descendants'] if 'a Lemmy 0.19 comment to report' in s['content']), ''))"; }
until_true 45 '[ -n "$(lm_comment_on_p)" ]' && ok "lemmyuser's comment to report reaches PrivaPub" || ko "lemmyuser's comment never reached PrivaPub"
lm_author_on_p=$(curl -s -H "$LAH" "$P/api/v1/statuses/$lm_cats_on_p" | j "print(d['account']['id'])")
reason="a pasture report $(date +%s)"
[ "$(p_report "$LAH" "$lm_author_on_p" "$reason" "$lm_cats_on_p" "$(lm_comment_on_p)")" = "True" ] \
&& ok "PrivaPub says alice's report was forwarded" || ko "PrivaPub did not forward alice's report"
lm_reports() { podman exec pasture-postgres psql -U pasture -d lemmy19 -tAc "select count(*) from $1 r join person p on p.id = r.creator_id where r.reason = '$reason' and p.actor_id = '$P_REPORTER'"; }
until_true 45 '[ "$(lm_reports post_report)" = "1" ]' && ok "Lemmy 0.19 keeps the thread's report from PrivaPub's reporter, with alice's words" || ko "Lemmy 0.19 kept no report of the thread"
until_true 45 '[ "$(lm_reports comment_report)" = "1" ]' && ok "Lemmy 0.19 keeps the comment's report from PrivaPub's reporter, with alice's words" || ko "Lemmy 0.19 kept no report of the comment"
[ "$(podman exec pasture-postgres psql -U pasture -d lemmy19 -tAc "select count(*) from post_report r join person p on p.id = r.creator_id where p.actor_id like '%alice_lemmy19%'")" = "0" ] \
&& ok "no report on Lemmy 0.19 names alice" || ko "a report on Lemmy 0.19 names alice"
lm POST post/remove "{\"post_id\":$lm_cats_id,\"removed\":true,\"reason\":\"pasture\"}" >/dev/null
until_true 45 '[ -z "$(p_home_has "$lm_cats_thread")" ]' && ok "a moderator's removal reaches PrivaPub" || ko "a moderator's removal did not reach PrivaPub"
echo " statistics"
stats_check lemmy19.test lemmy