- FEDERATION.md:
- It said only GoToSocial was tested live. It now lists all six pasture peers, run with signed fetches required as
production runs, and what is checked both ways.
- FEP-044f, FEP-9967, FEP-c0e0 and FEP-5feb moved from planned to supported.
- indexable and discoverable are described as the settings they are.
- ROADMAP: v1.19.1 deployed and verified, v1.20.0 (phase 4).
- Pasture, Akkoma: its like and boost count gets two minutes. In the final clean pass, all six peers with SecureMode on
gave 245 passed, 1 failed, 4 expected. The failure was this count, which Akkoma's job queue was slow to update
under the load of six peers; two reruns gave 44/44.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ELjqpznMFMNrJoJUj6K5p2
144 lines
16 KiB
Bash
144 lines
16 KiB
Bash
# Akkoma 3.20: discovery, follows, posts, CW, replies both ways, likes and boosts with their undos, emoji reactions
|
|
# both ways, DMs, polls, quotes both ways, edits with their history, deletes, media with alt text, the published time
|
|
# kept, blocks, statistics. Akkoma speaks the Mastodon API, with reactions under /api/v1/pleroma.
|
|
AK=https://akkoma.test:6443
|
|
acurl() { curl -sk --resolve akkoma.test:6443:127.0.0.1 "$@"; }
|
|
KT=$(cat "$here/.state/akkoma.token" 2>/dev/null)
|
|
KH="Authorization: Bearer $KT"
|
|
# what Akkoma holds of an account, newest first; and the one status whose uri is given
|
|
a_statuses() { acurl -H "$KH" "$AK/api/v1/accounts/$1/statuses?limit=40"; }
|
|
a_status_by_uri() { a_statuses "$1" | j "print(json.dumps(next((s for s in d if s['uri']=='$2'), None)))"; }
|
|
# a PrivaPub status in alice_akkoma's home whose text has the given words
|
|
p_home_id() { curl -s -H "$PH" "$P/api/v1/timelines/home?limit=40" | j "print(next((s['id'] for s in d if '$1' in s['content']), ''))"; }
|
|
|
|
echo "akkoma"
|
|
[ -n "$KT" ] && ok "Akkoma token for akuser" || { ko "Akkoma token"; return 1; }
|
|
PT=$(privapub_token alice_akkoma)
|
|
PH="Authorization: Bearer $PT"
|
|
[ -n "$PT" ] && ok "PrivaPub token for alice_akkoma" || { ko "PrivaPub token for alice_akkoma"; return 1; }
|
|
alice_self=$(curl -s -H "$PH" "$P/api/v1/accounts/verify_credentials" | j "print(d['id'])")
|
|
|
|
echo " discovery"
|
|
alice_on_a=$(acurl -H "$KH" "$AK/api/v2/search?q=@alice_akkoma@privapub.test&resolve=true&type=accounts" | j "print(d['accounts'][0]['id'])")
|
|
[ -n "$alice_on_a" ] && ok "Akkoma resolves @alice_akkoma@privapub.test" || ko "Akkoma cannot resolve alice_akkoma"
|
|
ak_on_p=$(curl -s -H "$PH" "$P/api/v2/search?q=akuser@akkoma.test&resolve=true&type=accounts" | j "print(d['accounts'][0]['id'])")
|
|
[ -n "$ak_on_p" ] && ok "PrivaPub resolves @akuser@akkoma.test" || ko "PrivaPub cannot resolve akuser"
|
|
|
|
echo " follows"
|
|
acurl -o /dev/null -X POST -H "$KH" "$AK/api/v1/accounts/$alice_on_a/follow"
|
|
until_true 30 '[ "$(acurl -H "$KH" "$AK/api/v1/accounts/relationships?id[]=$alice_on_a" | j "print(d[0][\"following\"])")" = "True" ]' && ok "akuser follows alice_akkoma (Accept arrived)" || ko "Akkoma's follow not accepted"
|
|
curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/accounts/$ak_on_p/follow"
|
|
until_true 30 '[ "$(curl -s -H "$PH" "$P/api/v1/accounts/relationships?id[]=$ak_on_p" | j "print(d[0][\"following\"])")" = "True" ]' && ok "alice_akkoma follows akuser (Accept arrived)" || ko "PrivaPub's follow not accepted"
|
|
|
|
echo " posts"
|
|
p_post=$(curl -s -X POST -H "$PH" $P/api/v1/statuses -d 'status=Hello Akkoma from PrivaPub&visibility=public')
|
|
p_post_id=$(echo "$p_post" | j "print(d['id'])"); p_post_uri=$(echo "$p_post" | j "print(d['uri'])")
|
|
until_true 30 '[ "$(a_status_by_uri "$alice_on_a" "$p_post_uri")" != "null" ]' && ok "alice_akkoma's post reaches Akkoma" || ko "post missing on Akkoma"
|
|
p_post_on_a=$(a_status_by_uri "$alice_on_a" "$p_post_uri" | j "print(d['id'])")
|
|
a_post=$(acurl -X POST -H "$KH" "$AK/api/v1/statuses" -d 'status=Hello PrivaPub from Akkoma&visibility=public')
|
|
a_post_id=$(echo "$a_post" | j "print(d['id'])")
|
|
until_true 30 '[ -n "$(p_home_id "Hello PrivaPub from Akkoma")" ]' && ok "akuser's post reaches alice_akkoma's home" || ko "Akkoma's post missing on PrivaPub"
|
|
a_post_on_p=$(p_home_id "Hello PrivaPub from Akkoma")
|
|
a_created=$(echo "$a_post" | j "print(d['created_at'][:19])")
|
|
[ "$(curl -s -H "$PH" "$P/api/v1/statuses/$a_post_on_p" | j "print(d['created_at'][:19])")" = "$a_created" ] \
|
|
&& ok "akuser's post keeps its published time" || ko "published time changed on PrivaPub"
|
|
cw_uri=$(curl -s -X POST -H "$PH" $P/api/v1/statuses -d 'status=behind a warning&spoiler_text=spoilers&visibility=public' | j "print(d['uri'])")
|
|
until_true 30 '[ "$(a_status_by_uri "$alice_on_a" "$cw_uri" | j "print(d and d[\"spoiler_text\"]==\"spoilers\" and d[\"sensitive\"])")" = "True" ]' && ok "a content warning survives to Akkoma" || ko "content warning lost on Akkoma"
|
|
fo_uri=$(curl -s -X POST -H "$PH" $P/api/v1/statuses -d 'status=only for followers&visibility=private' | j "print(d['uri'])")
|
|
until_true 30 '[ "$(a_status_by_uri "$alice_on_a" "$fo_uri" | j "print(d and d[\"visibility\"])")" = "private" ]' && ok "a followers-only post reaches Akkoma as private" || ko "followers-only post missing or widened on Akkoma"
|
|
|
|
echo " replies"
|
|
acurl -o /dev/null -X POST -H "$KH" "$AK/api/v1/statuses" -d "status=@alice_akkoma@privapub.test replying from Akkoma&in_reply_to_id=$p_post_on_a&visibility=public"
|
|
until_true 30 'curl -s -H "$PH" "$P/api/v1/notifications" | j "print(any(n[\"type\"]==\"mention\" for n in d))" | grep -q True' && ok "akuser's reply notifies alice_akkoma" || ko "reply did not notify"
|
|
until_true 15 '[ "$(curl -s -H "$PH" "$P/api/v1/statuses/$p_post_id/context" | j "print(len(d[\"descendants\"]))")" -ge 1 ]' && ok "the reply threads under alice_akkoma's post" || ko "reply not threaded on PrivaPub"
|
|
curl -s -o /dev/null -X POST -H "$PH" $P/api/v1/statuses -d "status=@akuser@akkoma.test replying from PrivaPub&in_reply_to_id=$a_post_on_p&visibility=public"
|
|
until_true 30 '[ "$(acurl -H "$KH" "$AK/api/v1/statuses/$a_post_id/context" | j "print(len(d[\"descendants\"]))")" -ge 1 ]' && ok "alice_akkoma's reply threads under akuser's post" || ko "outbound reply not threaded on Akkoma"
|
|
|
|
echo " likes, boosts and reactions"
|
|
curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/statuses/$a_post_on_p/favourite"
|
|
curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/statuses/$a_post_on_p/reblog"
|
|
# Akkoma counts them from its own job queue, which can lag behind under the load of a six-peer run: two minutes
|
|
until_true 60 '[ "$(acurl -H "$KH" "$AK/api/v1/statuses/$a_post_id" | j "print(d[\"favourites_count\"], d[\"reblogs_count\"])")" = "1 1" ]' && ok "alice_akkoma's like and boost count on Akkoma" || ko "like or boost not counted on Akkoma"
|
|
curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/statuses/$a_post_on_p/unfavourite"
|
|
curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/statuses/$a_post_on_p/unreblog"
|
|
until_true 30 '[ "$(acurl -H "$KH" "$AK/api/v1/statuses/$a_post_id" | j "print(d[\"favourites_count\"], d[\"reblogs_count\"])")" = "0 0" ]' && ok "alice_akkoma's unlike and unboost reach Akkoma" || ko "undo of like or boost not applied on Akkoma"
|
|
acurl -o /dev/null -X POST -H "$KH" "$AK/api/v1/statuses/$p_post_on_a/favourite"
|
|
acurl -o /dev/null -X POST -H "$KH" "$AK/api/v1/statuses/$p_post_on_a/reblog"
|
|
until_true 30 '[ "$(curl -s -H "$PH" "$P/api/v1/statuses/$p_post_id" | j "print(d[\"favourites_count\"], d[\"reblogs_count\"])")" = "1 1" ]' && ok "akuser's like and boost count on PrivaPub" || ko "like or boost not counted on PrivaPub"
|
|
acurl -o /dev/null -X POST -H "$KH" "$AK/api/v1/statuses/$p_post_on_a/unfavourite"
|
|
acurl -o /dev/null -X POST -H "$KH" "$AK/api/v1/statuses/$p_post_on_a/unreblog"
|
|
until_true 30 '[ "$(curl -s -H "$PH" "$P/api/v1/statuses/$p_post_id" | j "print(d[\"favourites_count\"], d[\"reblogs_count\"])")" = "0 0" ]' && ok "akuser's unlike and unboost reach PrivaPub" || ko "undo of like or boost not applied on PrivaPub"
|
|
acurl -o /dev/null -X PUT -H "$KH" "$AK/api/v1/pleroma/statuses/$p_post_on_a/reactions/%F0%9F%8E%89"
|
|
until_true 30 '[ "$(curl -s -H "$PH" "$P/api/v1/statuses/$p_post_id" | j "print(any(r[\"name\"]==\"🎉\" and r[\"count\"]==1 for r in d.get(\"emoji_reactions\") or []))")" = "True" ]' \
|
|
&& ok "akuser's 🎉 (EmojiReact) arrives as a reaction" || ko "Akkoma's reaction missing on PrivaPub"
|
|
acurl -o /dev/null -X DELETE -H "$KH" "$AK/api/v1/pleroma/statuses/$p_post_on_a/reactions/%F0%9F%8E%89"
|
|
until_true 30 '[ "$(curl -s -H "$PH" "$P/api/v1/statuses/$p_post_id" | j "print(len(d.get(\"emoji_reactions\") or []))")" = "0" ]' && ok "akuser's withdrawn reaction is gone" || ko "withdrawn reaction still on PrivaPub"
|
|
curl -s -o /dev/null -X PUT -H "$PH" "$P/api/v1/pleroma/statuses/$a_post_on_p/reactions/%F0%9F%91%8D"
|
|
until_true 30 '[ "$(acurl -H "$KH" "$AK/api/v1/statuses/$a_post_id" | j "print(any(r[\"name\"]==\"👍\" and r[\"count\"]==1 for r in (d.get(\"pleroma\") or {}).get(\"emoji_reactions\") or d.get(\"emoji_reactions\") or []))")" = "True" ]' \
|
|
&& ok "alice_akkoma's 👍 is a reaction on Akkoma" || ko "PrivaPub's reaction missing on Akkoma"
|
|
curl -s -o /dev/null -X DELETE -H "$PH" "$P/api/v1/pleroma/statuses/$a_post_on_p/reactions/%F0%9F%91%8D"
|
|
until_true 30 '[ "$(acurl -H "$KH" "$AK/api/v1/statuses/$a_post_id" | j "print(len((d.get(\"pleroma\") or {}).get(\"emoji_reactions\") or d.get(\"emoji_reactions\") or []))")" = "0" ]' && ok "alice_akkoma's withdrawn reaction is gone from Akkoma" || ko "withdrawn reaction still on Akkoma"
|
|
|
|
echo " direct messages"
|
|
curl -s -o /dev/null -X POST -H "$PH" $P/api/v1/statuses -d 'status=@akuser@akkoma.test a secret for Akkoma&visibility=direct'
|
|
until_true 30 'acurl -H "$KH" "$AK/api/v1/conversations" | grep -q "a secret for Akkoma"' && ok "alice_akkoma's DM reaches akuser" || ko "DM missing on Akkoma"
|
|
# Akkoma's Linkify never takes @user@host.test for a mention, so its DM names alice with Pleroma's to[] instead
|
|
acurl -o /dev/null -X POST -H "$KH" "$AK/api/v1/statuses" -d 'status=a secret for PrivaPub&visibility=direct&to[]=alice_akkoma@privapub.test'
|
|
until_true 30 'curl -s -H "$PH" "$P/api/v1/conversations" | grep -q "a secret for PrivaPub"' && ok "akuser's DM reaches alice_akkoma" || 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"
|
|
p_poll_uri=$(curl -s -X POST -H "$PH" $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 '[ "$(a_status_by_uri "$alice_on_a" "$p_poll_uri" | j "print(len(d[\"poll\"][\"options\"]))")" = "2" ]' && ok "alice_akkoma's poll reaches Akkoma as a poll" || ko "poll missing on Akkoma"
|
|
p_poll_on_a=$(a_status_by_uri "$alice_on_a" "$p_poll_uri" | j "print(d['poll']['id'])")
|
|
acurl -o /dev/null -X POST -H "$KH" "$AK/api/v1/polls/$p_poll_on_a/votes" -d 'choices[]=1'
|
|
until_true 30 '[ "$(curl -s -H "$PH" "$P/api/v1/accounts/$alice_self/statuses" | j "print(next(s[\"poll\"][\"options\"][1][\"votes_count\"] for s in d if s[\"uri\"]==\"$p_poll_uri\"))")" = "1" ]' && ok "akuser's vote counts on PrivaPub" || ko "vote not counted on PrivaPub"
|
|
a_poll=$(acurl -X POST -H "$KH" "$AK/api/v1/statuses" -d 'status=tea or coffee, Akkoma asks&visibility=public&poll[options][]=tea&poll[options][]=coffee&poll[expires_in]=3600' | j "print(d['id'])")
|
|
until_true 30 'curl -s -H "$PH" "$P/api/v1/timelines/home" | j "print(any(s[\"poll\"] and \"Akkoma asks\" in s[\"content\"] for s in d))" | grep -q True' && ok "akuser's poll reaches PrivaPub as a poll" || ko "poll missing on PrivaPub"
|
|
a_poll_on_p=$(curl -s -H "$PH" "$P/api/v1/timelines/home" | j "print(next(s['poll']['id'] for s in d if s['poll'] and 'Akkoma asks' in s['content']))")
|
|
curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/polls/$a_poll_on_p/votes" -d 'choices[]=0'
|
|
until_true 30 '[ "$(acurl -H "$KH" "$AK/api/v1/statuses/$a_poll" | j "print(d[\"poll\"][\"options\"][0][\"votes_count\"])")" = "1" ]' && ok "alice_akkoma's vote counts on Akkoma" || ko "vote not counted on Akkoma"
|
|
|
|
echo " quotes"
|
|
a_quote=$(acurl -X POST -H "$KH" "$AK/api/v1/statuses" -d "status=quoting PrivaPub&visibility=public"e_id=$p_post_on_a" | j "print(d['id'])")
|
|
until_true 30 'curl -s -H "$PH" "$P/api/v1/timelines/home" | j "print(any(\"quoting PrivaPub\" in s[\"content\"] and (s.get(\"quote\") or {}).get(\"state\")==\"accepted\" for s in d))" | grep -q True' \
|
|
&& ok "akuser's quote arrives as an accepted quote" || ko "Akkoma's quote not understood"
|
|
p_quote_uri=$(curl -s -X POST -H "$PH" $P/api/v1/statuses -d "status=quoting Akkoma&visibility=public"ed_status_id=$a_post_on_p" | j "print(d['uri'])")
|
|
until_true 30 '[ "$(a_status_by_uri "$alice_on_a" "$p_quote_uri" | j "print(d and ((d.get(\"quote\") or {}).get(\"id\") or d.get(\"quote_id\") or (d.get(\"pleroma\") or {}).get(\"quote_id\")) == \"$a_post_id\")")" = "True" ]' \
|
|
&& ok "alice_akkoma's quote is a quote on Akkoma" || ko "quote not understood by Akkoma"
|
|
|
|
echo " media"
|
|
make_png "$work/red.png"
|
|
p_media=$(curl -s -X POST -H "$PH" "$P/api/v2/media" -F "file=@$work/red.png;type=image/png" -F 'description=a red square' | j "print(d['id'])")
|
|
p_media_uri=$(curl -s -X POST -H "$PH" $P/api/v1/statuses -d "status=a picture&visibility=public&media_ids[]=$p_media" | j "print(d['uri'])")
|
|
until_true 30 '[ "$(a_status_by_uri "$alice_on_a" "$p_media_uri" | j "print(d[\"media_attachments\"][0][\"description\"])")" = "a red square" ]' && ok "an image with alt text reaches Akkoma" || ko "image or alt text missing on Akkoma"
|
|
a_media=$(acurl -X POST -H "$KH" "$AK/api/v1/media" -F "file=@$work/red.png;type=image/png" -F 'description=a red square from Akkoma' | j "print(d['id'])")
|
|
acurl -o /dev/null -X POST -H "$KH" "$AK/api/v1/statuses" -d "status=a picture from Akkoma&visibility=public&media_ids[]=$a_media"
|
|
until_true 30 'curl -s -H "$PH" "$P/api/v1/timelines/home" | j "print(any(\"a picture from Akkoma\" 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 Akkoma\" for s in d))" | grep -q True' \
|
|
&& ok "an image with alt text from Akkoma arrives through our proxy" || ko "Akkoma's image missing, unproxied or without alt text"
|
|
|
|
echo " edits and deletes"
|
|
curl -s -o /dev/null -X PUT -H "$PH" "$P/api/v1/statuses/$p_post_id" -d 'status=Hello Akkoma from PrivaPub, edited'
|
|
until_true 30 'acurl -H "$KH" "$AK/api/v1/statuses/$p_post_on_a" | grep -q "edited"' && ok "alice_akkoma's edit reaches Akkoma" || ko "edit not applied on Akkoma"
|
|
acurl -o /dev/null -X PUT -H "$KH" "$AK/api/v1/statuses/$a_post_id" -d 'status=Hello PrivaPub from Akkoma, edited'
|
|
until_true 30 '[ "$(curl -s -H "$PH" "$P/api/v1/statuses/$a_post_on_p/history" | j "print(len(d))")" = "2" ]' && ok "akuser's edit reaches PrivaPub with its history" || ko "Akkoma's edit not applied on PrivaPub"
|
|
cw_on_a=$(a_status_by_uri "$alice_on_a" "$cw_uri" | j "print(d['id'])")
|
|
cw_id=$(curl -s -H "$PH" "$P/api/v1/accounts/$alice_self/statuses" | j "print(next(s['id'] for s in d if s['uri']=='$cw_uri'))")
|
|
curl -s -o /dev/null -X DELETE -H "$PH" "$P/api/v1/statuses/$cw_id"
|
|
until_true 30 '[ "$(acurl -o /dev/null -w "%{http_code}" -H "$KH" "$AK/api/v1/statuses/$cw_on_a")" = "404" ]' && ok "alice_akkoma's delete reaches Akkoma" || ko "delete not applied on Akkoma"
|
|
acurl -o /dev/null -X DELETE -H "$KH" "$AK/api/v1/statuses/$a_poll"
|
|
until_true 30 '! curl -s -H "$PH" "$P/api/v1/timelines/home" | grep -q "Akkoma asks"' && ok "akuser's delete reaches PrivaPub" || ko "Akkoma's delete not applied on PrivaPub"
|
|
|
|
echo " blocks and unfollows"
|
|
curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/accounts/$ak_on_p/unfollow"
|
|
until_true 30 '[ "$(acurl -H "$KH" "$AK/api/v1/accounts/relationships?id[]=$alice_on_a" | j "print(d[0][\"followed_by\"])")" = "False" ]' && ok "alice_akkoma's unfollow reaches Akkoma" || ko "unfollow not applied on Akkoma"
|
|
curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/accounts/$ak_on_p/block"
|
|
# Akkoma never reports a remote blocker as blocked_by, so the block is read from its user_relationships (1 is a block)
|
|
a_blocked() { podman exec pasture-postgres psql -U pasture -d akkoma -tAc "select count(*) from user_relationships r join users a on a.id=r.source_id join users b on b.id=r.target_id where r.relationship_type=1 and a.nickname='alice_akkoma@privapub.test' and b.nickname='akuser'"; }
|
|
until_true 30 '[ "$(a_blocked)" = "1" ]' && ok "alice_akkoma's block reaches Akkoma" || ko "block not applied on Akkoma"
|
|
curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/accounts/$ak_on_p/unblock"
|
|
until_true 30 '[ "$(a_blocked)" = "0" ]' && ok "alice_akkoma's unblock reaches Akkoma" || ko "unblock not applied on Akkoma"
|
|
|
|
echo " statistics"
|
|
stats_check akkoma.test akkoma
|