From a85ebd30c0b3907a4c9c424f257276653ecf2c20 Mon Sep 17 00:00:00 2001 From: thepra Date: Mon, 5 Oct 2026 17:32:22 +0200 Subject: [PATCH] Pasture: Lemmy 0.19 joins, built to trust the pasture's CA Most of the threadiverse runs Lemmy 0.19, whose release trusts only the roots its rustls bundles. images/lemmy19 builds 0.19.20 from its tag with reqwest's rustls-tls-native-roots added, and the pasture runs it as lemmy19.test. scenarios/lemmy19.sh passes 27 checks with no change to PrivaPub (communities, threads, comments, votes, its private message, moderation) and one known gap: 0.19 takes private messages only as ChatMessage and answers our direct Note 400, like Mbin, so G-0008 now covers both and waits for the owner. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw --- CLAUDE.md | 5 + FEDERATION.md | 2 +- docs/INTEROP.md | 10 +- docs/ROADMAP.md | 6 +- tools/pasture/Caddyfile | 5 + tools/pasture/images/lemmy19/Containerfile | 13 +++ tools/pasture/peers/lemmy19.sh | 44 ++++++++ tools/pasture/scenarios/lemmy19.sh | 113 +++++++++++++++++++++ tools/pasture/town/gaps.json | 8 +- 9 files changed, 197 insertions(+), 9 deletions(-) create mode 100644 tools/pasture/images/lemmy19/Containerfile create mode 100644 tools/pasture/peers/lemmy19.sh create mode 100644 tools/pasture/scenarios/lemmy19.sh diff --git a/CLAUDE.md b/CLAUDE.md index 47e222e..012c85c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -547,6 +547,11 @@ tools/pasture/run.sh down # removes e API follows one only when named by its handle; a chat room is made, then written in. `scenarios/nodebb.sh`, 23 checks: a category followed and its topic as a titled thread, replies both ways, nbuser following alice, votes both ways, an edit and a deletion, a chat both ways, the unfollow, statistics. +- **Lemmy 0.19 (0.19.20), as `lemmy19.test`:** what most of the threadiverse runs. Its release trusts only the roots its + rustls bundles, so `images/lemmy19` builds the tag with reqwest's `rustls-tls-native-roots` added (about ten minutes + the first time) and the pasture's bundle is mounted as the system's. Its config names the database as `uri` (1.0: + `connection`) and its API is v3 (`/api/v3`, `sort=New`, `resolve_object` answering views). It takes private messages + only as `ChatMessage` (G-0008). `scenarios/lemmy19.sh`, 27 checks and that gap. - **Hollo (0.9.19):** Fedify's microblog server on the shared Postgres, set up through its web form (which checks `Origin` against `Host`, so the request names `hollo.test` without the port). It needs a 44-character `SECRET_KEY`, a media directory and a `themeColor`; statuses and votes go as JSON. Town only, no scenario. diff --git a/FEDERATION.md b/FEDERATION.md index 82f72bd..4d34337 100644 --- a/FEDERATION.md +++ b/FEDERATION.md @@ -21,7 +21,7 @@ and every run starting clean, with signed fetches required (as privapub.thepra.d - **Misskey 2026.10** - **Sharkey 2025.4.7** - **Akkoma 3.20.1** -- **Lemmy 1.0.0-beta.2** +- **Lemmy 1.0.0-beta.2**, and **Lemmy 0.19.20** (built with the platform's roots, so it trusts the pasture's CA) - **PeerTube 8.3.1** - **Pixelfed 0.14.4** - **WordPress 6 with ActivityPub 9.3.1** diff --git a/docs/INTEROP.md b/docs/INTEROP.md index 8a214ba..8d92fc0 100644 --- a/docs/INTEROP.md +++ b/docs/INTEROP.md @@ -554,7 +554,15 @@ What it showed: minute after the server is first seen, at the newest activity and skips everything older. On a clean pasture its first Follow of our community was lost, so the scenario waits for the worker (`federation_queue_state`). On the public network the same applies to the first thing a Lemmy sends to a PrivaPub it has just discovered. -- 0.19 cannot join the pasture: its rustls trusts only its bundled roots, never Caddy's CA. +- 0.19's release trusts only the roots its rustls bundles, never Caddy's CA, so the pasture builds 0.19.20 from its tag + with reqwest's `rustls-tls-native-roots` added (`tools/pasture/images/lemmy19`) and runs it as `lemmy19.test`. +- **0.19 takes private messages only as `ChatMessage`** (`ChatMessageType` has no `Note`) and answers our direct + `Note` 400. Most of the threadiverse runs 0.19, so this matters more than Mbin's same rule (G-0008). +- **Pasture evidence (2026-10-05, Lemmy 0.19.20, `tools/pasture/scenarios/lemmy19.sh`):** 27 checks pass and one gap is + expected (G-0008, direct messages to it), with no change to PrivaPub: communities both ways, threads with titles, + comments both ways, votes up and down both ways (relayed in the community's announces, as 1.0 does), its private + message to alice, a moderator's lock (replies then refused), unlock, ban and unban (`blocked_by`) and removal, + statistics. ### PieFed 1.7.17 and Mbin 1.10.1 diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 20c4bc6..becf3ea 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -69,11 +69,11 @@ Written 2026-10-01 from the original 2023 code, the decePubClient UI, a federati Funkwhale serve it; Funkwhale's answers named after our follow and its deletions of several uploads. - GoToSocial's interaction policies both ways; personas join and leave remote events; third-party replies to a persona's posts passed on to its followers (owner decisions 2026-10-05). - - wave 2, under way: PieFed, Mbin and NodeBB in the pasture with scenarios (2026-10-05). What they showed and was fixed: the + - wave 2, under way: PieFed, Mbin, NodeBB and Lemmy 0.19 in the pasture with scenarios (2026-10-05). What they showed and was fixed: the instance actor answers at the server's root, where PieFed looks for the inbox it announces to; a community's removal of a post on its own server is believed at once; a followed group's post its own server sends without announcing - it is kept, and a bare `Lock` from the post's server is taken (Mbin). Open: direct messages to Mbin, which takes - them only as `ChatMessage` (G-0008, waits for the owner). + it is kept, and a bare `Lock` from the post's server is taken (Mbin). Open: direct messages to Lemmy 0.19 and Mbin, + which take them only as `ChatMessage` (G-0008, waits for the owner). - [ ] P7 Threads, communities, moderation, the social graph - [ ] P8 Signatures, discovery, the long tail - [x] P9 Reading at volume (owner decision 2026-10-04, back from "Cut"): lists, server-side filters, scheduled posts, diff --git a/tools/pasture/Caddyfile b/tools/pasture/Caddyfile index 192a983..bd47373 100644 --- a/tools/pasture/Caddyfile +++ b/tools/pasture/Caddyfile @@ -107,3 +107,8 @@ nodebb.test { tls internal reverse_proxy pasture-nodebb:4567 } + +lemmy19.test { + tls internal + reverse_proxy pasture-lemmy19:8536 +} diff --git a/tools/pasture/images/lemmy19/Containerfile b/tools/pasture/images/lemmy19/Containerfile new file mode 100644 index 0000000..5d045bc --- /dev/null +++ b/tools/pasture/images/lemmy19/Containerfile @@ -0,0 +1,13 @@ +# Lemmy 0.19.20, built from its tag with one change: reqwest's rustls also trusts the platform's roots +# (rustls-tls-native-roots), where the pasture's CA is, besides the roots it bundles. The release image trusts only +# those, so 0.19 could not reach any pasture peer. +FROM docker.io/library/rust:1.81-bookworm AS build +RUN git clone --depth 1 --branch 0.19.20 --recurse-submodules https://github.com/LemmyNet/lemmy.git /lemmy +WORKDIR /lemmy +RUN sed -i 's/^ "rustls-tls",$/ "rustls-tls",\n "rustls-tls-native-roots",/' Cargo.toml && grep -q '"rustls-tls-native-roots"' Cargo.toml +RUN cargo build --release && strip target/release/lemmy_server + +FROM docker.io/library/debian:bookworm-slim +RUN apt-get update && apt-get install -y --no-install-recommends libpq5 ca-certificates && rm -rf /var/lib/apt/lists/* +COPY --from=build /lemmy/target/release/lemmy_server /usr/local/bin/lemmy_server +ENTRYPOINT ["/usr/local/bin/lemmy_server"] diff --git a/tools/pasture/peers/lemmy19.sh b/tools/pasture/peers/lemmy19.sh new file mode 100644 index 0000000..81c4d3e --- /dev/null +++ b/tools/pasture/peers/lemmy19.sh @@ -0,0 +1,44 @@ +# Lemmy 0.19.20, the version most of the threadiverse runs, as lemmy19.test: the backend alone on the shared Postgres, +# built by images/lemmy19 from its tag with reqwest also trusting the platform's roots (the release trusts only the ones +# it bundles), and given the pasture's bundle as the platform's. Its API is v3. +LEMMY19_IMAGE=${LEMMY19_IMAGE:-localhost/pasture-lemmy19:0.19.20} +. "$here/peers/shared.sh" + +lemmy19_up() { + podman image exists "$LEMMY19_IMAGE" || podman build -q -t "$LEMMY19_IMAGE" "$here/images/lemmy19" >/dev/null + shared_postgres_up + pg_db lemmy19 + mkdir -p "$here/.state/lemmy19" + cat > "$here/.state/lemmy19/config.hjson" </dev/null + for _ in $(seq 1 90); do + site lemmy19.test -s -o /dev/null -w '%{http_code}' https://lemmy19.test:6443/api/v3/site 2>/dev/null | grep -q 200 && break + sleep 2 + done + # Lemmy's default limits would throttle a scripted run; it reads them again when its site is edited + podman exec pasture-postgres psql -U pasture -d lemmy19 -qc "update local_site_rate_limit set message=100000, message_per_second=1, + post=100000, post_per_second=1, register=100000, register_per_second=1, image=100000, image_per_second=1, + comment=100000, comment_per_second=1, search=100000, search_per_second=1" >/dev/null + site lemmy19.test -s -X POST https://lemmy19.test:6443/api/v3/user/login -H 'Content-Type: application/json' \ + -d '{"username_or_email":"lemmyuser","password":"Lemmy-Pasture-Pass-1"}' \ + | python3 -c "import sys,json; print(json.load(sys.stdin)['jwt'])" > "$here/.state/lemmy19.token" 2>/dev/null || true + site lemmy19.test -s -o /dev/null -X PUT https://lemmy19.test:6443/api/v3/site -H "Authorization: Bearer $(cat "$here/.state/lemmy19.token")" \ + -H 'Content-Type: application/json' -d '{"registration_mode":"Open","require_email_verification":false,"captcha_enabled":false}' + echo "lemmy19: https://lemmy19.test:6443" +} diff --git a/tools/pasture/scenarios/lemmy19.sh b/tools/pasture/scenarios/lemmy19.sh new file mode 100644 index 0000000..ecd02a2 --- /dev/null +++ b/tools/pasture/scenarios/lemmy19.sh @@ -0,0 +1,113 @@ +# 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 [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" +# (0.19 takes a private message only as a ChatMessage and answers alice's direct Note 400, G-0008; 1.0 takes a Note) +curl -s -o /dev/null -X POST -H "$LAH" $P/api/v1/statuses -d 'status=@lemmyuser@lemmy19.test a secret from PrivaPub&visibility=direct' +until_true 30 'lm GET "private_message/list?limit=50" | grep -q "a secret from PrivaPub"' && ok "alice's DM arrives as a Lemmy 0.19 private message" \ + || xf "alice's DM never reaches Lemmy 0.19, which takes private messages only as ChatMessage (G-0008)" + +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" +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 diff --git a/tools/pasture/town/gaps.json b/tools/pasture/town/gaps.json index 5a2a841..16f9126 100644 --- a/tools/pasture/town/gaps.json +++ b/tools/pasture/town/gaps.json @@ -110,16 +110,16 @@ }, { "id": "G-0008", - "title": "A direct message to an Mbin account never arrives: Mbin takes a private message only as a ChatMessage", + "title": "A direct message to a Lemmy 0.19 or Mbin account never arrives: both take a private message only as a ChatMessage", "match": { "feature": "deliver\\.direct", - "observer": "mbin" + "observer": "(mbin|lemmy19)" }, "kind": "server", "phase": "P3", - "code": "mbin 1.10.1 src/Service/ActivityPub/ActivityPubContent.php getVisibility(): a Note neither public nor to the author's followers throws 'PM: not implemented'; only CreateHandler's ChatMessage branch makes a private message", + "code": "lemmy 0.19.20 crates/apub/src/protocol/objects/chat_message.rs: ChatMessageType has ChatMessage alone, so a Create{Note} to a person answers 400 (Lemmy 1.0 takes a Note); mbin 1.10.1 src/Service/ActivityPub/ActivityPubContent.php getVisibility(): a Note neither public nor to the author's followers throws 'PM: not implemented'", "opened": "2026-10-05", "status": "open", - "note": "Mbin's actors advertise nothing that says so, and PrivaPub never decides by a server's software name, so choosing ChatMessage for a recipient waits for the owner. Mbin's API also never starts a conversation with an account elsewhere, so nothing comes the other way through it." + "note": "Neither server's actors say so, and PrivaPub never decides by a server's software name, so choosing ChatMessage for a recipient waits for the owner. Pleroma and Akkoma file a ChatMessage as a chat apart from direct messages, so sending both would show them twice. Mbin's API also never starts a conversation with an account elsewhere." } ]