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
181 lines
20 KiB
Bash
181 lines
20 KiB
Bash
# Mastodon 4.7: discovery, follows, posts, CW, replies both ways, likes and boosts with their undos, DMs, polls,
|
|
# FEP-044f quotes both ways, edits and deletes both ways, media with alt text, locked follows, blocks, statistics.
|
|
M=https://mastodon.test:6443
|
|
mcurl() { curl -sk --resolve mastodon.test:6443:127.0.0.1 "$@"; }
|
|
. "$here/peers/mastodon.sh"
|
|
|
|
echo "mastodon"
|
|
MT=$(mastodon_token)
|
|
MH="Authorization: Bearer $MT"
|
|
[ -n "$MT" ] && ok "Mastodon token for mastouser" || { ko "Mastodon token"; return 1; }
|
|
AT=$(privapub_token alice_masto)
|
|
AH="Authorization: Bearer $AT"
|
|
[ -n "$AT" ] && ok "PrivaPub token for alice_masto" || { ko "PrivaPub token for alice_masto"; return 1; }
|
|
# what Mastodon holds of an account, newest first; and the one status whose uri is given
|
|
m_statuses() { mcurl -H "$MH" "$M/api/v1/accounts/$1/statuses?limit=40"; }
|
|
m_status_by_uri() { m_statuses "$1" | j "print(json.dumps(next((s for s in d if s['uri']=='$2'), None)))"; }
|
|
|
|
echo " discovery"
|
|
alice_on_m=$(mcurl -H "$MH" "$M/api/v2/search?q=@alice_masto@privapub.test&resolve=true&type=accounts" | j "print(d['accounts'][0]['id'])")
|
|
[ -n "$alice_on_m" ] && ok "Mastodon resolves @alice_masto@privapub.test" || ko "Mastodon cannot resolve alice_masto"
|
|
masto_on_p=$(curl -s -H "$AH" "$P/api/v2/search?q=mastouser@mastodon.test&resolve=true&type=accounts" | j "print(d['accounts'][0]['id'])")
|
|
[ -n "$masto_on_p" ] && ok "PrivaPub resolves @mastouser@mastodon.test" || ko "PrivaPub cannot resolve mastouser"
|
|
|
|
echo " follows"
|
|
mcurl -o /dev/null -X POST -H "$MH" "$M/api/v1/accounts/$alice_on_m/follow"
|
|
until_true 30 '[ "$(mcurl -H "$MH" "$M/api/v1/accounts/relationships?id[]=$alice_on_m" | j "print(d[0][\"following\"])")" = "True" ]' && ok "mastouser follows alice_masto (Accept arrived)" || ko "Mastodon's follow not accepted"
|
|
curl -s -o /dev/null -X POST -H "$AH" "$P/api/v1/accounts/$masto_on_p/follow"
|
|
until_true 30 '[ "$(curl -s -H "$AH" "$P/api/v1/accounts/relationships?id[]=$masto_on_p" | j "print(d[0][\"following\"])")" = "True" ]' && ok "alice_masto follows mastouser (Accept arrived)" || ko "PrivaPub's follow not accepted"
|
|
|
|
echo " posts"
|
|
a_post=$(curl -s -X POST -H "$AH" $P/api/v1/statuses -d 'status=Hello Mastodon from PrivaPub&visibility=public')
|
|
a_post_id=$(echo "$a_post" | j "print(d['id'])"); a_post_uri=$(echo "$a_post" | j "print(d['uri'])")
|
|
until_true 30 '[ "$(m_status_by_uri "$alice_on_m" "$a_post_uri")" != "null" ]' && ok "alice_masto's post reaches Mastodon" || ko "post missing on Mastodon"
|
|
a_post_on_m=$(m_status_by_uri "$alice_on_m" "$a_post_uri" | j "print(d['id'])")
|
|
m_post=$(mcurl -X POST -H "$MH" "$M/api/v1/statuses" -d 'status=Hello PrivaPub from Mastodon&visibility=public' | j "print(d['id'])")
|
|
until_true 30 'curl -s -H "$AH" "$P/api/v1/timelines/home" | grep -q "Hello PrivaPub from Mastodon"' && ok "mastouser's post reaches alice_masto's home" || ko "Mastodon's post missing on PrivaPub"
|
|
m_post_on_p=$(curl -s -H "$AH" "$P/api/v1/timelines/home" | j "print(next(s['id'] for s in d if 'Hello PrivaPub from Mastodon' in s['content']))")
|
|
cw_uri=$(curl -s -X POST -H "$AH" $P/api/v1/statuses -d 'status=behind a warning&spoiler_text=spoilers&visibility=public' | j "print(d['uri'])")
|
|
until_true 30 '[ "$(m_status_by_uri "$alice_on_m" "$cw_uri" | j "print(d and d[\"spoiler_text\"]==\"spoilers\" and d[\"sensitive\"])")" = "True" ]' && ok "a content warning survives to Mastodon" || ko "content warning lost on Mastodon"
|
|
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"
|
|
until_true 30 'curl -s -H "$AH" "$P/api/v1/notifications" | j "print(any(n[\"type\"]==\"mention\" for n in d))" | grep -q True' && ok "mastouser's reply notifies alice_masto" || ko "reply did not notify"
|
|
until_true 15 '[ "$(curl -s -H "$AH" "$P/api/v1/statuses/$a_post_id/context" | j "print(len(d[\"descendants\"]))")" -ge 1 ]' && ok "the reply threads under alice_masto's post" || ko "reply not threaded on PrivaPub"
|
|
curl -s -o /dev/null -X POST -H "$AH" $P/api/v1/statuses -d "status=@mastouser@mastodon.test replying from PrivaPub&in_reply_to_id=$m_post_on_p&visibility=public"
|
|
until_true 30 '[ "$(mcurl -H "$MH" "$M/api/v1/statuses/$m_post/context" | j "print(len(d[\"descendants\"]))")" -ge 1 ]' && ok "alice_masto's reply threads under mastouser's post" || ko "outbound reply not threaded on Mastodon"
|
|
|
|
echo " likes and boosts"
|
|
curl -s -o /dev/null -X POST -H "$AH" "$P/api/v1/statuses/$m_post_on_p/favourite"
|
|
curl -s -o /dev/null -X POST -H "$AH" "$P/api/v1/statuses/$m_post_on_p/reblog"
|
|
until_true 30 '[ "$(mcurl -H "$MH" "$M/api/v1/statuses/$m_post" | j "print(d[\"favourites_count\"])")" = "1" ]' && ok "alice_masto's like counts on Mastodon" || ko "like not counted on Mastodon"
|
|
until_true 30 '[ "$(mcurl -H "$MH" "$M/api/v1/statuses/$m_post" | j "print(d[\"reblogs_count\"])")" = "1" ]' && ok "alice_masto's boost counts on Mastodon" || ko "boost not counted on Mastodon"
|
|
curl -s -o /dev/null -X POST -H "$AH" "$P/api/v1/statuses/$m_post_on_p/unfavourite"
|
|
curl -s -o /dev/null -X POST -H "$AH" "$P/api/v1/statuses/$m_post_on_p/unreblog"
|
|
until_true 30 '[ "$(mcurl -H "$MH" "$M/api/v1/statuses/$m_post" | j "print(d[\"favourites_count\"], d[\"reblogs_count\"])")" = "0 0" ]' && ok "alice_masto's unlike and unboost reach Mastodon" || ko "undo of like or boost not applied on Mastodon"
|
|
mcurl -o /dev/null -X POST -H "$MH" "$M/api/v1/statuses/$a_post_on_m/favourite"
|
|
mcurl -o /dev/null -X POST -H "$MH" "$M/api/v1/statuses/$a_post_on_m/reblog"
|
|
until_true 30 '[ "$(curl -s -H "$AH" "$P/api/v1/statuses/$a_post_id" | j "print(d[\"favourites_count\"], d[\"reblogs_count\"])")" = "1 1" ]' && ok "mastouser's like and boost count on PrivaPub" || ko "like or boost not counted on PrivaPub"
|
|
mcurl -o /dev/null -X POST -H "$MH" "$M/api/v1/statuses/$a_post_on_m/unfavourite"
|
|
mcurl -o /dev/null -X POST -H "$MH" "$M/api/v1/statuses/$a_post_on_m/unreblog"
|
|
until_true 30 '[ "$(curl -s -H "$AH" "$P/api/v1/statuses/$a_post_id" | j "print(d[\"favourites_count\"], d[\"reblogs_count\"])")" = "0 0" ]' && ok "mastouser's unlike and unboost reach PrivaPub" || ko "undo of like or boost not applied on PrivaPub"
|
|
|
|
echo " direct messages"
|
|
curl -s -o /dev/null -X POST -H "$AH" $P/api/v1/statuses -d 'status=@mastouser@mastodon.test a secret for Mastodon&visibility=direct'
|
|
until_true 30 'mcurl -H "$MH" "$M/api/v1/conversations" | grep -q "a secret for Mastodon"' && ok "alice_masto's DM reaches mastouser" || ko "DM missing on Mastodon"
|
|
mcurl -o /dev/null -X POST -H "$MH" "$M/api/v1/statuses" -d 'status=@alice_masto@privapub.test a secret for PrivaPub&visibility=direct'
|
|
until_true 30 'curl -s -H "$AH" "$P/api/v1/conversations" | grep -q "a secret for PrivaPub"' && ok "mastouser's DM reaches alice_masto" || ko "DM missing on PrivaPub"
|
|
! curl -s "$P/api/v1/timelines/public" | grep -q "a secret for PrivaPub" && ok "the DM is not on PrivaPub's public timeline" || ko "DM leaked to the public timeline"
|
|
|
|
echo " polls"
|
|
a_poll_uri=$(curl -s -X POST -H "$AH" $P/api/v1/statuses -d 'status=cats or dogs&visibility=public&poll[options][]=cats&poll[options][]=dogs&poll[expires_in]=3600' | j "print(d['uri'])")
|
|
until_true 30 '[ "$(m_status_by_uri "$alice_on_m" "$a_poll_uri" | j "print(len(d[\"poll\"][\"options\"]))")" = "2" ]' && ok "alice_masto's poll reaches Mastodon as a poll" || ko "poll missing on Mastodon"
|
|
a_poll_on_m=$(m_status_by_uri "$alice_on_m" "$a_poll_uri" | j "print(d['poll']['id'])")
|
|
mcurl -o /dev/null -X POST -H "$MH" "$M/api/v1/polls/$a_poll_on_m/votes" -d 'choices[]=1'
|
|
a_poll_id=$(curl -s -H "$AH" "$P/api/v1/accounts/verify_credentials" | j "print(d['id'])")
|
|
until_true 30 '[ "$(curl -s -H "$AH" "$P/api/v1/accounts/$a_poll_id/statuses" | j "print(next(s[\"poll\"][\"options\"][1][\"votes_count\"] for s in d if s[\"uri\"]==\"$a_poll_uri\"))")" = "1" ]' && ok "mastouser's vote counts on PrivaPub" || ko "vote not counted on PrivaPub"
|
|
m_poll=$(mcurl -X POST -H "$MH" "$M/api/v1/statuses" -d 'status=tea or coffee, Mastodon asks&visibility=public&poll[options][]=tea&poll[options][]=coffee&poll[expires_in]=3600' | j "print(d['id'])")
|
|
until_true 30 'curl -s -H "$AH" "$P/api/v1/timelines/home" | j "print(any(s[\"poll\"] and \"Mastodon asks\" in s[\"content\"] for s in d))" | grep -q True' && ok "mastouser's poll reaches PrivaPub as a poll" || ko "poll missing on PrivaPub"
|
|
m_poll_on_p=$(curl -s -H "$AH" "$P/api/v1/timelines/home" | j "print(next(s['poll']['id'] for s in d if s['poll'] and 'Mastodon asks' in s['content']))")
|
|
curl -s -o /dev/null -X POST -H "$AH" "$P/api/v1/polls/$m_poll_on_p/votes" -d 'choices[]=0'
|
|
until_true 30 '[ "$(mcurl -H "$MH" "$M/api/v1/statuses/$m_poll" | j "print(d[\"poll\"][\"options\"][0][\"votes_count\"])")" = "1" ]' && ok "alice_masto's vote counts on Mastodon" || ko "vote not counted on Mastodon"
|
|
|
|
echo " quotes"
|
|
q_target=$(mcurl -X POST -H "$MH" "$M/api/v1/statuses" -d 'status=quote me if you like&visibility=public' | j "print(d['id'])")
|
|
until_true 30 'curl -s -H "$AH" "$P/api/v1/timelines/home" | grep -q "quote me if you like"' || true
|
|
q_target_on_p=$(curl -s -H "$AH" "$P/api/v1/timelines/home" | j "print(next(s['id'] for s in d if 'quote me if you like' in s['content']))")
|
|
a_quote=$(curl -s -X POST -H "$AH" $P/api/v1/statuses -d "status=quoting Mastodon&visibility=public"ed_status_id=$q_target_on_p" | j "print(d['id'])")
|
|
until_true 30 '[ "$(curl -s -H "$AH" "$P/api/v1/statuses/$a_quote" | j "print((d.get(\"quote\") or {}).get(\"state\"))")" = "accepted" ]' && ok "Mastodon approves alice_masto's quote (FEP-044f)" || ko "quote of a Mastodon post not approved"
|
|
m_quote=$(mcurl -X POST -H "$MH" "$M/api/v1/statuses" -d "status=quoting PrivaPub&visibility=public"ed_status_id=$a_post_on_m" | j "print(d['id'])")
|
|
until_true 30 '[ "$(mcurl -H "$MH" "$M/api/v1/statuses/$m_quote" | j "print((d.get(\"quote\") or {}).get(\"state\"))")" = "accepted" ]' && ok "PrivaPub approves mastouser's quote (FEP-044f)" || ko "quote of a PrivaPub post not approved on Mastodon"
|
|
|
|
echo " media"
|
|
make_png "$work/red.png"
|
|
a_media=$(curl -s -X POST -H "$AH" "$P/api/v2/media" -F "file=@$work/red.png;type=image/png" -F 'description=a red square' | j "print(d['id'])")
|
|
a_media_uri=$(curl -s -X POST -H "$AH" $P/api/v1/statuses -d "status=a picture&visibility=public&media_ids[]=$a_media" | j "print(d['uri'])")
|
|
until_true 30 '[ "$(m_status_by_uri "$alice_on_m" "$a_media_uri" | j "print(d[\"media_attachments\"][0][\"description\"])")" = "a red square" ]' && ok "an image with alt text reaches Mastodon" || ko "image or alt text missing on Mastodon"
|
|
m_media=$(mcurl -X POST -H "$MH" "$M/api/v2/media" -F "file=@$work/red.png;type=image/png" -F 'description=a red square from Mastodon' | j "print(d['id'])")
|
|
until_true 10 '[ "$(mcurl -o /dev/null -w "%{http_code}" -H "$MH" "$M/api/v1/media/$m_media")" = "200" ]' || true
|
|
mcurl -o /dev/null -X POST -H "$MH" "$M/api/v1/statuses" -d "status=a picture from Mastodon&visibility=public&media_ids[]=$m_media"
|
|
until_true 30 'curl -s -H "$AH" "$P/api/v1/timelines/home" | j "print(any(\"a picture from Mastodon\" in s[\"content\"] and s[\"media_attachments\"] and \"/media/proxy/\" in s[\"media_attachments\"][0][\"url\"] and s[\"media_attachments\"][0][\"description\"]==\"a red square from Mastodon\" for s in d))" | grep -q True' \
|
|
&& ok "an image with alt text from Mastodon arrives through our proxy" || ko "Mastodon's image missing, unproxied or without alt text"
|
|
|
|
echo " edits and deletes"
|
|
curl -s -o /dev/null -X PUT -H "$AH" "$P/api/v1/statuses/$a_post_id" -d 'status=Hello Mastodon from PrivaPub, edited'
|
|
until_true 30 'mcurl -H "$MH" "$M/api/v1/statuses/$a_post_on_m" | grep -q "edited"' && ok "alice_masto's edit reaches Mastodon" || ko "edit not applied on Mastodon"
|
|
mcurl -o /dev/null -X PUT -H "$MH" "$M/api/v1/statuses/$m_post" -d 'status=Hello PrivaPub from Mastodon, edited'
|
|
until_true 30 '[ "$(curl -s -H "$AH" "$P/api/v1/statuses/$m_post_on_p/history" | j "print(len(d))")" = "2" ]' && ok "mastouser's edit reaches PrivaPub with its history" || ko "Mastodon's edit not applied on PrivaPub"
|
|
cw_on_m=$(m_status_by_uri "$alice_on_m" "$cw_uri" | j "print(d['id'])")
|
|
cw_id=$(curl -s -H "$AH" "$P/api/v1/accounts/$a_poll_id/statuses" | j "print(next(s['id'] for s in d if s['uri']=='$cw_uri'))")
|
|
curl -s -o /dev/null -X DELETE -H "$AH" "$P/api/v1/statuses/$cw_id"
|
|
until_true 30 '[ "$(mcurl -o /dev/null -w "%{http_code}" -H "$MH" "$M/api/v1/statuses/$cw_on_m")" = "404" ]' && ok "alice_masto's delete reaches Mastodon" || ko "delete not applied on Mastodon"
|
|
[ "$(pfetch -o /dev/null -w '%{http_code}' -H 'Accept: application/activity+json' "$cw_uri")" = "410" ] && ok "the deleted post answers 410" || ko "deleted post does not answer 410"
|
|
mcurl -o /dev/null -X DELETE -H "$MH" "$M/api/v1/statuses/$m_poll"
|
|
until_true 30 '! curl -s -H "$AH" "$P/api/v1/timelines/home" | grep -q "Mastodon asks"' && ok "mastouser's delete reaches PrivaPub" || ko "Mastodon's delete not applied on PrivaPub"
|
|
|
|
echo " locked follows"
|
|
mcurl -o /dev/null -X PATCH -H "$MH" "$M/api/v1/accounts/update_credentials" -d 'locked=true'
|
|
BT=$(privapub_token bob_masto); BH="Authorization: Bearer $BT"
|
|
masto_on_p_b=$(curl -s -H "$BH" "$P/api/v2/search?q=mastouser@mastodon.test&resolve=true&type=accounts" | j "print(d['accounts'][0]['id'])")
|
|
curl -s -o /dev/null -X POST -H "$BH" "$P/api/v1/accounts/$masto_on_p_b/follow"
|
|
until_true 30 'mcurl -H "$MH" "$M/api/v1/follow_requests" | j "print(any(a[\"acct\"]==\"bob_masto@privapub.test\" for a in d))" | grep -q True' && ok "bob_masto's follow waits as a request on locked mastouser" || ko "follow request missing on Mastodon"
|
|
bob_on_m=$(mcurl -H "$MH" "$M/api/v1/follow_requests" | j "print(next(a['id'] for a in d if a['acct']=='bob_masto@privapub.test'))")
|
|
mcurl -o /dev/null -X POST -H "$MH" "$M/api/v1/follow_requests/$bob_on_m/authorize"
|
|
until_true 30 '[ "$(curl -s -H "$BH" "$P/api/v1/accounts/relationships?id[]=$masto_on_p_b" | j "print(d[0][\"following\"])")" = "True" ]' && ok "mastouser's approval reaches PrivaPub" || ko "approval not applied on PrivaPub"
|
|
|
|
echo " circles"
|
|
jwt=$(privapub_root)
|
|
alice_id=$(curl -s -H "$AH" "$P/api/v1/accounts/verify_credentials" | j "print(d['id'])")
|
|
circle_name="circle$(date +%s)"
|
|
circle=$(curl -s -X POST $P/clientapi/group/insert -H 'Content-Type: application/json' -H "Authorization: Bearer $jwt" \
|
|
-d "{\"avatarId\":\"$alice_id\",\"userName\":\"$circle_name\",\"name\":\"a circle\",\"description\":\"just us\",\"isCommunity\":false}" | j "print(d['id'])")
|
|
circle_on_m=$(mcurl -H "$MH" "$M/api/v2/search?q=@$circle_name@privapub.test&resolve=true&type=accounts" | j "print(d['accounts'][0]['id'])")
|
|
mcurl -o /dev/null -X POST -H "$MH" "$M/api/v1/accounts/$circle_on_m/follow"
|
|
until_true 15 '[ "$(mcurl -H "$MH" "$M/api/v1/accounts/relationships?id[]=$circle_on_m" | j "print(d[0][\"requested\"])")" = "True" ]' && ok "mastouser's request to join a circle waits for its owner" || ko "joining the circle was not held for approval"
|
|
# Mastodon 4.7 names its actors by number (https://mastodon.test/ap/users/<id>), so the pending request says who asked
|
|
requester=$(podman exec pasture-mongo mongosh --quiet PrivaPub --eval "print(db.Follower.findOne({LocalActorId:'$circle', IsAccepted:false}).ActorURI)")
|
|
curl -s -o /dev/null -X POST $P/clientapi/group/approve -H 'Content-Type: application/json' -H "Authorization: Bearer $jwt" \
|
|
-d "{\"avatarId\":\"$alice_id\",\"groupId\":\"$circle\",\"memberActorURI\":\"$requester\"}"
|
|
until_true 30 '[ "$(mcurl -H "$MH" "$M/api/v1/accounts/relationships?id[]=$circle_on_m" | j "print(d[0][\"following\"])")" = "True" ]' && ok "the owner's approval makes mastouser a circle member" || ko "circle approval did not reach Mastodon"
|
|
circle_post=$(curl -s -X POST $P/clientapi/post/insert -H 'Content-Type: application/json' -H "Authorization: Bearer $jwt" \
|
|
-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 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; }
|
|
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'])")
|
|
[ "$(mcurl -H "Authorization: Bearer $OT" "$M/api/v1/accounts/$alice_on_m_o/statuses?limit=40" | j "print(any(s['uri']=='$circle_uri' for s in d))")" = "False" ] \
|
|
&& ok "another Mastodon user does not see the circle post" || ko "the circle post leaked to a non-member"
|
|
[ "$(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 " reports"
|
|
curl -s -o /dev/null -X POST -H "$AH" "$P/api/v1/reports" -d "account_id=$masto_on_p&comment=pasture-report-$circle_name&forward=true"
|
|
until_true 30 'mcurl -H "$MH" "$M/api/v1/admin/reports" | grep -q "pasture-report-$circle_name"' && ok "a report reaches Mastodon's moderators" || ko "report missing on Mastodon"
|
|
[ "$(mcurl -H "$MH" "$M/api/v1/admin/reports" | j "print(next((r['account']['username'] for r in d if 'pasture-report-$circle_name' in r['comment']), ''))")" != "alice_masto" ] \
|
|
&& ok "the report does not come from the reporting persona" || ko "the report names the reporting persona"
|
|
|
|
echo " blocks and unfollows"
|
|
curl -s -o /dev/null -X POST -H "$AH" "$P/api/v1/accounts/$masto_on_p/unfollow"
|
|
until_true 30 '[ "$(mcurl -H "$MH" "$M/api/v1/accounts/relationships?id[]=$alice_on_m" | j "print(d[0][\"followed_by\"])")" = "False" ]' && ok "alice_masto's unfollow reaches Mastodon" || ko "unfollow not applied on Mastodon"
|
|
curl -s -o /dev/null -X POST -H "$AH" "$P/api/v1/accounts/$masto_on_p/block"
|
|
until_true 30 '[ "$(mcurl -H "$MH" "$M/api/v1/accounts/relationships?id[]=$alice_on_m" | j "print(d[0][\"blocked_by\"])")" = "True" ]' && ok "alice_masto's block reaches Mastodon" || ko "block not applied on Mastodon"
|
|
curl -s -o /dev/null -X POST -H "$AH" "$P/api/v1/accounts/$masto_on_p/unblock"
|
|
mcurl -o /dev/null -X POST -H "$MH" "$M/api/v1/accounts/$alice_on_m/block"
|
|
sleep 5; xf "Mastodon's block is enforced on PrivaPub (inbound Block is P7)"
|
|
|
|
echo " statistics"
|
|
stats_check mastodon.test mastodon
|