Everything on, phase 2: circle posts for everyone, private posts on signed refetch, browsers past SecureMode

Circles (owner decision 2026-10-04: fix them for compatibility):
- Mastodon 4.7 and GoToSocial drop a post that names none of their accounts, and a circle post named only the circle
  and its /flock. OutboxPublisher.Publish now sends each member a copy that also names that member in `cc`, on the
  activity and on the object, and names no other member. The Create, every Update (edit, poll, quote approval, policy,
  through the new PublishUpdate) and the Delete (StatusService.Remove now uses Publish) all go that way.
- UpdateOf renders with the post's group, so an Update keeps a circle post's `audience` and a community post's `Page`
  and title.
- A reply to a circle post stays in the circle, whichever client wrote it.
- A circle post can no longer quote a post that needs permission: asking would show the circle post to its author.

Posts that are not public, on refetch (SignedFetchAuthorizer.MayRead):
- Followers-only, direct and circle posts are served to a signed request from someone they were for, or from the
  instance actor of a server where one of them lives. That is a follower or an addressed account, an addressed
  account, or a member. Everyone else still gets 404.
- Once deleted they answer those readers 410. Mastodon deletes its copy when a refetch answers 404.
- A circle refetch names the requesting member, or the members on the requesting server, as the delivered copy did.
- /grunts/create-{id} serves the same.
- /peasants/{name}/whispers/{id}, a DM's `context`, was never routed. It is now the conversation's posts, for its
  participants only.

SecureMode lets browsers through to the redirect to the public page, instead of answering them 401.

653 tests pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ELjqpznMFMNrJoJUj6K5p2
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-04 03:01:14 +02:00
1 parent 5f56681c01
commit fcd35f5043
14 files changed
+339 -46

No files matched your search

+3 -5
View File
@@ -165,11 +165,9 @@ until_true 20 '[ "$(gcurl -s -H "$GH" "$G/api/v1/accounts/relationships?id[]=$ci
curl -s -o /dev/null -X POST $P/clientapi/post/insert -H 'Content-Type: application/json' -H "Authorization: Bearer $jwt" \
-d "{\"avatarId\":\"$alice_id\",\"text\":\"only the circle on GoToSocial sees this\",\"groupId\":\"$circle\"}"
circle_uri=$(podman exec pasture-mongo mongosh --quiet PrivaPub --eval 'print(db.Post.findOne({Text:/only the circle on GoToSocial/}).ObjectURI)')
if until_true 15 '[ "$(on_gts "$circle_uri" | j "print(len(d[\"statuses\"]))")" = "1" ]'; then
ok "a circle post reaches its GoToSocial member"
else
xf "a circle post reaches its GoToSocial member (GoToSocial keeps no post addressed only to a collection it does not know)"
fi
# GoToSocial keeps no post addressed only to a collection it does not know, so each member's copy names that member
until_true 15 '[ "$(on_gts "$circle_uri" | j "print(len(d[\"statuses\"]))")" = "1" ]' \
&& ok "a circle post, naming its member, reaches its GoToSocial member" || ko "circle post missing on GoToSocial"
[ "$(pfetch -o /dev/null -w '%{http_code}' -H 'Accept: application/activity+json' "$circle_uri")" = "404" ] && ok "the circle post is not served unsigned" || ko "circle post served unsigned"
echo "locked personas"
+9 -7
View File
@@ -40,6 +40,9 @@ until_true 30 '[ "$(m_status_by_uri "$alice_on_m" "$cw_uri" | j "print(d and d[\
fo_uri=$(curl -s -X POST -H "$AH" $P/api/v1/statuses -d 'status=only for followers&visibility=private' | j "print(d['uri'])")
until_true 30 '[ "$(m_status_by_uri "$alice_on_m" "$fo_uri" | j "print(d and d[\"visibility\"])")" = "private" ]' && ok "a followers-only post reaches Mastodon as private" || ko "followers-only post missing or widened on Mastodon"
[ "$(pfetch -o /dev/null -w '%{http_code}' -H 'Accept: application/activity+json' "$fo_uri")" = "404" ] && ok "the followers-only post is not served unsigned" || ko "followers-only post served unsigned"
# Mastodon deletes its copy when a refetch answers 404, so a signed refetch from a follower's server is answered
fo_refetch=$(podman exec pasture-mastodon bin/rails runner "s = ActivityPub::FetchRemoteStatusService.new.call('$fo_uri'); puts(s.present? ? s.visibility : 'lost')" 2>/dev/null | tail -1)
[ "$fo_refetch" = "private" ] && ok "Mastodon's signed refetch of the followers-only post keeps it private" || ko "Mastodon's refetch of the followers-only post: $fo_refetch"
echo " replies"
mcurl -o /dev/null -X POST -H "$MH" "$M/api/v1/statuses" -d "status=@alice_masto@privapub.test replying from Mastodon&in_reply_to_id=$a_post_on_m&visibility=public"
@@ -144,14 +147,13 @@ circle_post=$(curl -s -X POST $P/clientapi/post/insert -H 'Content-Type: applica
-d "{\"avatarId\":\"$alice_id\",\"text\":\"only the circle sees this\",\"groupId\":\"$circle\"}")
circle_uri=$(podman exec pasture-mongo mongosh --quiet PrivaPub --eval 'print(db.Post.findOne({Text:/only the circle sees this/},{ObjectURI:1}).ObjectURI)')
# Mastodon 4.7 learns whose personal inbox a delivery reached only from /users/<name>/inbox, never from the numeric
# /ap/users/<id>/inbox it now advertises, and keeps a post naming no local account only for that recipient
# (InboxesController#account_required?, Create#addresses_local_accounts?). A circle post names only the circle.
# /ap/users/<id>/inbox it now advertises, and keeps a post naming no local account of its own only for that recipient
# (InboxesController#account_required?, Create#addresses_local_accounts?). So each member's copy names that member.
m_stored() { podman exec pasture-mastodon bin/rails runner "puts Status.exists?(uri: '$1')" 2>/dev/null | tail -1; }
if until_true 15 '[ "$(m_stored "$circle_uri")" = "true" ]'; then
ok "a circle post reaches its Mastodon member"
else
xf "a circle post reaches its Mastodon member (Mastodon 4.7 loses the recipient of numeric-inbox deliveries; owner decision pending)"
fi
until_true 15 '[ "$(m_stored "$circle_uri")" = "true" ]' && ok "a circle post, naming its member, reaches its Mastodon member" || ko "circle post missing on Mastodon"
# a signed refetch, as Mastodon does it, is answered for a member's server and names the member, so the copy stays
refetched=$(podman exec pasture-mastodon bin/rails runner "s = ActivityPub::FetchRemoteStatusService.new.call('$circle_uri'); puts(s.present? ? 'kept' : 'lost')" 2>/dev/null | tail -1)
[ "$refetched" = "kept" ] && ok "Mastodon's signed refetch of the circle post keeps it" || ko "Mastodon's refetch of the circle post failed ($refetched)"
mastodon_user outsider
OT=$(mastodon_token outsider)
alice_on_m_o=$(mcurl -H "Authorization: Bearer $OT" "$M/api/v2/search?q=@alice_masto@privapub.test&resolve=true&type=accounts" | j "print(d['accounts'][0]['id'])")