# 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